# 指示書S — ローカルDBで動かす第4歩（`prismaSource.prechecks()` を実DB経由にする）

| 項目 | 内容 |
|---|---|
| 実行環境 | ★**Cursor 内の Claude が実装します**★（`.claude/commands/d-watch.md` の役割分担表を参照） |
| 実行状態 | ★★**実行済み（2026-09-10）**★★ — ★段階0〜段階5 をすべて実施。記録は `docs/decisions.md` の **D-105** / `docs/handoff.md` の **追記30**★ |
| 実行順 | ★**指示書R（D-099）の続き**★。★指示書M / P / R が作った `scripts/seed-local-db.ts` / `lib/db/prisma.ts` / `lib/db/local-guard.ts` / `lib/db/use-fixtures.ts` / `lib/db/ds-status.ts` / `lib/db/clean-log-mapping.ts` / `lib/format/date.ts` の上に積む作業★です。★`prismaSource.facilities()` / `schedules()` / `dsDevices()` / `dsAlerts()` / `cleanLogs()` は1文字も変えません★。★触るコードが重なる未実行の指示書は、着手直前に自分で確認すること★（作成時点では、`docs/decisions.md` / `docs/handoff.md` を共通で触る指示書と同時に走らせないこと） |
| 取得する決定番号 | ★**着手直前に `grep -nE "^### D-(0\|1)[0-9][0-9]" docs/decisions.md \| tail -3` で最終番号を実測すること**★。★作成時点の最終は `D-101`（`docs/decisions.md:4784`）なので、最初の空き番号は `D-102` の見込み★。★★`^### D-0` だけでは3桁台（D-100 以降）を拾えない★★（D-101 冒頭の注記）。★2026-09-07 に `D-068` の二重採番が実際に起きています★ |
| 前提 | ★**D-090 項6 / 項7 / 項8**★（`docs/decisions.md:3382` / `:3383` / `:3384`。★`prechecks` の未決3件★） / ★**D-091 項4**★（`:3546`。`precheck` が分前を表す予定） / ★**D-093 項2**★（`:3749`。`precheck` は任意の分数。★本指示書の設計根拠★） / ★**D-092**★（`:3599`。precheck 行の仕組み） / ★**D-098**★（`:4342`。`Standby.`） / ★**D-099**★（`:4442`。指示書R の実行記録） / ★**D-101**★（`:4784`。認証 `proxy.ts`） / **D-070 1-2**（`:1130`。`error_id` を使い string のまま） / **D-052**（`:325`。ソート順） / **D-054**（`:336`。`open` の意味） / **D-042**（`:226`。書き込み4カラム） / **D-050**（`:308`） / **D-056**（`:353`） / **D-077**（`:1847`。エラー値を変換しない） |
| 段階数 | ★**6段階**★（段階0〜段階5） |
| ★着手前確認★ | ★★**なし（すべて解決済み）**★★ — ★`launch_failed` の判定は ★**B案**★（本清掃行に紐づく `clean_status_histories` の行が無い。`cleanLogs()` の `departed` と同じ判定を共有する）で確定★（★記録: `docs/decisions.md` の **D-103**（`:5222`）★）。★B案の帰結として残っていた「エラー行が無い `launch_failed` 行の各フィールドをどう埋めるか」も **D-104** で確定★（★段階3 の「3-3b 出発失敗の行の埋め方」を見ること★）。★★新しい仕様判断は1つも要らない。ユーザーの回答を待たずに段階0 から通しで実行してよい★★ |
| 着手前の基準 | ★**着手直前に自分で `npm test` / `npx tsc --noEmit` / `npm run lint` を実行し、その実測値を基準にすること**★（D-099 の完了値は「34ファイル / 658テスト / `tsc` 0 / `lint` 0」だが、★鵜呑みにしない★） |
| 作成 | 2026-09-10（作成時点の決定事項最終番号 **D-101** / `docs/handoff.md` の最新は **追記29**（`docs/handoff.md:2342`）） |

> 実行後はこの表と、文書末尾の「実行状態」表を更新してください。

> ★**この指示書を読んでいるあなたへ**★ — `.claude/commands/d-watch.md` の「実装は Cursor 側」は
> **2つの環境の役割分担**です。**Cursor の中で動いている Claude は実装担当**です。
> ★調査や改訂ではなく実装を行ってください。★
> ★★**うまくいかなければ途中で止めて構いません**★★ — ただし ★本指示書は API契約と型を変えます★。
> ★段階1 まで進めて止めた場合、`USE_FIXTURES` を戻すだけでは元に戻りません★。
> ★止めるなら段階の切れ目（コミット単位）で止めること★。

---

## ★★冒頭に読むべきこと — なぜ `prechecks` 1本だけなのか★★

★**本指示書の範囲は `prismaSource.prechecks()` の1本だけです。**★
★★サマリ系4本（`reportKpis` / `facilityPerformance` / `errorBreakdown` / `continuousProblems`）には
手を付けません。★★

★D-090 項2（`docs/decisions.md:3349` 付近の分割表）の当初計画では、第4歩を
「`prechecks` ＋ サマリ3本」の4本立てにしていました。★**サマリ3本を外します**★。理由は3つ:

| # | 理由 |
|---|---|
| ★1★ | ★★`prechecks` は API契約（`d-watch-api.yaml`）と型（`lib/types/index.ts`）の変更を伴う★★ — ★他の7メソッドが「既存の契約に合わせて DB から値を作る」作業なのに対し、`prechecks` だけは「契約そのものが実データを表現できない」ことが分かっている★（D-093 項2）。★性質がまったく違うので同じ指示書に混ぜない★ |
| ★2★ | ★サマリ系は「サマリテーブルを読む＋当期（進行中の週/月/年）をその場集計する」という**共通構造**★（`CLAUDE.md` §4.7。★進行中の期間はサマリテーブルに存在しない★）。★4本を1本の指示書にまとめたほうが、当期集計の置き場所（D-090 項14 / 項15）を1回で決められる★ |
| ★3★ | ★`continuousProblems` は `cleanLogs` の連続日数算出に依存する★（D-090 項2 の分割方針）。★`lib/db/clean-log-mapping.ts:112` の `computeConsecutiveProblemDays()` を流用する話になるので、サマリ系と一緒に扱うのが自然★ |

★→ サマリ系4本は **第5歩**（別の指示書）で扱います★。

---

## ★★この指示書の核心 — `severity` の型を変える必要がある★★

### 現状（★すべて実測。2026-09-10★）

| 箇所 | 実測内容 |
|---|---|
| ★`lib/types/index.ts:124`★ | `export type PrecheckSeverity = "precheck_1h" \| "precheck_30m" \| "launch_failed";` — ★3値固定★（`:140` の `severity` フィールドで使用） |
| ★`d-watch-api.yaml:1156`★ | ★`enum: [precheck_1h, precheck_30m, launch_failed]`★（`PrecheckItem` は `:1149` から） |
| ★`components/features/alerts/AlertsScreen.tsx:20-24`★ | `SEVERITY_LABEL` が ★`:21` `precheck_1h: "1h前異常"` / `:22` `precheck_30m: "30分前異常"` / `:23` `launch_failed: "出発失敗"`★。★固定ラベル★（`:156` で `t(SEVERITY_LABEL[item.severity])` として使用） |

> ★注意★: ★D-093 項2-1 は `lib/types/index.ts:118` / `d-watch-api.yaml:1072` と記録しているが★その後の編集で行がずれている★。★上の実測値が正。**行番号は必ず自分で取り直すこと**★。

### なぜ変えるのか

- ★**D-093 項2（`docs/decisions.md:3749`。ユーザー回答）**★: `clean_timetables.precheck` は
  「0 / 60 / 30」の3値ではなく、★**設定値に応じた任意の分数**★。★`20` なら20分前★
- ★つまり `precheck = 20` の行が来ても、いまの3値では表現できない★
- ★★現行DDLの `clean_timetables.precheck` は `tinyint unsigned`（0〜255）なので、
  値が 1/2 から 60/30/20 に変わっても **DB 側の型変更は不要**★★（D-094 で確認済み）。
  ★★変えるのは当方の API契約と型のほう★★

### ★推奨する設計（D-093 項2-2 の案A）★

| 項目 | 変更後 |
|---|---|
| `PrecheckSeverity` | ★`"precheck" \| "launch_failed"` の**2値**★ |
| ★新フィールド★ | ★`precheck_minutes: number \| null`★ — ★「何分前か」を**数値**で持つ★ |
| ★`launch_failed` のとき★ | ★`precheck_minutes` は **`null`**★ |
| 画面 | ★`${n}分前異常` と**組み立てる**★（固定ラベルをやめる） |

★案B（`precheck_20` のような enum を増やす）★ / ★案C（60/30 以外をその他に丸める）★は
★D-093 項2-2 で「採らない」と結論済み★。★蒸し返さないこと★。

### ★i18n の注意★

- ★`lib/i18n.ts` はキーが**日本語原文の完全一致**なので、数値が混ざる文言はそのままキーにできない★
- ★★既にある「方式A（プレースホルダ + replace）」に従うこと★★ —
  ★`lib/i18n.ts:86-87`★ が方式Aを ★推奨★ として説明している
  （`t("他{n}件を表示 →").replace("{n}", String(n))`。実例は ★`:96`★）。
  ★方式B（前後を分割して連結）は `:92-95` で「1文字キーは他画面の文言と衝突しやすい」として非推奨★
- ★同種の前例がすぐ隣にある★: ★`lib/i18n.ts:158` `"第{n}回チェック": "Check #{n}"` /
  `:159` `"{n}分前": "{n} min before"`★。★`"{n}分前異常"` を足すのが自然★
- ★★`"1h前異常"` / `"30分前異常"` / `"出発失敗"` は現在 `lib/i18n.ts` に**キーが無い**★★
  （実測。grep でヒット0件。D-093 項2-3 が指摘済み）。
  ★`AlertsScreen.tsx:156` が `t()` を通しているのに対訳が無く、★EN でも日本語のまま出る★★。
  ★新しい文言を作るついでに対訳も入れること★
- ★★同じ画面の `"残量"`（`AlertsScreen.tsx:167`）/ `"検知"`（`:171`）にもキーが無い★★（実測）。
  ★ついでに入れてよい（1行ずつなので巻き込み事故のリスクは小さい）★

---

## ★★着手前確認 — ★★なし（すべて解決済み）★★★★

★以下は**解決済みの記録**です。★★確認を待つ必要はありません★★（★判定＝D-103 / 各フィールドの埋め方＝D-104★）。

### ★`launch_failed`（出発失敗）を DB のどの状態で判定するか★ → ★★B案で確定★★

★★ユーザーの問い（2026-09-10。原文）★★:

```
Sは本清掃の清掃結果を抽出するところと同じではだめですか
```

★★この指摘が正しい★★。★`launch_failed` は「出発しなかった」という事象そのもの★であり、
★指示書R（D-099）が `cleanLogs()` で既に判定している `departed === false` と**同じ出来事**★です。
★清掃詳細ログ（F-02）とアラート（F-03）が、同じ出来事を別の切り口で見ているだけ★。
★別ロジックを立てると、片方だけ直したときに数字が食い違う★。

**★確定内容★**:

- ★`launch_failed` は ★★「本清掃行（`precheck === CLEAN_TIMETABLE_PRECHECK.main`）に紐づく
  `clean_status_histories` の行が無い」★★で判定する★
- ★★`cleanLogs()` の `departed` と**同じ判定を共有すること**★★

**根拠（★2026-09-10 実測。着手時に自分でも開いて確認すること★）**:

- `lib/db/index.ts:274-279` のコメント（★原文★）:

  ```
  // ★「出発したか」は DB の値で決める。「予定＋猶予30分」を当方で計算しない★
  //   （D-091 項1 / 指示書R 禁止事項3。LAUNCH_GRACE_MINUTES はここから読まない）
  //   ★本清掃に紐づく clean_status_histories の行が無い＝未出発（未実施）★という設計。
  //   `clean_status`（-1/0/1/2）に「出発しなかった」を表す値が無く、
  //   行が作られないことが未実施を意味する、という D-091 項1 後段の読みを採った。
  //   ★実データでどのカラムが未実施を表すかは要確認★（決定台帳に記載）。
  ```

- 実装は `lib/db/index.ts:292` の ★`departed: history !== undefined`★
  （`history` は `:280` の `historyByTimetableId.get(key(row.id))`）
- ★D-090 項6（`docs/decisions.md:3382`）の「★`launch_failed` の DB 表現も未定義★」は、
  本回答で解決★（★記録は `docs/decisions.md` の **D-103**★）

**選択肢（★当初の提示をそのまま残す。採用案に ✅★）**:

| 案 | 判定方法 | 帰結 | 採否 |
|---|---|---|---|
| A | ★`robot_error_reports.clean_timetable_id` が**本清掃行**（`precheck = 本清掃コード`）を指すエラー = `launch_failed`★。★precheck 行を指すものは `severity: "precheck"` ＋ `precheck_minutes = その行の precheck 値`★ | ★1つのクエリで両方を出し分けられ、`severity` の判定規則が「紐づく `clean_timetables` 行の `precheck` 値だけ」で閉じる★。★`resolution_status` / `responded_at` / `resolved_at` / `note`（＝書き込み許可4カラム。D-042）を `launch_failed` にも持たせられる★。★ただしシードに本清掃行のエラーを足す必要がある（段階1）★ | ★**不採用**★ — ★「エラー行があること」を出発失敗の条件にすると、`cleanLogs()` の `departed` と**別の定義**になる★。★同じ出来事に2つの判定ができ、片方だけ直したときに `/detail-log` と `/alerts` で数字が食い違う★ |
| ★**B** ✅★ | ★本清掃行に `clean_status_histories` が無い（＝未出発。D-099 の `departed` 判定）ことを `launch_failed` とする★ | ★`cleanLogs()` の `departed=false` と定義が完全に一致する★。★しかし**エラー行が存在しない**ので、`robot_status` / `detected_at` / `resolution_status` / `responded_at` / `resolved_at` / `note` の置き場が無い★ | ★★**採用**★★ — ★同じ出来事は同じ判定で出す★。★下記「残る課題」で4カラムの扱いを決めること★ |
| C | ★第4歩では `launch_failed` を出さない（precheck 行由来の `severity: "precheck"` のみ返す）★ | ★実装は最小。TeamWorks の回答を待てる★。★ただし `/alerts` から「出発失敗」が消え、fixtures との突き合わせ（段階4）で常に差が出る★ | ★**不採用**★ — ★待つ必要が無かった★。★判定は既にコードの中にあり、TeamWorks の回答を待つ論点ではない★ |

**★★実装上の注意 — 判定を2箇所に書かないこと★★**:

- ★現状、この判定は `lib/db/clean-log-mapping.ts` に**切り出されていない**★ —
  ★`lib/db/index.ts:292` にインラインで書かれている★
  （★同ファイルの export は `formatModelName` / `robotStatusFromErrors` /
  `isProblemCompletion` / `computeConsecutiveProblemDays` の4つのみ。2026-09-10 実測★）
- ★★したがって「流用」ではなく「切り出し」になる★★ — ★`lib/db/clean-log-mapping.ts` に
  純粋関数として出し、`cleanLogs()` と `prechecks()` の**両方から呼ぶ**こと★。
  ★`cleanLogs()` の挙動は1ビットも変えない（禁止事項の「既存5メソッドは1文字も変えない」は、
  この切り出しに伴う呼び出し置換のみ例外とし、段階5 で記録すること）★

**★B案の帰結（★**D-104** で確定済み。段階3 で「決める」必要はもう無い★）★**:

- ★B案では `launch_failed` に対応する `robot_error_reports` の行が存在しない★ため、
  ★`resolution_status` / `responded_at` / `resolved_at` / `note`（D-042 の書き込み許可4カラム）の
  置き場が無い★
- ★★埋め方は **D-104** で確定した★★ — ★`resolution_status` は `unresolved` 固定 /
  `note` / `responded_at` / `resolved_at` は `null`★。★詳細と根拠は段階3 の
  「3-3b 出発失敗の行の埋め方」を見ること★。★precheck 由来の行だけが4カラムの実体を持つ、という形になる★

★★すべて解決済みなので、そのまま段階1 に入ってよい★★。
★決定は `docs/decisions.md` の **D-103**（判定）と **D-104**（埋め方）に記録済み★。

---

## ★調査済みの事実（★現物で裏を取ってから使うこと★）★

### 1. ★`PrecheckItem` の各フィールドの取得元と、未決3件の扱い★

★`PrecheckItem` は `lib/types/index.ts:138-155`★。

| フィールド | 取得元 | 状態・扱い |
|---|---|---|
| ★`id`★ | ★`robot_error_reports.id` / `error_id`（★どちらも `varchar(50)`★。`prisma/schema.prisma:303-304`）★ | ★**D-090 項7**（`docs/decisions.md:3383`）: 型が `number`（`lib/types/index.ts:139`）と食い違う★。★**D-070 1-2**（`:1130`）で「`error_id` を使い string のまま」と**方針は確定済み**、★実装が未着手★★ → ★★段階1 で `string` にする★★ |
| ★`severity`★ | `clean_timetables.precheck` | ★**D-090 項6**: 未決。★上記の型変更（2値化 ＋ `precheck_minutes`）で解決する★ |
| ★`precheck_minutes`（新設）★ | `clean_timetables.precheck` の**値そのもの** | ★`launch_failed` のときは `null`★ |
| `serial_number` | `robot_error_reports.serial_number` | ★`varchar(20)`。NOT NULL★ |
| `facility_id` / `facility_name` | `robot_error_reports.facility_id` → ★`facility` リレーションを `include`★ | ★`facility_id` は FK があるのでリレーションで辿れる（`prisma/schema.prisma:329`）★。★`BigInt` → `Number()`★ |
| `scheduled_at` | ★`clean_timetables.start_at`（**本清掃行**）★ | ★`toJstIso()`（`lib/format/date.ts`）を使う★（指示書P で導入済み）。★precheck 行の `start_at` ではなく**親の本清掃行**の値★（`parent_id` で引く） |
| ★`robot_status`★ | ★`robot_error_reports.error_type` の**生値**★ | ★**D-077 / D-098**。日本語に変換しない★。★エラーが無ければ `ROBOT_STATUS_STANDBY`（`lib/config/index.ts:72` = `"Standby."`）★。★`lib/db/clean-log-mapping.ts:54` の `robotStatusFromErrors()` がこの規則を既に実装している → **流用すること**★ |
| ★`detected_at`★ | ★`robot_error_reports.errored_at`（`prisma/schema.prisma:324`。★nullable★）または `created_at`（`:327`。★NOT NULL / default now()★）★ | ★★どちらを使うか未決。決めて記録すること★★。★推奨: `errored_at ?? created_at`（`errored_at` を優先し、null のときだけ `created_at` に落とす）★。★理由: `errored_at` が「エラーが起きた時刻」で意味的に正しいが nullable なので、NOT NULL の `created_at` を保険にする★ |
| `resolution_status` | `robot_error_reports.status`（`:322`。★`Int @default(0) @db.UnsignedTinyInt`★） | ★0 = `unresolved` / 1 = `in_progress` / 2 = `resolved`★。★`lib/db/ds-status.ts` の `resolutionStatusFromControlErrorStatus()` と**同じ値域**なので、同じ作法で純粋関数に切り出すこと★ |
| `responded_at` / `resolved_at` | 同名カラム（`:325` / `:326`。★どちらも nullable★） | ★**書き込み許可4カラム**（D-042。`docs/decisions.md:226`）★。★本作業は**読むだけ**★ |
| `note` | `robot_error_reports.comment`（`:323`。★nullable★） | 同上 |
| ★`battery_level`★ | ★★取得元が無い★★ | ★**D-090 項8**（`docs/decisions.md:3384`）: `robots.battery` は**現在値のみで履歴が無い**★。★★`null` を返すことを推奨★★（「事前チェック時点の残量」は DB から取れないため、当方で現在値を代入すると**別時点の値を過去のアラートに貼る**ことになる）。★決めて記録すること★ |

### 2. ★API契約（YAML）の側にも抜けがある（実測）★

★`d-watch-api.yaml:1149-1176` の `PrecheckItem` には★:

- ★`facility_id` が**無い**★（`lib/types/index.ts:142` にはある）
- ★`battery_level` が**無い**★（`lib/types/index.ts:154` にはある。`AlertsScreen.tsx:165-169` が表示している）

★★段階1 でこの2つも足すこと★★（★`CLAUDE.md` §6-1「API契約を変えたくなったら先に YAML を直す」★）。

### 3. ★★`id` を `string` にすると影響が出る箇所（★洗い出し済み。自分で grep し直すこと★）★★

| # | 箇所 | 内容 |
|---|---|---|
| 1 | ★`lib/types/index.ts:139`★ | `id: number` → ★`id: string`★ |
| 2 | ★`d-watch-api.yaml:1153`★ 付近 | `id: { type: integer, example: 1 }` → ★`{ type: string }`★ |
| ★3★ | ★`d-watch-api.yaml:442`★ | ★`/prechecks/{id}/status` が共有パラメータ `PathId`（`:997-1001`。★`type: integer`★）を参照している★。★★`PathId` は他に6箇所（`:337` `:370` `:769` `:957` `:969`）から参照されているので、`PathId` 自体を変えないこと★★ → ★`/prechecks/{id}/status` にだけ**インラインの string パラメータ**を書く★ |
| ★4★ | ★`lib/prechecks/query.ts:132`★ | `sortPrechecks()` の同値時タイブレークが ★`a.id - b.id`（数値減算）★。★string では動かない★ → ★`a.id < b.id ? -1 : a.id > b.id ? 1 : 0` 等に変える★。★D-052（`docs/decisions.md:325`）の「detected_at 降順 / 同値は id 昇順」という契約は変えないこと★ |
| 5 | ★`lib/fixtures/prechecks.ts:21` `:36` `:52` `:69` `:84` `:101` `:116` `:131`★（8件） | ★`id: 1` → `id: "1"` 等★。★★`lib/fixtures/` の改変になるので、指示書R 禁止事項12（`CLAUDE.md` §9.5）に抵触する — ★型変更に伴う必然の追従なので本指示書では**許可する**★。★値の意味は変えないこと（`1` → `"1"` のように同じ文字列にする）★★ |
| 6 | ★`lib/prechecks/__tests__/query.test.ts`★（`:14` `:143-145` `:151-153` `:158` `:162` `:166` `:170` `:194` `:284` `:293`） | ★`id` と `severity` の両方が変わる★。★期待値表を先に直してからテストを走らせること★（`CLAUDE.md` §9.4） |
| 7 | ★`components/features/alerts/AlertsScreen.tsx:149`★ | `key={item.id}` — ★React の key は string でも動くので**変更不要**★（確認だけすること） |
| 8 | `app/api/prechecks/route.ts` | ★`id` を直接触っていない（`:42` で型として使うだけ）ので**変更不要**の見込み★。★確認すること★ |

★★`/prechecks/{id}/status`（PATCH）の API ルートは**まだ実装されていません**★★
（`app/api/prechecks/` には `route.ts` しかない。実測）。★本指示書で作らないこと★。

### 4. ★落とし穴（★指示書R と共通。現物で確認すること★）★

| # | 落とし穴 |
|---|---|
| ★1★ | ★`robot_error_reports.clean_timetable_id`（`prisma/schema.prisma:312`）に **FK も KEY も無い**★（★しかも `BigInt?` で nullable★）。★`include` で `clean_timetables` を辿れない → **別クエリ＋メモリ上の手動 join**★（`cleanLogs()` と同じ。★`lib/db/index.ts:205-250` 付近の実装をそのまま流用できる★） |
| ★2★ | ★`robot_error_reports` に `deleted_at` が**無い**★（`prisma/schema.prisma:299-337` を実測）→ ★論理削除の絞り込みを書かない★。★書くと型エラーになる（＝安全側）。無理に回避しないこと★ |
| ★3★ | ★`id` / `error_id` は `varchar(50)`★（`:303-304`）→ ★`Number()` は `NaN` になっても**例外を出さない**★。★★本作業では `Number()` を通さない（string のまま返す）のが正解★★ |
| ★4★ | ★`BigInt` → `Number()`★ を忘れると `JSON.stringify` が例外を投げる（`NextResponse.json` が 500 になる。★`tsc` は捕まえない★）。★`facility_id` が該当★ |
| ★5★ | ★`RobotErrorReport` には `@id` が無く `error_id @unique` だけ★（`:303-304`）→ ★Prisma の `findUnique` は `error_id` でしか引けない★ |
| ★6★ | ★`clean_timetables.start_at` は `datetime`（TZ情報なし）★。★API は `+09:00` 付き ISO8601 → `toJstIso()` を使う★ |

### 5. ★シードの状況（★実測して確認すること★）★

★指示書R（D-099 段階1）で `clean_timetables` / `clean_statuses` / `clean_status_histories` /
`robot_error_reports` は**シード済み**★（`scripts/seed-local-db.ts`。★`npm run seed:local`★）。
★ただし `prechecks` の観点では**足りないものがあります**★:

| # | 不足 | 帰結 | 対応 |
|---|---|---|---|
| ★1★ | ★`robot_error_reports` の `status` / `comment` / `responded_at` / `resolved_at` を**1件も入れていない**★（`scripts/seed-local-db.ts:689-705` を実測） | ★`status` は default `0` なので、★**全件が `unresolved`・`note` が `null`**★になる★。★`/alerts` の「対応中 / 解消」が1件も出ない★ | ★段階1 に含めること★。★`lib/fixtures/prechecks.ts` の8件と**同じ分布**（未解消3 / 対応中2 / 解消3）になるよう、シードで値を散らすこと★ |
| ★2★ | ★本清掃行に紐づくエラー行が無い★（precheck 行にしか作っていない） | ★★B案（確定）ではこれが正常★★。★`launch_failed` はエラー行ではなく、本清掃行に `clean_status_histories` の行が無いことで判定する（`cleanLogs()` の `departed` と同じ。D-103）ため、★★エラー行が無いことがそのまま `launch_failed` の存在を意味する★★（エラー行の有無を条件にする必要が無い） | ★不採用の A案ならここでエラー行の追加が要ったが、B案ではこの不足を埋める作業は不要★。★段階1 に追加のシード作業は無い★ |
| 3 | ★シードのエラー行は `lib/fixtures/clean-logs.ts` 由来であり、`lib/fixtures/prechecks.ts` の8件とは**別物**★ | ★段階4 の `USE_FIXTURES` 突き合わせで**件数が一致しない**★ | ★★これは想定内★★。★「同じ8件を再現する」ことは目指さない★。★確認するのは**変換規則が正しいこと**（`severity` / `precheck_minutes` / `robot_status` / `resolution_status` の作られ方）★ |

★シードを触る場合の作法★: ★`deleteMany` の逆順（`scripts/seed-local-db.ts:392-395`）を崩さない★ /
★2回続けて実行しても結果が同じであること★ / ★実在の住所・電話番号・社名・人名を入れないこと★
（`CLAUDE.md` §7。★既存のダミー値は `:229-231` の `ERROR_LEVEL_PLACEHOLDER` /
`ERROR_DETAIL_PLACEHOLDER`★）。

---

## ★禁止事項（この指示書の全段階に効きます）★

| # | ★してはいけないこと★ | 理由 |
|---|---|---|
| ★1★ | ★★**サマリ系4本（`reportKpis` / `facilityPerformance` / `errorBreakdown` / `continuousProblems`）に手を付けること**★★ | ★第5歩の範囲★。★共通構造でまとめて書くほうが良いと本書冒頭で結論済み★ |
| ★2★ | ★★**エラー種別を日本語に変換すること / 7分類に寄せること**★★ | ★D-077（`docs/decisions.md:1847`）/ D-088 / D-098★。★`error_type` の値をそのまま `robot_status` に入れる★ |
| ★3★ | ★★**`precheck` の値（0/1/2、60、30 など）をコードに直書きすること**★★ | ★`CLAUDE.md` §7 / D-091 項4 / D-093 項2★。★`lib/config/index.ts:49-54` の `CLEAN_TIMETABLE_PRECHECK` を使う★。★値が変わったとき1箇所で追従できる形を壊さない★ |
| ★4★ | ★**`deleted_at` が無いテーブルで論理削除の絞り込みを書くこと**★ | ★型エラーになる（＝安全側）。落とし穴2★ |
| ★5★ | ★**`clean_timetables.inactive` で絞ること**★ | ★第1〜3歩と同じ方針（`lib/db/index.ts:180`）。★仕様が未確定なので絞らない側に倒す★ |
| ★6★ | ★★**メインDB（TeamWorks 側）に接続・書き込みすること / `dwatch_ddl` に書き込むこと**★★ | `CLAUDE.md` §2 / §7 / D-042。★作業は `dwatch_local` の中だけ★。★接続先ホスト名は未受領★ |
| ★7★ | ★**`prisma migrate` / `db push` / `db pull` を使うこと / `prisma/schema.prisma` を手で書き換えること**★ | ★`CLAUDE.md` §7 / D-056 / D-059★。★`prisma/schema.prisma:301-302` のコメントが D-070 1-2 より古い（「API 契約を変更しない」と書いてある）が、★**それでも手で直さない**★★ |
| ★8★ | ★**`prismaSource` の既存5メソッド（`facilities` / `schedules` / `dsDevices` / `dsAlerts` / `cleanLogs`）を変更すること**★ | ★指示書M / P / R の成果物★。★本作業は「その隣に1本足す」だけ★ |
| ★9★ | ★**`lib/db/prisma.ts` / `local-guard.ts` / `use-fixtures.ts` / `ds-status.ts` を変更すること**★ | ★完成しており、そのまま効く★。★特に `use-fixtures.ts` は Edge Runtime バンドル破壊の修正そのもの★（D-086） |
| ★10★ | ★**`lib/db/` にフィルタ・ページング・ソートを持ち込むこと**★ | ★`lib/db/index.ts:71-75` の設計方針★。★`prechecks` には judge も連続日数も無いので、この例外は使えない★ |
| ★11★ | ★**書き込み許可4カラム以外に書き込むこと / 本作業で DB に書き込むこと**★ | ★D-042★。★本作業は**読むだけ**（シードを除く）★ |
| ★12★ | ★**テストを DB 依存にすること**★ | ★遅く・不安定になり、DB が空の環境で落ちる★。★テストは fixtures と純粋関数だけ★ |
| ★13★ | ★**`git add -A` を使うこと**★ | `CLAUDE.md` §7。★必ずファイルパスを明示★ |
| ★14★ | ★**`.env.local` をコミットすること / DB の認証情報を文書やコードに書くこと**★ | `CLAUDE.md` §9.3。★確認後は `USE_FIXTURES=true` に戻す★ |
| ★15★ | ★**`npm run dev` を Bash で起動すること**★ | ★Browser pane の `preview_start` を使う★（段階4） |

---

## 作業の順序（★崩さないこと★）

★段階0（前提の確認。★読むだけ・コミットなし★）→ 段階1（API契約と型 ＋ 必要ならシードの補完）
→ 段階2（fixtures と画面）→ 段階3（`prismaSource.prechecks()`）→ 段階4（動作確認。★コミットなし★）
→ 段階5（記録と push）★。★★段階1・2・3・5 でそれぞれ1コミット★★。

★**段階ごとに `npx tsc --noEmit` / `npm run lint` / `npm test` を実行し、通ってからコミットすること**★
（D-050。`docs/decisions.md:308`）。

---

## 段階0 — 前提の確認（★読むだけ・コミットなし★）

| # | 対象 | 見る点 |
|---|---|---|
| 1 | ★D-090 項6 / 項7 / 項8★（`docs/decisions.md:3382-3384`） | ★未決3件の原文★ |
| 2 | ★D-093 項2★（`:3749`。特に ★2-2 の案A（`:3774`）★ と ★2-3 の注意（`:3782`）★） | ★本指示書の設計根拠★ |
| 3 | ★D-091 項4★（`:3546`）/ ★D-070 1-2★（`:1130`）/ ★D-098★（`:4342`） | ★`precheck` が分前を表す予定★ / ★`error_id` を string のまま★ / ★`Standby.`★ |
| 4 | ★★`prismaSource.cleanLogs()`（`lib/db/index.ts:186-320`）★★ | ★**これが雛形**★。★手動 join（`:205-250` 付近）/ 遅延 import / `Number()` / `toJstIso()` / docstring の書き方を**そのまま流用する**★ |
| 5 | ★`lib/db/clean-log-mapping.ts`（全141行）★ | ★`robotStatusFromErrors()`（`:54`）は `prechecks` でも**そのまま使える**★。★`RobotErrorLike`（`:32`）も同じ★ |
| 6 | ★`lib/db/ds-status.ts`★ | ★`resolutionStatusFromControlErrorStatus()` の作法（0/1/2 → 3値）を `robot_error_reports.status` に流用する★ |
| 7 | ★`scripts/seed-local-db.ts`（全784行。特に `:646-712`）★ | ★precheck 行とエラー行の作り方★。★「調査済みの事実」5 の不足2件を自分で確認すること★ |
| 8 | ★`lib/i18n.ts:80-97` / `:155-160`★ | ★方式A の説明と既存の実例★ |

### 接続先の確認とこの段階の完了条件

- ★`.env.local` の `DATABASE_URL` が `dwatch_local` を指していることを確認する★。
  ★**値そのものを画面・ログ・文書に出さないこと**★（`CLAUDE.md` §9.3）
- ★`.env.local` に `ALLOW_DIRECT_ACCESS=true` があることを確認する★（★作成時点では設定済み★。
  D-101 項2。★無いと `proxy.ts` が全ルートを止めるので段階4 で詰まる★）
- ★着手前の基準値（`npm test` のファイル数・テスト数 / `tsc` / `lint`）を自分で実測して控えた★
- ★★着手前確認は解決済み（`launch_failed` の DB 表現 = B案。D-103）。新たに回答を得る必要はない★★

---

## 段階1 — ★API契約と型を変える★

★**実装より先に契約を直す**★（`CLAUDE.md` §6-1）。

### 1-1. `d-watch-api.yaml`

- ★`:1156` の `enum` を ★`[precheck, launch_failed]`★ に変更し、`description` も直す★
- ★`precheck_minutes` を追加★（`{ type: integer, nullable: true }`。
  ★description に「何分前のチェックか。`launch_failed` のときは null」と書く★）
- ★`:1152` 付近の `id` を `{ type: string, example: "RER-1001-1" }` に変更★
- ★`facility_id` と `battery_level` を追加★（「調査済みの事実」2）
- ★`:442` の `PathId` 参照を、`/prechecks/{id}/status` 専用のインライン string パラメータに置き換える★
  （★★`components/parameters/PathId` 自体は変えない★★。他6箇所が参照している）

### 1-2. `lib/types/index.ts`

- ★`:124` `PrecheckSeverity` を `"precheck" | "launch_failed"` に★
- ★`:139` `id: number` → `id: string`★（D-090 項7 / D-070 1-2。★決定済みなのでここで実装する★）
- ★`PrecheckItem` に `precheck_minutes: number | null` を追加★（★コメントで「`launch_failed` は null」
  「値は `clean_timetables.precheck` の生値」を明記★）

### 1-3. ★`id` を `string` にした影響の追従★

★「調査済みの事実」3 の表の #4・#6 を直す★（`lib/prechecks/query.ts:132` のタイブレークと、
`lib/prechecks/__tests__/query.test.ts`）。★#5（fixtures）は段階2 でまとめて直してよい★。

### 1-4. ★シードの補完（「調査済みの事実」5）★

- ★`robot_error_reports` に `status` / `comment` / `responded_at` / `resolved_at` を入れる★
  （★分布は fixtures の8件に倣う。値は決め打ちでよいが、**規則をコメントに書くこと**★）
- ★★B案確定のため不要★★: ★不採用の A案なら本清掃行に紐づくエラー行を数件作る必要があったが、
  B案の `launch_failed` はエラー行を使わず「本清掃行に `clean_status_histories` が無いこと」
  （`departed`。D-103）で判定するため、ここでのエラー行追加はしない★
- ★冪等であること（2回実行して同じ結果）★ / ★件数を出力すること★

### この段階の完了条件

- ★`npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス★
- ★`d-watch-api.yaml` と `lib/types/index.ts` の内容が一致していること★（★enum / nullable / 型★）
- ★`components/parameters/PathId` に差分が無いこと★
- ★シードを触った場合、2回実行して結果が同じであること★
- ★**ここで1コミット**★

---

## 段階2 — fixtures と画面を追従させる

### 2-1. `lib/fixtures/prechecks.ts`

- ★8件すべてを新しい型に★: ★`id` を string に★ /
  ★`severity: "precheck_1h"` → `severity: "precheck", precheck_minutes: 60`★ /
  ★`"precheck_30m"` → `severity: "precheck", precheck_minutes: 30`★ /
  ★`"launch_failed"` → `precheck_minutes: null`★
- ★**分数は直書きせず `PRECHECK_MINUTES_BEFORE`（`lib/config/index.ts:34`）から取ること**★（禁止事項3）
- ★★境界値を1件足すこと★★ — ★`precheck_minutes: 20`（設定値以外の任意の分数）の行★。
  ★D-093 項2 の「20 なら20分前」を画面と型が本当に表現できるかの検証になる★。
  ★★これが本指示書で fixtures を触る主目的なので、忘れないこと★★

### 2-2. `components/features/alerts/AlertsScreen.tsx`

- ★`:20-24` の `SEVERITY_LABEL` を**分数から組み立てる形**に変える★:
  - ★`launch_failed` → `t("出発失敗")`★
  - ★`precheck` → `t("{n}分前異常").replace("{n}", String(item.precheck_minutes))`★
  - ★★`precheck_minutes` が `null` の `precheck`（起きないはずの組み合わせ）をどう出すか決めること★★
    （★推奨: `t("事前チェック異常")` にフォールバックし、黙って空文字を出さない★）
- ★`:26-28` の `severityColor()` は `launch_failed` かどうかで分岐しているだけなので**そのまま動く**★
  （確認だけすること）

### 2-3. ★i18n の対訳を入れる★

★`lib/i18n.ts` に追加する（★方式A。`:86-87` / `:158-159` の前例に倣う★）★:

| キー | EN | 備考 |
|---|---|---|
| ★`"{n}分前異常"`★ | ★`"{n} min before: error"` 等★ | ★方式A。`:159` の `"{n}分前"` の隣に置くと読みやすい★ |
| ★`"出発失敗"`★ | `"Launch failed"` | ★★現在キーが無い（EN でも日本語のまま出ている）★★ |
| ★`"事前チェック異常"`★ | `"Pre-check error"` | ★2-2 のフォールバックを作る場合★ |
| ★`"残量"` / `"検知"`★ | `"Battery"` / `"Detected"` | ★同じ画面でキーが無いもの（`AlertsScreen.tsx:167` / `:171`）★ |

★★`"1h前異常"` / `"30分前異常"` はキーごと消える★★（★組み立て式に変わるため★）。
★キーが無かったので削除対象も無い（実測）★。

### 2-4. ★判断事項 — `robot_status` を生値に寄せるか★

★`lib/fixtures/prechecks.ts` の `robot_status` は現在 ★日本語6値★★
（`"ハンドルアップ"` / `"オフライン"` / `"充電中"` / `"セキュリティ有効"` / `"緊急停止"`）。

- ★★指示書K（D-088。`docs/decisions.md:3084`）で `error_type` を英語識別子に寄せたのと
  同じ扱いにするのが一貫する★★。★DB 経由では `error_type` の生値がそのまま入るため★
- ★寄せると `lib/theme-status.ts` の色分けがグレーになる★（★日本語6文字列の完全一致で引くため。
  `lib/config/index.ts:197` の `ROBOT_STATUS_TONES`）。★これは D-077 の想定挙動★
- ★指示書R では「エラー無し時の値」だけを `Standby.` にし、
  ★`ROBOT_STATUS_TONES` には足さなかった★★（D-099 項2-2。`lib/config/index.ts:185-187` のコメント）
- ★★判断してから実装し、理由を記録すること★★。★寄せない場合、段階4 の突き合わせで
  `robot_status` に差が出るのは**想定内**として記録する★

### この段階の完了条件

- `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス
- ★`USE_FIXTURES=true` のまま `/alerts` が表示され、★「60分前異常」「30分前異常」「20分前異常」
  「出発失敗」がすべて正しく出ること★★
- ★EN 表示に切り替えて、追加した対訳が効いていること★
- ★★`0/1/2` や `60` / `30` が `AlertsScreen.tsx` / `lib/fixtures/` に直書きされていないこと★★
- ★**ここで1コミット**★

---

## 段階3 — `prismaSource.prechecks()` を実装する

### 3-1. 作るもの

★`lib/db/index.ts:321-323` の `notImplemented("prechecks")` を実装に置き換える★。
★`cleanLogs()`（`:186-320`）と**同じ形**にすること★（遅延 import → クエリ → 手動 join → `map`）。

### 3-2. ★手動 join（落とし穴1）★

1. ★`robot_error_reports` を `findMany`（★`facility` は FK があるので `include` してよい★）★
2. ★`clean_timetables` を `findMany`（★`clean_timetable_id` に FK が無いので `include` で辿れない★）★
3. ★`clean_timetables.id` をキーに `Map` を作る★
4. ★エラー行 → 紐づく `clean_timetables` 行 → `precheck` の値 で `severity` / `precheck_minutes` を決める★
5. ★`scheduled_at` は**親の本清掃行**の `start_at`★ —
   ★precheck 行なら `parent_id` で親を引く / 本清掃行なら自分自身★
6. ★`clean_timetable_id` が `null` のエラー行★（`prisma/schema.prisma:312` は nullable）★の扱いを
   決めて記録すること★（★推奨: `prechecks` の対象外として除外する。
   「清掃タイムテーブルに紐づかないエラー」は事前チェックのアラートではないため★）

★★この方針（なぜ `include` が使えないのか）をコードコメントに残すこと★★ —
★D-094 で TeamWorks に FK 追加を再依頼中なので、FK が付いたら `include` に書き換えられる★。
★根拠（`prisma/schema.prisma:312` に FK も KEY も無い）を書くこと★。

### 3-3. ★`severity` の組み立て★

★★`precheck = 0`（本清掃）の行に紐づくエラーは `launch_failed` に相当するか？★★ —
★**B案で確定（D-103）**★。★D-090 項6 が「`launch_failed` の DB 表現は未定義」としていたが、これで解決済み★。

| 紐づく `clean_timetables` 行 | `severity` | `precheck_minutes` |
|---|---|---|
| ★`precheck === CLEAN_TIMETABLE_PRECHECK.main`（本清掃）★ | ★A案なら `launch_failed`★ | ★`null`★ |
| ★それ以外（事前チェック行）★ | ★`"precheck"`★ | ★★`precheck` の値を**そのまま**★★ …ただし ★現行DDLは `1` / `2` のコード値★（D-091 項4 の切替前）なので、★`CLEAN_TIMETABLE_PRECHECK.rounds` と `PRECHECK_MINUTES_BEFORE` の**対応表で分数に直す**★（`lib/config/index.ts:49-54` のコメントがこの対応を明記している）。★★切替後は「そのまま使う」に変わる — その分岐を1箇所の純粋関数に閉じ込め、コメントで明示すること★★ |

- ★★`0/1/2` や `60/30` を直書きしないこと★★（禁止事項3）
- ★★決めたことを記録すること★★（段階5）

### 3-3b. ★★出発失敗の行の埋め方（D-104 で確定。判断不要）★★

★上の 3-3 の表は「A案なら `launch_failed`」という条件付きで書かれているが、
★★採ったのは **B案** なので、その分岐には入らない★★★ —
★`launch_failed` は**エラー行から作らない**（本清掃行に紐づく `clean_status_histories` が
無いことから作る。D-103）★。★したがって「本清掃行を指すエラー行」を `launch_failed` に
読み替えてはいけない★。

★★出発失敗の行には `robot_error_reports` の行が存在しない★★ため、エラー行から取るはずの
フィールドが空になる。★埋め方は**すべて既存の前例に揃える**。新しい判断は要らない★:

| フィールド | 確定内容 | なぜそうするか | 前例 |
|---|---|---|---|
| ★`id`★ | ★**本清掃行の `clean_timetables.id`**★（★`BigInt` なので `Number()` を通す★） | ★1回の清掃を一意に指す最小の単位★。★エラー行が無いので `error_id` は使えないが、出発失敗は「その本清掃が出発しなかった」という事象なので、本清掃行の id で一意に決まる★ | ★**D-099 項4**（`docs/decisions.md:4559`）★ — ★`CleanLogItem.id` に**まったく同じ値**を採っている★（★原文: 「`id`（D-090 項3 の解決）: 本清掃行（`precheck = 0`）の `clean_timetables.id`。1回の清掃を一意に指す最小の単位。事前チェック行は `parent_id` でこれを指すので、1件の `CleanLogItem` に対応する id が必ず1つに決まる」★）。★実装は `lib/db/index.ts:283` の `id: Number(row.id)`（`row` は本清掃行）★ |
| ★`robot_status`★ | ★**`Standby.`**★ | ★エラー行が0件のときの値★。★自分で文字列を書かず、既存の純粋関数に**空配列を渡す**だけでよい★ | ★**D-098**★。★`robotStatusFromErrors()`（`lib/db/clean-log-mapping.ts:54`）が `errors.length === 0` のとき `ROBOT_STATUS_STANDBY`（`lib/config/index.ts:72` = `"Standby."`）を返す★。★`cleanLogs()` も `lib/db/index.ts:271` で同じ関数を呼んでいる★ |
| ★`resolution_status`★ | ★**`unresolved`** 固定★ | ★「まだ誰も対応していない」の意味★。★`PrecheckStatus` は `unresolved` / `in_progress` / `resolved` の**3値**（`lib/types/index.ts:125`。YAML は `d-watch-api.yaml:1165`）なので、状態を持てない行は先頭の値に倒すしかない★ | ★precheck 由来の行が `robot_error_reports.status` の default `0`（= `unresolved`）から作られるのと同じ値★ |
| ★`note` / `responded_at` / `resolved_at`★ | ★すべて **`null`**★ | ★エラー行が無いので備考も日時も存在しない★。★`null` は「値が無い」という事実そのもの★ | ★型定義がいずれも nullable（`lib/types/index.ts:149` / `:151` / `:152`）★ |

★★「その他」「不明」のような語を使わないこと★★ — ★**D-089 項3**（`docs/decisions.md:3249-3251`）で
同じ判断をしている★: ★「『その他』は D-067（破棄済み）の『未知の値をその他に寄せる』方針と
紛らわしい」★。★やり方（1箇所の定数に固定値を置く）は同じでよいが、★**語だけは避ける**★★。

★★`id` を D-099 と同じにする利点★★:

- ★★清掃詳細ログ（F-02）とアラート（F-03）に、同じ清掃について**同じ番号**が出る★★
- ★同じ出来事を別の切り口で見ているだけなので、★将来2画面を突き合わせるときに揃う★★
  （★D-103 項1 で「同じ出来事は同じ判定で出す」と決めたのと同じ理屈を、判定だけでなく**識別子**にも適用する★）
- ★★注意★★: ★precheck 由来の行は `error_id`（string）、`launch_failed` の行は本清掃行の id（数値の文字列化）になる★。
  ★`PrecheckItem.id` は段階1 で `string` にするので型は揃う★。★★衝突しないことを確認し、
  必要なら**混ざらないことが分かる形**にするかを判断して段階5 に記録すること★★

**★★申し送り（第4歩のスコープ外。将来の課題）★★**

- ★出発失敗の行には `robot_error_reports` の行が無いため、★★将来「対応状況の更新機能」を
  作るときに書き込み先が無い★★★
- ★★いまは問題にならない★★ — ★`PATCH /prechecks/{id}/status` は API契約にあるだけで**未実装**★
  （`app/api/prechecks/` には `route.ts` しかない。★実測して確認すること★）。
  ★画面も表示だけで、`components/features/alerts/AlertsScreen.tsx` の `onClick` は
  フィルタ切替の3つ（`:117` / `:124` / `:131`。すべて `setStatusFilter`）だけ★。★D-Sec 側も同様★
- ★将来の選択肢は2つ★:

  | 案 | 内容 | 前例 |
  |---|---|---|
  | ★1★ | ★DFA 独自ストアに持つ★ | ★**D-076**（`docs/decisions.md:1762`）の D-Sec デバイス備考★ — ★`lib/settings/ds-device-notes.ts` が `json-store.ts` を薄く包み、GET のときに DB 由来の値へ上書きしている（`app/api/ds-devices/route.ts`）★ |
  | ★2★ | ★TeamWorks に「出発失敗のときもエラー行を作る」ことを依頼する★ | ★D-042 の書き込み許可4カラムがそのまま使えるようになる★ |

- ★★本指示書では**どちらも実装しない**★★。★段階5 で申し送りとして記録するだけでよい★

### 3-4. ★その他のフィールド★

- ★`robot_status`★: ★`robotStatusFromErrors()`（`lib/db/clean-log-mapping.ts:54`）を流用★。
  ★1件のエラー行を扱うので配列に包んで渡すだけでよい★
- ★`detected_at`★: ★決めた規則（推奨 `errored_at ?? created_at`）で `toJstIso()` を通す★
- ★`resolution_status`★: ★`status`（0/1/2）→ 3値。★純粋関数に切り出す★（`ds-status.ts` と同じ作法）★
- ★`battery_level`★: ★`null`★（D-090 項8。★理由をコメントに残すこと★）
- ★`id`★: ★`error_id` を使う★（D-070 1-2）。★`Number()` を通さない★
- ★`facility_id`★: ★`Number()`★（BigInt。落とし穴4）

### 3-5. ★変換ロジックは純粋関数に切り出してテストを書く★

- ★置き場所★: ★`lib/db/precheck-mapping.ts` を新設するか、`lib/db/clean-log-mapping.ts` に足すか★を
  判断して記録すること（★推奨: 新設。`cleanLogs` 側のテストと混ざらない★）
- ★切り出す候補★: ★`severity` / `precheck_minutes` の組み立て（precheck コード → 2値＋分数）★ /
  ★`resolution_status` の変換★ / ★`detected_at` の選択★
- ★★期待値表を先に作ること★★（`CLAUDE.md` §9.4）。★境界値: 本清掃コード / rounds の各値 /
  未知の precheck 値（例 `20`）/ `status` の 0・1・2・想定外値 / `errored_at` が null★
- ★DB 依存のテストは書かない★（禁止事項12）

### この段階の完了条件

- `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス
- ★`prismaSource` の既存5メソッドに差分が無いこと★（禁止事項8）
- ★`app/api/prechecks/route.ts` に差分が無いこと★（★データ層の差し替えだけで動くことが正しさの証明★）
- ★手動 join の理由・`severity` の組み立て規則・`battery_level` を null にした理由が
  コメントに残っていること★
- ★**ここで1コミット**★

---

## 段階4 — 動作確認

### 4-1. 起動と★認証★

- ★**`.env.local` の `USE_FIXTURES` を `false` にする**★
  （★`lib/db/use-fixtures.ts` は文字列 `"false"` のみを見る。`FALSE` や `0` では切り替わらない★）
- ★**`npm run dev` を Bash で起動しないこと。Browser pane の `preview_start` を使う**★
  （`.claude/launch.json` に `d-watch`（`npm run dev` / port 3000）がある）
- ★★**認証が入ったので注意**★★ — ★指示書I（D-101。`docs/decisions.md:4784`）で `proxy.ts` が入り、
  ★全ルートが保護されている★★
  - ★ローカルで確認するには `.env.local` に `ALLOW_DIRECT_ACCESS=true` が必要★
    （★作成時点では設定済み。**自分で確認すること**★）
  - ★★`/login` でトークンを入れる必要があるかを実際に確かめること★★
    （`app/login/page.tsx` / `app/api/auth/login` が存在する）。
    ★必要なら `.env.local` の `AUTH_TOKEN` を使う（★値を文書やログに出さないこと★）★
- ★環境変数を変えたら dev サーバを再起動し、反映を確認すること★
- ★Browser pane が黒画面 / 0×0 になる既知の問題★: ★`preview_start` に `url` を渡して開き直す★。
  ★指示書R（D-099 段階3）では Browser pane が使えず curl で代替した★。
  ★同じ状況なら curl でよいが、★**その旨と「目視できなかった項目」を必ず記録すること**★★

### 4-2. ★★確認1 — `USE_FIXTURES` の true / false で `/api/prechecks` を突き合わせる★★

- ★同一クエリで両方のレスポンスを取得し、**差分を1件ずつ説明できること**★
- ★★件数は一致しません★★ — ★fixtures の8件とシード由来の行は**別物**（「調査済みの事実」5-3）★。
  ★確認するのは**変換規則**であって件数ではない★
- ★確認する点★:

| # | 項目 |
|---|---|
| 1 | ★`severity` が `"precheck"` / `"launch_failed"` の2値だけであること★（★旧3値が1件も出ないこと★） |
| 2 | ★`precheck_minutes` が **数値**で入り、`launch_failed` では `null` であること★ |
| 3 | ★`id` が **文字列**であること★（★`"RER-..."` 形式。`NaN` や `null` が混ざっていないこと★） |
| 4 | ★`scheduled_at` / `detected_at` / `responded_at` / `resolved_at` が ★`+09:00` 付き★ であること★ |
| 5 | ★`robot_status` に**英語識別子**（例 `Handrail Lifted`）または `Standby.` が出ること★。★★日本語（「ハンドルアップ」等）が出たら変換してしまっています★★ |
| 6 | ★`resolution_status` が `unresolved` / `in_progress` / `resolved` に散っていること★（★全件 `unresolved` なら段階1-4 のシード補完が効いていない★） |
| 7 | ★`battery_level` が全件 `null` であること★（D-090 項8） |
| 8 | ★ソート順が `detected_at` 降順・同値は `id` 昇順であること★（D-052） |

### 4-3. ★確認2 — `/alerts` 画面★

| # | 項目 |
|---|---|
| 1 | ★`/alerts` が実DB経由で表示されること（HTTP 200）★ |
| 2 | ★★「◯分前異常」が正しく出ること★★（★`precheck_minutes` の値がそのままラベルに出る★） |
| 3 | ★「出発失敗」が出ること★（★B案確定。不採用の C案を選んでいた場合のみ出ないのが正しかった★） |
| 4 | ★色分け（`severityColor`）が `launch_failed` だけ赤（`c.ng`）になること★ |
| 5 | ★`robot_status` のバッジがグレーになること★（★生値は日本語6値に一致しない。**想定挙動**★） |
| 6 | ★サマリカード（未解消 / 解消 / 全件）の数が items と整合すること★（D-054） |
| 7 | ★EN 表示で対訳が効いていること★ |

### 4-4. ★確認3 — 回帰していないこと★

| 画面 | 期待 |
|---|---|
| ★`/detail-log` / `/dashboard` / `/facilities` / `/schedules` / `/ds-logs` / `/ds-alerts`★ | ★**200。回帰していないこと**★（★特に `/detail-log` 253件 / `/dashboard` が `9 / 9`。D-099★） |
| `/reports` | ★500★（第5歩） |

- ★エラーメッセージが `[db] xxx のPrisma実装はまだありません。…` であること★（`lib/db/index.ts:155-159`）
- ★★**404 が出たら Edge Runtime バンドル破壊の回帰を疑うこと**★★（D-086）。
  ★500 = 想定どおりの未実装 / 404 = ビルドが壊れている★

### 4-5. ★ログの確認と `USE_FIXTURES=true` への復帰★

- ★`read_console_messages` でブラウザ側のエラー0件★（★使えなければ「確認できなかった」と記録★）
- ★`preview_logs` でサーバ側のエラーを確認★（★`A Node.js module is loaded` が出ていないこと★）
- ★★`.env.local` を `USE_FIXTURES=true` に戻し、dev サーバを再起動する★★
- ★**全画面が従来どおり動くこと**を確認する★（★特に `/alerts` で fixtures の8件＋境界値1件が
  正しいラベルで出ること★）。★これを確認して初めて「壊していない」と言えます★

### この段階の完了条件

- ★4-2 の8項目をすべて実測した★
- ★4-3 の「◯分前異常」を実測した★
- ★確認できなかった項目があれば、その理由と代替手段を控えた★
- ★★`USE_FIXTURES=true` に戻して全画面が従来どおり動くことを確認した★★

---

## 段階5 — 記録と push

### `docs/decisions.md` に追記

★**着手直前に `grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md | tail -3` で最終番号を実測すること**★
（★`^### D-0` だけでは3桁台を拾えない。`^### D-1` も併せて grep すること★。
作成時点の最終は `D-101`）。

★記録すること★:

1. ★★`severity` の新しい設計★★ — ★2値化 ＋ `precheck_minutes` を採った理由（D-093 項2-2 の案A）★ /
   ★precheck コード → 分数の対応をどこに閉じ込めたか★ / ★D-091 項4 の切替後にどう変わるか★
2. ★★`PrecheckItem.id` を `string` にしたこと★★（D-090 項7 / D-070 1-2 の実装完了）と、
   ★影響が出た箇所の一覧★（★特に `PathId` を変えずインライン化した判断★）
3. ★★`detected_at` に `errored_at` / `created_at` のどちらを使ったか★★と理由
4. ★★`battery_level` を `null` にした判断★★（D-090 項8 の解決）と、
   ★「事前チェック時点の残量」を取るには何が必要か★（★TeamWorks への照会候補として残す★）
5. ★★`launch_failed` の DB 表現をどう決めたか★★（★B案で確定。D-103。D-090 項6 の解決★）
6. ★★D-090 項6 / 項7 / 項8 の解決状況★★（★3件とも解決したか、残ったものがあるか★）
7. ★i18n に足した対訳★（★`"出発失敗"` 等がこれまで対訳無しだった事実も記録★）
8. ★段階2-4 の判断（`robot_status` を生値に寄せたか）★と理由
9. ★シードに足したもの★（`status` / `comment` / 4カラム / 本清掃行のエラー行）
10. ★実データで確認すべきこととして残るもの★ — ★`precheck` が 0/1/2 → 0/60/30 に切り替わる時期と
    新旧混在の有無（D-091 項4）★ / ★`PRECHECK_MINUTES_BEFORE` と DB の `precheck` のどちらが正か
    （D-093 項3 の未決2）★ / ★`precheck` に入りうる値の範囲（D-093 項3 の第7回候補）★ /
    ★`error_type` の実値（D-090 項18）★
11. ★段階4 で確認できなかった項目があれば、その理由と限界★
12. ★第5歩に残したもの★（サマリ系4本）と ★実測値★
    （`npm test` のファイル数・テスト数 / `tsc` / `lint`。★着手前の基準値との差★）

### `docs/handoff.md` に追記

★最新は 追記29（`docs/handoff.md:2342`）★。次は ★追記30★ の見込み。★着手直前に自分で確認すること★。

### push とこの段階の完了条件

- ★`git add` は必ずファイルパスを明示。`git add -A` は禁止★（禁止事項13）
- ★`.env.local` をコミットしないこと★（禁止事項14）。★`USE_FIXTURES=true` に戻したことを確認してから★
- `git push origin develop`（★`main` へは push しない★）
- ★完了条件★: `docs/decisions.md` に決定番号を追記した（★上記12項目すべて記録★）/
  `docs/handoff.md` に追記した / ★`origin/develop` へ push 済み★ /
  ★文書末尾の「実行状態」表を更新した★

---

## ★やらないこと（第5歩以降）★

| 対象 | いつ |
|---|---|
| ★`reportKpis` / `facilityPerformance` / `errorBreakdown` / `continuousProblems`★ | ★第5歩★（★サマリテーブル＋当期その場集計の共通構造でまとめて★） |
| ★`dsAlerts` の `security_off` 側★ | ★D-091 項3 の内部調整の結果待ち★ |
| ★`PATCH /prechecks/{id}/status` の API ルート実装★ | ★別の指示書★（★契約は直すが実装しない★） |
| ★点検記録2本（`inspections` / `inspectionDetail`）★ | ★最後★（D-072 項7） |
| ★`robot_status` への `formatRawLabel()` 適用（D-089 項7 の申し送り）★ | ★別の指示書★ |

---

## 完了条件（全体）

1. ★**着手前確認1件（`launch_failed` の DB 表現）は D-103 でB案に解決済み。新たな回答を待たずに段階0 から通しで実行した**★
2. 段階0〜5 をすべて実施した（★途中で止めた場合は、どこで止めたかを実行状態表に書くこと★）
3. `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス
4. ★★`d-watch-api.yaml` の `enum` が `[precheck, launch_failed]` になり、`precheck_minutes` /
   `facility_id` / `battery_level` が定義され、`id` が string になっていること★★
5. ★★`components/parameters/PathId` に差分が無いこと★★（他6箇所への巻き込みを避けた）
6. ★**`USE_FIXTURES=false` で `/alerts` が 200 で表示され、「◯分前異常」が正しく出ること**★
7. ★★**`precheck_minutes: 20` の fixtures 行が「20分前異常」と表示されること**★★（★任意の分数に対応できた証明★）
8. ★**`robot_status` に英語識別子または `Standby.` が出ること（生値方針の帰結）**★
9. ★**`id` が文字列で返り、ソート（D-052）が壊れていないこと**★
10. ★**`USE_FIXTURES=true` に戻して全画面が従来どおり動くこと**★
11. ★**`prisma/schema.prisma` / `app/api/prechecks/route.ts` / `prismaSource` の既存5メソッド /
    `lib/db/prisma.ts` / `local-guard.ts` / `use-fixtures.ts` / `ds-status.ts` /
    ★サマリ系4本（`notImplemented` のまま）★ に差分が無いこと**★
12. `docs/decisions.md` / `docs/handoff.md` の更新 / ★**`origin/develop` へ push 済み**★ /
    **文書末尾の「実行状態」表を更新した**

---

## 実行状態

| 段階 | 状態 | コミット |
|---|---|---|
| ★**着手前の確認**★ | ★★**なし（すべて解決済み。2026-09-10）**★★ — ★判定 = **B案**★（`launch_failed` = ★本清掃行に紐づく `clean_status_histories` の行が無い★。`cleanLogs()` の `departed` と同じ判定を共有する）。★記録: `docs/decisions.md` の **D-103**（`:5222`）★ / ★出発失敗の行の埋め方（`id` / `robot_status` / `resolution_status` / `note` / `responded_at` / `resolved_at`）= **D-104**★。★★新しい仕様判断は1つも要らない★★ | — |
| 段階0 前提の確認 | ★実施済み★ | コミットなし |
| 段階1 API契約と型 | ★実施済み★ | ★`0af0722`★ |
| 段階2 fixtures と画面 | ★実施済み★ | ★`8cdd299`★ |
| 段階3 `prismaSource.prechecks()` | ★実施済み★ | ★`cbfffc9`★ |
| 段階4 動作確認 | ★実施済み（★Browser pane が使えず curl で代替。目視できなかった項目は D-105 項12 に記録★）★ | コミットなし |
| 段階5 記録と push | ★実施済み★ | ★D-105 / 追記30 のコミット★ |

**実行日**: ★2026-09-10★ / **取得した決定番号**: ★**D-105**★（★着手直前に `grep -nE "^### D-(0\|1)[0-9][0-9]" docs/decisions.md \| tail -3` で最終番号 `D-104` を実測してから採番。ずれは無かった★）
**実測値（着手前 / 完了後）**: ★41ファイル / 778テスト / `tsc` 0 / `lint` 0★ → ★43ファイル / 801テスト / `tsc` 0 / `lint` 0★
**★決めたこと★**: `launch_failed` の DB 表現 = ★**B案で確定済み（D-103）**★ /
出発失敗の行の埋め方 = ★**D-104 で確定済み**★（`id` = 本清掃行の `clean_timetables.id` /
`robot_status` = `Standby.` / `resolution_status` = `unresolved` / `note`・`responded_at`・`resolved_at` = `null`）/
`detected_at` のカラム = ★`errored_at ?? created_at`★（出発失敗の行は ★`scheduled_at` と同値★。fixtures の前例に揃えた） /
`battery_level` の扱い = ★常に `null`★（D-090 項8 の解決） / 段階2-4（`robot_status` を生値に寄せたか） = ★★寄せない（日本語のまま）★★ — ★`lib/fixtures/clean-logs.ts` が日本語のままで、シードがその値を `error_type` にそのまま入れているため、片方だけ英語にすると逆に食い違う★ /
変換ロジックの置き場所 = ★`lib/db/precheck-mapping.ts` を新設★ /
`clean_timetable_id` が `null` のエラー行 = ★対象外★ / 本清掃行を指すエラー行 = ★対象外（`launch_failed` に読み替えない）★
**詳細**: `docs/decisions.md` の該当番号を参照
