# 指示書P — ローカルDBで動かす第2歩（D-Sec の `dsDevices` / `dsAlerts`(disconnected) を実DB経由にする）

| 項目 | 内容 |
|---|---|
| 実行環境 | ★**Cursor 内の Claude が実装します**★（`.claude/commands/d-watch.md` の役割分担表を参照） |
| 実行状態 | ★**実行済み**★（D-097。段階0〜5すべて完了。`origin/develop` へ push 済み。着手前確認1件はA案でユーザー回答済みのうえで着手した。着手途中で `dwatch_local` のスキーマ再同期（指示書Q・D-096）を挟んでいる） |
| 実行順 | ★**指示書M（D-086）の続き**★。★指示書M が作った `scripts/seed-local-db.ts` / `lib/db/prisma.ts` / `lib/db/local-guard.ts` / `lib/db/use-fixtures.ts` の上に積む作業★です。★指示書M が実装した `prismaSource.facilities()` / `schedules()` は1文字も変えません★。★触るコードが重なる未実行の指示書はありません★（未実行は ★指示書I（`lib/auth/` / `proxy.ts`）／指示書J（`lib/settings/`）★ の2本 — ★着手直前に自分で確認すること★）が、★`docs/decisions.md` / `docs/handoff.md` は共通で触ります★。★**同時に走らせないこと**★ |
| 取得する決定番号 | ★**着手直前に `grep -n "^### D-0" docs/decisions.md` で最終番号を確認すること**★。★作成時点の最終は `D-090`（`docs/decisions.md:3325`）なので、最初の空き番号は `D-091` の見込み★。★**2026-09-07 に `D-068` の二重採番が実際に起きています（D-070 冒頭）。2026-09-09 にも `D-089` の取り合いが起きかけました（D-090 冒頭）。必ず自分で確認してください**★ |
| 前提 | ★**D-090**★（`docs/decisions.md:3325`。本指示書の設計根拠。★未決20件の一覧★） / ★**D-086**★（`:2814`。第1歩の実行記録。★Edge Runtime バンドル破壊の地雷を含む★） / ★**D-065**★（`:474`。Q1-1 備考 `:482` / Q1-2 `controls.status` `:483` / Q1-3 `control_error_histories.status` `:484`） / ★**D-067 項1**★（`:604`。★2系統の取得元は完全に別★） / ★**D-070 D-3**★（`:1133`。`controls.status` を読むだけでよい。訂正節は `:1161`） / **D-042**（`:226`。書き込みは4カラムのみ） / **D-050**（`:308`。通ってからコミット） / **D-044**（`:230`。fixtures で開発する） / **D-056**（`:353`。`schema.prisma` を手で書き換えない） |
| 段階数 | ★**6段階**★（段階0〜段階5） |
| 着手前の基準 | ★**着手直前に自分で `npm test` / `npx tsc --noEmit` / `npm run lint` を実行し、その実測値を基準にすること**★（作成時点の参考値は D-089 の「31ファイル / 610テスト / `tsc` 0 / `lint` 0」だが、★鵜呑みにしない★） |
| 作成 | 2026-09-09（作成時点の決定事項最終番号 **D-090** / `docs/handoff.md` の最新は **追記25**（`docs/handoff.md:2061`）） |

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

> ★**この指示書を読んでいるあなたへ**★
> `.claude/commands/d-watch.md` に「実装は Cursor 側、Claude Code は指示書作成まで」とありますが、
> これは**2つの環境の役割分担**です。**Cursor の中で動いている Claude は実装担当**です。
> この指示書を渡されたなら、**調査や改訂ではなく実装を行ってください。**

> ★★**これは「第2歩」です。うまくいかなければ途中で止めて構いません。**★★
> ★`.env.local` の `USE_FIXTURES` を `true` に戻すだけで、
> **完全に元の状態（fixtures で全画面が動く状態）に戻ります**★。
> ★DB に書いたデータも `dwatch_local` の中にしかありません。★

---

## ★★冒頭に読むべきこと — なぜ `security_off` を含めないのか★★

★**`dsAlerts()` は `disconnected` だけを返します。`security_off` は返しません。**★

> ★**注記（2026-09-09 追記。D-091 項3）**★: ★ユーザー判断により、`security_off` は
> そもそもアラートから外す方向で内部調整中です★（「セキュリティが無効であっても異常ではない」）。
> ★したがって本指示書の「`disconnected` のみ」という範囲は、結果的に最終形に近い可能性があります★。
> ★ただし確定ではないので、本指示書の方針（第3歩で `security_off` を扱う）はそのままにしておきます★。

`DsAlert`（`lib/types/index.ts:312-330`）が要求する
★`id` / `device_serial` / `resolution_status` / `responded_at` / `resolved_at` / `memo` の6フィールド★に
★対応するカラムが `security_statuses`（`prisma/schema.prisma:362-377`）にも
`security_status_histories`（`:379-392`）にも存在しません★（D-090 項3-9）。
両テーブルが持つのは `id` / `facility_id` / `section_id` / `status` / `reported_at` / `created_at` だけです。

さらに ★`security_statuses.reported_at` は `@updatedAt`★（`prisma/schema.prisma:368`）なので、
★`detected_at`（「いつからセキュリティが解除されたままか」）が取れません★（D-090 項3-10）。

★**この6つを埋めるには推測が要ります。それは `CLAUDE.md` §7「仕様が不明な箇所を推測で実装すること」の
禁止に真正面から当たります。**★

一方 `disconnected` 側は ★取得元が完全に確定しています★:

| 種別 | 取得元 | 根拠 |
|---|---|---|
| ★`disconnected`★ | ★`control_error_histories`★（制御盤異常のエラー） | ★D-067 項1（`docs/decisions.md:604`）★ |
| `security_off` | `security_statuses`（状態から導出。エラーではない） | 同上。★**この2つを同じテーブルから取ろうとしないこと**★ |

### ★★帰結 — 静かに件数が減ります★★

★`USE_FIXTURES=false` にすると、以下が起こります。**これは想定内であり、バグではありません**★:

| 画面 | 変化 | 理由 |
|---|---|---|
| ★`/ds-alerts`★ | ★12件 → **5件** に減る★ | ★`security_off` 7件が返らなくなる★ |
| ★`/dashboard` のセキュリティバナー★ | ★出なくなる（空配列）★ | ★`computeSecurityBanner()`（`lib/dashboard/aggregate.ts:113-125`）が `alert_type === "security_off"` で絞っているため★ |

★★**「静かに減る」ので、段階4 の確認項目に件数の突き合わせを必ず入れてあります。飛ばさないでください。**★★
★差がちょうど7件であることを数えること★（fixtures の内訳は `security_off` 7件 / `disconnected` 5件 —
`lib/fixtures/ds-alerts.ts` を実測して確かめること）。

★`security_off` は第3歩で扱います★（D-090 項2 の表）。

---

## ★★着手前にユーザーへ確認すること（1件）★★

### ★施設12（フレッシュタウン湖畔店）の `DS-012` をどう扱うか★

**事実**（★すべて現物で裏を取ってから使うこと★）:

- 施設12 は ★ロボット0台★（`lib/fixtures/facilities.ts:213` の `robots: []`）
  → ★シードは「ロボット1台につき section を1件」作る★（`scripts/seed-local-db.ts:207-224`）ので
  ★施設12 には `sections` が0件★になる
- 施設12 は ★`d_security_enabled: false`★（`lib/fixtures/facilities.ts:212`）
- しかし fixtures には ★`DS-012`（`lib/fixtures/ds-devices.ts:109-115`）と
  対応するアラート（`lib/fixtures/ds-alerts.ts:140-152`。`id: 10`）★ がある
- ★`controls.section_id` は NOT NULL の `sections` への FK★
  （`prisma/schema.prisma:182` / `:193`）。★そのままではシードできず、FK 違反で止まる★

**選択肢**:

| 案 | 内容 | 帰結 |
|---|---|---|
| ★**A（推奨）**★ | ★`DS-012` とそのアラートをシードから除外する★ | ★指示書M で施設12 のスケジュールを除外したのと同じ扱い★（D-086 / `scripts/seed-local-db.ts:128-149`）。★`d_security_enabled: false` の施設が D-Sec デバイスを持つのは、そもそも fixtures 側の矛盾★。★**`/ds-logs` が 12台 → 11台 になります**★（異常5→4） |
| B | 施設12 に `sections` を1件作る（ロボットは無いまま） | ★「1清掃エリア = 1ロボット」（`CLAUDE.md` §8）の前提に反する section が DB に生まれる★ |
| C | `DS-012` を別の施設に付け替える | ★fixtures と DB で施設が変わる。段階4 の突き合わせが読みにくくなる★ |

★**A を採る場合、`/ds-logs` の台数が 12 → 11 に変わります。段階4 の期待値を書き換えること。**★
★**回答を得るまで段階1 に入らないこと**★（`CLAUDE.md` §0-1「仕様が不明なら止まる」）。

---

## この指示書の位置づけ

### ★どこまでやるか — 残り10メソッド中2つだけ★

`lib/db/index.ts` の `prismaSource`（`:149-257`）は、指示書M で `facilities()`（`:162-198`）と
`schedules()`（`:199-238`）の2本が実装済みです。★残り10本は `notImplemented()` のまま★。

本指示書で実装するのは ★そのうち2本だけ★:

| メソッド | 現在地 | 使われる画面 |
|---|---|---|
| ★`dsDevices()`★ | `lib/db/index.ts:156-158` | ★F-06 D-Secログ★（`/ds-logs`）。`app/api/ds-devices/route.ts:42` / `app/api/ds-devices/[serial_number]/note/route.ts` |
| ★`dsAlerts()`★（★disconnected のみ★） | `lib/db/index.ts:159-161` | ★F-07 D-Secアラート★（`/ds-alerts`）+ ★F-01 ダッシュボード★。`app/api/ds-alerts/route.ts:43` / `app/api/dashboard/route.ts:59` |

★この2本を選んだ理由★（D-090 項2）:

- ★取得元が D-065 / D-067 / D-070 で確定している★（唯一、推測なしで書ける組み合わせ）
- ★judge も集計も連続日数も不要★ — 変換だけで済む
- ★影響画面が F-06 / F-07 に閉じており、失敗しても被害が小さい★

### ★今回の成果になるもの★

★**fixtures の施設名の不整合が、DB 経由にすることで構造的に消えます**★（D-090 項4）:

| # | fixtures の値 | 施設マスタの実名 | 該当箇所 |
|---|---|---|---|
| 1 | ★「メガバリューマート緑野第二店」★（`facility_id: 12`） | ★フレッシュタウン湖畔店★（`lib/fixtures/facilities.ts:204`） | `lib/fixtures/ds-devices.ts:111` / `lib/fixtures/ds-alerts.ts:145` |
| 2 | ★「バリューマート城南店」★（`facility_id: 10`） | ★フレッシュタウン大和店★（`lib/fixtures/facilities.ts:169`） | `lib/fixtures/ds-devices.ts:103` / `lib/fixtures/ds-alerts.ts:132` |

★#1 は既知（D-086 項1 / 項15）、#2 は D-090 で初めて記録された新発見★です。

★**なぜ DB にすると消えるのか**★: `controls` / `control_reports` / `control_error_histories` は
いずれも `facilities` への FK を持つ（`prisma/schema.prisma:192` / `:333` / `:353`）ので、
★施設名は結合で取ることになり、手書きの文字列を入れる余地が構造的に無くなる★からです。
★段階1 で `facility_name` をシードに渡さないこと（後述 1-6）が、この成果の前提です。★

---

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

### 1. ★対象テーブルの列（`prisma/schema.prisma` の実測）★

| テーブル | model | 行 | ★`deleted_at`★ | 備考 |
|---|---|---|---|---|
| `controls` | `Control` | `:174-198` | ★**ある**★（`:188`） | `serial_number`（`:176`）/ `facility_id`（`:180`）/ ★`section_id` NOT NULL FK★（`:182`, `:193`）/ `status`（`:183`）/ `inactive`（`:184`） |
| `control_reports` | `ControlReport` | `:323-340` | ★**無い**★ | `control_id`（`:327`）/ `serial_number`（`:328`）/ ★`reported_at`★（`:329`） |
| `control_error_histories` | `ControlErrorHistory` | `:342-360` | ★**無い**★ | `control_id`（`:346`）/ ★`status`★（`:347`）/ ★`comment`★（`:348`）/ ★`occurred_at`★（`:349`）/ `responded_at`（`:350`）/ `resolved_at`（`:351`）。★`created_at` / `updated_at` も無い★ |
| `sections` | `Section` | `:142-172` | ある（`:151`） | ★`section_name`★（`:145`） |

★**`deleted_at` が無いテーブルに `where: { deleted_at: null }` を書くと型エラーになります（＝安全側）**★。
★型エラーが出たら「このテーブルには論理削除が無い」というサインです。無理に回避しないこと。★

### 2. ★コード値の変換表（★これが実装の中身そのものです★）★

| 変換元 | 変換先 | 根拠 |
|---|---|---|
| ★`controls.status` = 0★ | ★`DsDevice.connection_status = "ok"`★ | ★D-065 Q1-2（`docs/decisions.md:483`）★ |
| ★`controls.status` = 1★ | ★`DsDevice.connection_status = "error"`★ | 同上。★3状態目は存在しない★（`lib/types/index.ts:284-290`） |
| ★`control_error_histories.status` = 0★ | ★`DsAlert.resolution_status = "pending"`★ | ★D-065 Q1-3（`docs/decisions.md:484`）★ |
| ★= 1★ | ★`"in_progress"`★ | 同上 |
| ★= 2★ | ★`"done"`★ | 同上 |

★**判定の再現はしないこと**★: 通信断のしきい値（`link_schedules.intervals`）から自分で
異常を計算し直す必要はありません。★TeamWorks 側のサーバーが判定結果を `controls.status` に
格納するので、DFA は**読むだけ**です★（D-070「★通信断判定についての訂正★」。`docs/decisions.md:1161`）。

### 3. ★挿入順序（外部キーの依存）★

★既存のシード（`scripts/seed-local-db.ts:152-158`）が入れている7テーブルの**後**に足します★:

```
（既存）groups → customers → devices → facilities → sections → robots → clean_schedules
（追加）        → controls → control_reports → control_error_histories
```

★削除は挿入の逆順★（`scripts/seed-local-db.ts:151-158` と同じ流儀）。

### 4. ★備考（`note`）はメインDBに無い★

★`DsDevice.note` に対応するカラムは `controls` にありません★（D-065 Q1-1。`docs/decisions.md:482`）。
★実値は `app/api/ds-devices/route.ts:75-78` が JSON ストア（`data/settings/ds-device-notes.json`）から
マージします★。★`prismaSource` 側は何もしません（常に `null` を返す）★。

---

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

| # | ★してはいけないこと★ | 理由 |
|---|---|---|
| 1 | ★★**`security_off` 側を推測で実装すること**★★ | ★対応カラムが存在しない★（D-090 項3-9 / 3-10）。★`CLAUDE.md` §7「仕様が不明な箇所を推測で実装すること」の禁止★。★件数が減るのは想定内。埋めようとしないこと★ |
| 2 | ★★**メインDB（TeamWorks 側）に接続・書き込みすること**★★ | `CLAUDE.md` §2 / §7 / D-042。★作業は `dwatch_local` の中だけで完結します★。★接続先ホスト名は未受領なので、そもそも繋がりません★ |
| 3 | ★**`dwatch_ddl` に書き込むこと**★ | ★参照専用★（受領した実DDL の姿を保つため） |
| 4 | ★**`prisma migrate` / `prisma db push` / `prisma db pull` を使うこと**★ | ★`CLAUDE.md` §7 / D-056 / D-059★。★`dwatch_local` には余分な4列があり `db push` は DROP しようとします★（D-086 項3）。★テーブルは既にあるので不要★ |
| 5 | ★**`prisma/schema.prisma` を手で書き換えること**★ | ★D-056★。★db pull の生成物が正★ |
| 6 | ★**`lib/db/prisma.ts` / `lib/db/local-guard.ts` / `lib/db/use-fixtures.ts` を変更すること**★ | ★指示書M で完成しており、そのまま効きます★。★特に `use-fixtures.ts` の切り出しは Edge Runtime バンドル破壊の修正そのものです★（D-086。`553b3cc`）。★戻すと `/api/reports/*` が404になります★ |
| 7 | ★**`prismaSource.facilities()` / `schedules()` を変更すること**★ | ★指示書M の成果物★。★本作業は「その隣に2本足す」だけです★ |
| 8 | ★**`lib/ds-devices/query.ts` / `lib/ds-alerts/query.ts` / API ルート2本 / 画面2枚を変更すること**★ | ★本作業はデータ層の差し替えです★。★**これらが1文字も変わらないまま画面が実DBで動くこと**が、変換が正しいことの証明になります★ |
| 9 | ★**`lib/db/` にフィルタ・ページング・ソートを持ち込むこと**★ | ★`lib/db/index.ts:59-63` の設計方針★（「フィルタや集計はこの層では行わず、APIルート側で組み立てます」）。★`DataSource` は引数を取らない契約（`:74-82`）★ |
| 10 | ★**`inactive` で絞り込むこと**★ | ★第1歩と同じ方針（D-086 / `lib/db/index.ts:165-167`）★。★`inactive` は「無効化」であって削除ではなく、画面が見せ分ける可能性がある。仕様が未確定なので**絞らない側に倒す**★ |
| 11 | ★**`lib/fixtures/` を削除・改変すること**★ | ★`CLAUDE.md` §9.5★（DB接続後も残す）。★施設名の不整合も「直さない」— DB 経由で消えることを実測するのが今回の成果です★ |
| 12 | ★**テストを DB 依存にすること**★ | ★遅く・不安定になり、DB が空の環境で落ちます★。★テストは fixtures と純粋関数だけで書く★ |
| 13 | ★**シードに実在の住所・電話番号・社名・人名を入れること**★ | `CLAUDE.md` §7 / D-043。★fixtures の架空値をそのまま使えば安全★ |
| 14 | ★**`git add -A` を使うこと**★ | `CLAUDE.md` §7。★過去に別作業の途中ファイルを巻き込む事故が起きています。必ずファイルパスを明示★ |
| 15 | ★**`.env.local` をコミットすること**★ | `CLAUDE.md` §9.3。★`git add` でパスを明示すれば入ってしまいます★。★確認後は `USE_FIXTURES=true` に戻すこと★ |
| 16 | ★**DB のユーザー名・パスワードを git 管理下の文書やコードに書くこと**★ | `CLAUDE.md` §9.3。★`.env.local` にのみ置く★ |

---

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

| 段階 | 内容 | コミット |
|---|---|---|
| 段階0 | 前提の確認（★読むだけ★） | **コミットなし** |
| 段階1 | シードに D-Sec の3テーブルを足す | 段階1で1コミット |
| 段階2 | `prismaSource.dsDevices()` を実装する | 段階2で1コミット |
| 段階3 | `prismaSource.dsAlerts()`（disconnected のみ）を実装する | 段階3で1コミット |
| 段階4 | ★**動作確認（ここが本題）**★ | コミットなし |
| 段階5 | 記録と push | 段階5で1コミット + push |

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

---

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

### 読むもの

| # | 対象 | 見る点 |
|---|---|---|
| 1 | ★D-065 Q1-2 / Q1-3★（`docs/decisions.md:483-484`） | ★`controls.status` は 0/1 の2値★ / ★`control_error_histories.status` は 0/1/2★ |
| 2 | ★D-067 項1★（`docs/decisions.md:604`） | ★2系統の取得元は完全に別★。★同じテーブルから取ろうとしないこと★ |
| 3 | ★D-070 D-3★（`docs/decisions.md:1133`）+ ★訂正節★（`:1161`） | ★`controls.status` を読むだけでよい★。★`intervals` から判定を再現する必要はない★ |
| 4 | ★D-090★（`docs/decisions.md:3325`） | ★本指示書の設計根拠と未決20件★ |
| 5 | ★指示書M の `facilities()` / `schedules()`★（`docs/handoff/指示書M_ローカルDBで動かす第1歩.md:529-720`） | ★**雛形として読む**★。★遅延 import・変換の書き方・テストの作り方をそのまま踏襲する★ |
| 6 | ★`lib/db/prisma.ts`★（全40行） | ★シングルトン・★遅延 import が必須★な理由（`:4-8`）★ |
| 7 | ★`lib/db/local-guard.ts`★（全31行） | ★`dwatch_local` 以外を弾く仕組み。★そのまま効きます★★ |
| 8 | ★`scripts/seed-local-db.ts`★（全276行） | ★冪等の作り（`:152-158`）★ / ★スキップの作法（`:128-149`）★ / ★id を明示する方針（`:14-16`）★ |
| 9 | ★D-086 の「段階5で発見した想定外の不具合」★（`docs/decisions.md:2912-2930`） | ★Edge Runtime バンドル破壊★。★404 が出たらこれを疑う★ |

### ★接続先の確認（★これを飛ばさないこと★）★

★`.env.local` の `DATABASE_URL` が `dwatch_local` を指していることを確認してください。★
★**値そのものを画面・ログ・文書に出さないこと**★（`CLAUDE.md` §9.3）。
★確認は「ローカルを向いているか」だけで十分です★（`lib/db/local-guard.ts:10-20` と同じ判定）。

### 段階0の完了条件

- 上記9点を読んだ
- ★着手前確認（施設12 の `DS-012`）の回答を得た★
- ★着手前の基準値（`npm test` のファイル数・テスト数 / `tsc` / `lint`）を自分で実測して控えた★

---

## 段階1 — シードに D-Sec の3テーブルを足す

### 1-1. 作るもの

★`scripts/seed-local-db.ts` に**追記**する★（新規ファイルを作らない）。
★ガード（`assertLocalDwatchDatabaseUrl`。`:40`）はそのまま効きます★。

★新たにシードするテーブル（挿入順）★: `controls` → `control_reports` → `control_error_histories`

### 1-2. ★`controls`★

- ★`lib/fixtures/ds-devices.ts` の `dsDevices` を import して変換する★
- ★`section_id` は `sections` に実在する id を渡すこと★（`prisma/schema.prisma:182` / `:193` の NOT NULL FK）
  - ★既存シードは `sections.id` を**ロボットの id と同じ**にしている★（`scripts/seed-local-db.ts:217-224`）
  - ★施設ごとに section を引ける対応表を作ること★（★`facility_id` → その施設の section の1つ★）
- ★★施設12（フレッシュタウン湖畔店）は `sections` が0件なので、`DS-012` をそのままシードすると
  FK 違反で止まります★★ → ★着手前確認の回答に従うこと★
  - ★A案を採る場合は、既存のスケジュール除外（`scripts/seed-local-db.ts:128-149`）と**同じ書き方**にすること★:
    ★`facility_id: 12` を名指ししない★。★「section が存在しない施設のデバイス」を構造的に弾き、
    スキップ件数が想定と違ったら例外で止める★
- ★`status` は `connection_status` から逆変換★（`"ok"` → 0 / `"error"` → 1）
- ★`password` は NOT NULL（`prisma/schema.prisma:177`）★ — ★fixtures に無いのでダミー値を入れる★。
  ★**実在しそうな文字列を入れないこと**★（`CLAUDE.md` §7 / D-043）
- ★id は明示する★（`scripts/seed-local-db.ts:14-16` の方針。AUTO_INCREMENT に任せない）

### 1-3. ★`control_reports`★

- ★`DsDevice.last_reported_at` が `reported_at` の元になります★
- ★★デバイスごとに「最新1件」が正しく引けるデータを入れること★★
  - ★1件だけ入れるのでは「最新を選ぶロジック」が検証できません★。
    ★**少なくとも数台については、より古い行をもう1件足すこと**★
  - ★古い行の `reported_at` は `last_reported_at` より確実に前にすること★
- ★`facility_id` / `section_id` / `control_id` / `serial_number` はすべて NOT NULL★
  （`prisma/schema.prisma:325-328`）

### 1-4. ★`control_error_histories`★

- ★`lib/fixtures/ds-alerts.ts` の `dsAlerts` のうち ★`alert_type === "disconnected"` の行だけ★ を変換する★
  - ★`security_off` の行はシードしない★（DB に置き場所が無い。禁止事項1）
  - ★スキップ件数を数えて出力すること★（★fixtures の `security_off` の件数と一致するはず★ — 自分で数えること）
- ★`status` は `resolution_status` から逆変換★（`"pending"` → 0 / `"in_progress"` → 1 / `"done"` → 2）
- ★`comment` ← `memo`★ / ★`occurred_at` ← `detected_at`★ / `responded_at` / `resolved_at` はそのまま
- ★`control_id` は `serial_number` から引くこと★（`device_serial` → `controls.id` の対応表）

### 1-5. ★冪等にすること★

★`deleteMany` を挿入の逆順で先頭に足す★（`scripts/seed-local-db.ts:151-158` の既存ブロックに、
★D-Sec の3本を**先頭側**（＝ clean_schedules より前）に★ 追加する）:

```
control_error_histories → control_reports → controls → （既存）clean_schedules → robots → sections → ...
```

★**2回続けて実行しても結果が同じであること**を実際に確かめること★。

### 1-6. ★★`facility_name` はシードに渡さないこと★★

★fixtures の `facility_name`（`ds-devices.ts` / `ds-alerts.ts` の文字列）を使ってはいけません。★
★シードに渡すのは `facility_id` だけ★です。施設名は ★段階2 / 段階3 で `facilities` を結合して取ります★。

★**これが「城南店」「緑野第二店」の不整合が構造的に消える仕組みそのもの**です★（D-090 項4）。

★同様に `section_name`（`ds-alerts.ts` のダミー値。`lib/fixtures/ds-alerts.ts:15-17` に
「暫定のダミー値」と明記）も渡しません。★`sections.section_name` を結合で取ります★。

### 1-7. ★検算★

★シード後にテーブル件数を数えること★。

```
# ★mysql.exe は PATH に無い。フルパスで呼ぶこと★
# ★接続アカウントは `.env.local` の DATABASE_URL に書いてあるものを使う★
#   （★この文書にユーザー名・パスワードを書かないこと★。CLAUDE.md §9.3）
SELECT (SELECT COUNT(*) FROM controls) c,
       (SELECT COUNT(*) FROM control_reports) cr,
       (SELECT COUNT(*) FROM control_error_histories) ceh;
```

★期待値は**自分で fixtures を数えて立てること**★（★作成時点の参考: `controls` は fixtures の
デバイス数から施設12 の分（A案なら1件）を引いた数 / `control_error_histories` は
`disconnected` の件数★ — ★鵜呑みにせず実測すること★）。

### 段階1の完了条件

- ★**2回続けて実行しても結果が同じであること**★
- ★**スキップされたデバイス／アラートの件数と内容が出力されていること**★
- ★**ガードが効くこと**★（★`DATABASE_URL` を一時的に別の値にして中断を1回だけ確かめ、必ず元に戻す★）
- ★**`prisma/schema.prisma` に差分が無いこと**★（`git diff --stat prisma/` が空）
- ★**`lib/fixtures/` に差分が無いこと**★（`git diff --stat lib/fixtures/` が空）
- `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス
- ★**ここで1コミット**★

---

## 段階2 — `prismaSource.dsDevices()` を実装する

### 2-1. 作るもの

★`lib/db/index.ts:156-158` の `notImplemented("dsDevices")` を実装に置き換える★。
★`facilities()`（`:162-198`）と同じ形にすること★（遅延 import → `findMany` → `map` で変換）。

### 2-2. ★結合★

`controls` + `control_reports`（★最新1件★）+ `facilities`

- ★`facilities` は `facility_name` を取るため★（★fixtures の手書き文字列は使わない★）
- ★`control_reports` は `last_reported_at` を取るため★。
  ★**デバイスごとに `reported_at` が最大の1件**を選ぶこと★
  （Prisma の `include` で `orderBy` + `take: 1` を使うのが素直。★段階1-3 で古い行を入れたのは
  ここを検証するためです★）

### 2-3. ★変換★

| 出力（`DsDevice`。`lib/types/index.ts:295-302`） | 入力 |
|---|---|
| `serial_number` | `controls.serial_number` |
| `facility_id` | `controls.facility_id`（★`Number()` で変換。`BigInt` のまま返さない★ — `facilities()` の `:180` と同じ） |
| ★`facility_name`★ | ★`facilities.facility_name`（結合）★ |
| `connection_status` | ★`controls.status` 0 → `"ok"` / 1 → `"error"`★（D-065 Q1-2） |
| `last_reported_at` | `control_reports.reported_at`（最新1件）。★ISO 8601（JST, +09:00）★（`CLAUDE.md` §9.3） |
| ★`note`★ | ★**常に `null`**★（下記 2-4） |

### 2-4. ★`note` は常に `null` を返すこと★

★実値は `app/api/ds-devices/route.ts:75-78` が JSON ストアからマージします★（D-065 Q1-1）。
★`prismaSource` 側では何もしません。★
★**理由をコードコメントに残すこと**★（後任が「実装漏れ」と誤解して埋めにいかないように）。

### 2-5. ★`deleted_at` の扱い★

- ★`controls` には `deleted_at` があります★（`prisma/schema.prisma:188`）→ ★`where: { deleted_at: null }` で絞る★
  （`facilities()` の `lib/db/index.ts:169` と同じ）
- ★`control_reports` には `deleted_at` がありません★ → ★書くと型エラーになります（＝安全側）★。
  ★型エラーが出たら「このテーブルには無い」というサインなので、無理に回避しないこと★

### 2-6. ★`inactive` では絞らないこと★

★禁止事項10★。第1歩と同じ方針（D-086 / `lib/db/index.ts:165-167`）。
★理由をコメントに残すこと★。

### 2-7. ★テスト★

★DB 依存のテストは書かないこと★（禁止事項12）。
★変換の中に純粋関数として切り出せる部分（コード値 → 文字列）があれば、そこにテストを書く★。
★期待値表を先に作ること★（`CLAUDE.md` §9.4）。

### 段階2の完了条件

- `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス
- ★`lib/ds-devices/query.ts` / `app/api/ds-devices/` / 画面に差分が無いこと★（禁止事項8）
- ★**ここで1コミット**★

---

## 段階3 — `prismaSource.dsAlerts()` を実装する（★disconnected のみ★）

### 3-1. 作るもの

★`lib/db/index.ts:159-161` の `notImplemented("dsAlerts")` を実装に置き換える★。

### 3-2. ★結合★

`control_error_histories` + `controls` + `sections` + `facilities`

- ★`controls` は `device_serial` を取るため★
- ★`sections` は `section_name` を取るため★（★fixtures ではダミー値だった。
  `lib/fixtures/ds-alerts.ts:15-17`★）
- ★`facilities` は `facility_name` を取るため★

### 3-3. ★変換★

| 出力（`DsAlert`。`lib/types/index.ts:312-330`） | 入力 |
|---|---|
| `id` | `control_error_histories.id`（★`Number()` で変換★） |
| ★`alert_type`★ | ★固定で `"disconnected"`★（`control_error_histories` は制御盤異常しか持たない。D-067 項1） |
| `device_serial` | `controls.serial_number`（結合） |
| `facility_id` | `control_error_histories.facility_id`（★`Number()`★） |
| ★`facility_name`★ | ★`facilities.facility_name`（結合）★ |
| ★`section_name`★ | ★`sections.section_name`（結合）★ |
| `detected_at` | `control_error_histories.occurred_at`（★ISO 8601 / +09:00★） |
| ★`resolution_status`★ | ★`status` 0 → `"pending"` / 1 → `"in_progress"` / 2 → `"done"`★（D-065 Q1-3） |
| `responded_at` / `resolved_at` | 同名カラム（nullable のまま） |
| ★`memo`★ | ★`control_error_histories.comment`★（★書き込み許可4カラムの1つ。D-042★） |

### 3-4. ★★`security_off` を返さないことを docstring に明記すること★★

★メソッドの直上に、次の3点をコメントで残すこと★:

1. ★このメソッドは `disconnected` しか返さないこと★
2. ★理由: `security_statuses` / `security_status_histories` に
   `id` / `device_serial` / `resolution_status` / `responded_at` / `resolved_at` / `memo` に
   対応するカラムが無い（D-090 項3-9）。`detected_at` も `@updatedAt` のため取れない（同 3-10）★
3. ★帰結: `/ds-alerts` の件数が減り、`/dashboard` のセキュリティバナーが出なくなる。
   **これは意図した挙動であり、実装漏れではない**。第3歩で扱う★

★★「実装漏れ」ではなく「意図的な未実装」だと後任が一目で分かるようにすること★★。

### 3-5. ★`deleted_at` / `inactive`★

- ★`control_error_histories` には `deleted_at` がありません★（`prisma/schema.prisma:342-360`）→ ★絞らない★
- ★`controls` の `deleted_at` は結合側で絞ってよい★
- ★`inactive` では絞らない★（禁止事項10）

### 段階3の完了条件

- `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス
- ★docstring に「`security_off` を返さない」と理由が書かれていること★
- ★`lib/ds-alerts/query.ts` / `app/api/ds-alerts/` / `lib/dashboard/` / 画面に差分が無いこと★（禁止事項8）
- ★**ここで1コミット**★

---

## 段階4 — ★★動作確認（ここが本題）★★

★**ここまでの3段階は準備です。この段階で「本当に DB から出ているのか」を確かめます。**★

### 4-1. 起動

- ★**`.env.local` の `USE_FIXTURES` を `false` にする**★
  （★`lib/db/use-fixtures.ts` は `process.env.USE_FIXTURES !== "false"` なので、
  ★**文字列 `false` 以外はすべて fixtures 扱い**★です。`FALSE` や `0` では切り替わりません）
- ★**`npm run dev` を Bash で起動しないこと。Browser pane の `preview_start` を使う**★
  （★`.claude/launch.json` に `d-watch`（`npm run dev` / port 3000）があります★ — ★着手時に確認すること★）
- ★**Browser pane が黒画面 / 0×0 になる既知の問題**★: ★`preview_start` に `url` を渡して開き直す★
  （指示書O `:183` の前例）
- ★**環境変数を変えたら dev サーバを再起動し、反映されていることを確認すること**★

### 4-2. ★★確認1 — `/ds-logs`（F-06）★★

| # | 項目 |
|---|---|
| 1 | ★台数が期待どおりであること★ — ★fixtures は **12台**（正常7 / 異常5）★。★A案（施設12 除外）を採った場合は **11台**（正常7 / 異常4）★。★着手前確認の回答に応じて期待値を確定させること★ |
| 2 | ★★施設名が「バリューマート城南店」ではなく「フレッシュタウン大和店」で出ること★★（`DS-011` / 施設10） ← ★**これが今回の成果**★ |
| 3 | ★★施設12 を残した場合、「メガバリューマート緑野第二店」ではなく「フレッシュタウン湖畔店」で出ること★★（`DS-012`） |
| 4 | ★`last_reported_at` が正しい（最新の `control_reports` の値である）こと★ — ★段階1-3 で古い行を足したデバイスで、**古いほうが出ていないこと**を確認する★ |
| 5 | ★備考が消えていないこと★ — ★JSON ストアに保存済みの値があれば、それが表示されること★（`app/api/ds-devices/route.ts:75-78` のマージが効いている証拠） |
| 6 | ★ソートが「異常 → 正常」であること★（サーバ側ソート。`lib/ds-devices/query.ts`） |

### 4-3. ★★確認2 — `/ds-alerts`（F-07）★★ ← ★件数の突き合わせを飛ばさないこと★

| # | 項目 |
|---|---|
| 1 | ★★件数を数えること★★ — ★fixtures は **12件**（`security_off` 7 + `disconnected` 5）★、★DB 経由では **5件**★（`disconnected` のみ）。★A案で `DS-012` を除外しても、`DS-012` のアラートは `security_off` なので **disconnected の5件は変わらない**★ — ★自分で fixtures を数えて確かめること★ |
| 2 | ★★差がちょうど7件（= `security_off` の件数）であること★★。★これ以外の差が出たら変換のバグです★ |
| 3 | ★`section_name` が実データで出ること★ — ★fixtures のダミー値（「1F売り場」「2F衣料品」「1Fレジ前」「B1食品」）ではなく、シードが入れた `sections.section_name` が出ること★ |
| 4 | ★施設名が施設マスタの実名で出ること★（確認1-2 と同じ趣旨） |
| 5 | ★`resolution_status` の3値がすべて出ること★（`pending` / `in_progress` / `done` — ★fixtures の `disconnected` には `done` が無い（`lib/fixtures/ds-alerts.ts:182-185` に「意図的に作っていない」と明記）ので、**`done` が0件なのが正しい**★） |
| 6 | ★`memo` が `comment` から出ていること★ |

### 4-4. ★★確認3 — `/dashboard`★★

| # | 項目 |
|---|---|
| 1 | ★★セキュリティバナーが出なくなること★★（★`security_off` が返らないため。`lib/dashboard/aggregate.ts:113-125`★）。★**これは想定内。記録すること**★ |
| 2 | ★ただし `cleanLogs` が未実装なので `/api/dashboard` 自体が 500 になるはず★（`app/api/dashboard/route.ts:57` が `db.cleanLogs()` を呼ぶ）。★**バナーの確認は 500 の内側なので目視できない可能性が高い**★ → ★その場合は「確認できなかった」と正直に記録すること★ |

### 4-5. ★★確認4 — 他画面が落ちることを「確認して記録する」★★

★**これは失敗ではありません。第2歩の想定どおりの状態です。**★

| 画面 | 呼んでいる未実装メソッド | 期待 |
|---|---|---|
| `/dashboard` | `cleanLogs` / `prechecks` | ★500★ |
| `/detail-log` | `cleanLogs` | ★500★ |
| `/alerts` | `prechecks` / `cleanLogs` | ★500★ |
| `/reports` | `reportKpis` / `facilityPerformance` / `continuousProblems` / `errorBreakdown` | ★500★ |
| ★`/facilities` / `/schedules`★ | ★（実装済み。指示書M）★ | ★**200。回帰していないこと**★ |
| ★`/ds-logs` / `/ds-alerts`★ | ★（本作業で実装）★ | ★**200**★ |

- ★エラーメッセージが `[db] xxx のPrisma実装はまだありません。USE_FIXTURES=true で開発してください。`
  であること★（`lib/db/index.ts:143-147`）
- ★★**404 が出たら Edge Runtime バンドル破壊の回帰を疑うこと**★★
  （D-086 の「段階5で発見した想定外の不具合」。`docs/decisions.md:2912-2930`）。
  ★500 と 404 は意味が違います★ — ★500 = 想定どおりの未実装 / 404 = ビルドが壊れている★

### 4-6. ★`USE_FIXTURES=true` に戻す★

- ★`.env.local` を `true` に戻し、dev サーバを再起動する★
- ★**全画面が従来どおり動くこと**を確認する★（★特に `/ds-logs` 12台 / `/ds-alerts` 12件 /
  `/dashboard` のセキュリティバナーが出ること★）
- ★これを確認して初めて「壊していない」と言えます★

### 4-7. ★ログの確認★

- ★`read_console_messages` でブラウザ側のエラー0件★
- ★`preview_logs` でサーバ側のエラーを確認★（★`A Node.js module is loaded` が出ていないこと★ ← 4-5 の404対策）

---

## 段階5 — 記録と push

### `docs/decisions.md` に ★D-091★ を追記

★**着手直前に `grep -n "^### D-0" docs/decisions.md` で最終番号を確認すること**★。
★D-089 は指示書O が、D-090 は本指示書の設計根拠が既に取得済みです★。★空き番号は自分で確かめること★。

★記録すること★:

1. ★実装した2メソッドと、それぞれの結合・変換★
2. ★★`security_off` を含めなかった理由★★（対応カラムが無い。D-090 項3-9 / 3-10）と、
   ★その帰結（`/ds-alerts` が5件になり、`/dashboard` のバナーが出なくなる）★
3. ★★fixtures の不整合が構造的に消えたことの**実測**★★ —
   ★「城南店」→「フレッシュタウン大和店」／「緑野第二店」→「フレッシュタウン湖畔店」が
   実際に画面で確認できたか★。★`section_name` がダミー値でなくなったか★
4. ★★施設12 の `DS-012` をどう扱ったか★★（着手前確認の回答と、その結果の台数）
5. ★段階4 で確認できなかった項目があれば、その理由★（★ブラウザが使えなかった場合の代替手段と限界を明記★。
   指示書N・指示書O の前例）
6. ★第3歩に残したもの★（`dsAlerts` の `security_off` / `cleanLogs` ほか。D-090 項2 の表を引くこと）
7. ★実測値★（`npm test` のファイル数・テスト数 / `tsc` / `lint`。★着手前の基準値との差★）

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

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

### push

- ★`git add` は必ずファイルパスを明示。`git add -A` は禁止★（禁止事項14）
- ★`.env.local` をコミットしないこと★（禁止事項15）。★`USE_FIXTURES=true` に戻したことを確認してから★
- `git push origin develop`（★`main` へは push しない★）

### 段階5の完了条件

1. `docs/decisions.md` に決定番号を追記した（★上記7項目すべて記録★）
2. `docs/handoff.md` に追記した
3. ★`origin/develop` へ push 済み★
4. ★文書末尾の「実行状態」表を更新した★

---

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

★本指示書の範囲外です。手を出さないこと★（D-090 項2 の分割表）:

| 対象 | いつ |
|---|---|
| ★`dsAlerts` の `security_off` 側★ | 第3歩 |
| `cleanLogs` | 第3歩 |
| `prechecks` | 第4歩 |
| `reportKpis` / `facilityPerformance` / `errorBreakdown` | 第4歩 |
| `continuousProblems` | 第5歩 |
| ★点検記録2本（`inspections` / `inspectionDetail`）★ | ★最後★（D-072 項7。`docs/decisions.md:1052`） |

★D-090 項3 の未決20件も本指示書では解決しません★。★「未決である」と分かっている状態を保つのが正しい★。

---

## 完了条件（全体）

1. ★**着手前確認1件（施設12 の `DS-012`）の回答を得てから段階1 に入った**★
2. 段階0〜5 をすべて実施したこと（★途中で止めた場合は、どこで止めたかを実行状態表に書くこと★）
3. `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス
4. ★**`USE_FIXTURES=false` で `/ds-logs` と `/ds-alerts` が 200 で表示されること**★
5. ★★**施設名が施設マスタの実名（フレッシュタウン大和店 / フレッシュタウン湖畔店）で出ること**★★
6. ★★**`/ds-alerts` の件数が fixtures より7件（= `security_off` の件数）少ないことを実測で確認したこと**★★
7. ★**`section_name` が実データ（`sections.section_name`）で出ること**★
8. ★**`dsAlerts()` の docstring に「`security_off` を返さない」と理由が書かれていること**★
9. ★**`USE_FIXTURES=true` に戻して全画面が従来どおり動くこと**★
10. ★**`prisma/schema.prisma` / `lib/fixtures/` / `lib/ds-devices/query.ts` / `lib/ds-alerts/query.ts` /
    API ルート / 画面 に差分が無いこと**★
11. ★**`lib/db/prisma.ts` / `lib/db/local-guard.ts` / `lib/db/use-fixtures.ts` /
    `prismaSource.facilities()` / `prismaSource.schedules()` に差分が無いこと**★
12. `docs/decisions.md` / `docs/handoff.md` の更新
13. ★**`origin/develop` へ push 済み**★
14. **文書末尾の「実行状態」表を更新した**

---

## 実行状態

| 段階 | 状態 | コミット |
|---|---|---|
| ★**着手前の確認**★ | ★**回答済み（A案。施設12の`DS-012`をシードから除外）**★ | — |
| 段階0 前提の確認 | 完了 | コミットなし |
| 段階1 シードに D-Sec の3テーブルを足す | 完了 | `9b39847` |
| 段階2 `prismaSource.dsDevices()` | 完了 | `7811572` |
| 段階3 `prismaSource.dsAlerts()`（disconnected のみ） | 完了 | `f05b522` |
| 段階4 動作確認 | 完了（Browser pane無し。curlで代替。限界は D-097 §5 参照） | コミットなし |
| 段階5 記録と push | 完了 | 本コミット |

**実行日**: 2026-09-09
**取得した決定番号**: D-097（`docs/decisions.md:4223`）。途中の寄り道で D-095（`dwatch_local`作り直し）・
  D-096（指示書Q。`prisma/schema.prisma`再同期）も取得
**着手前の実測値**: 33ファイル / 631テスト、`tsc` 0、`lint` 0
**施設12 の `DS-012` の扱い**: A案採用（シードから除外）。ユーザー事前回答どおり
**`/ds-logs` の台数**: 12台 → **11台**（正常7 / 異常4）
**`/ds-alerts` の件数（fixtures / DB）**: 12件 → **5件**（disconnectedのみ。差7件 = security_off件数と一致）
**完了後の実測値**: 33ファイル / 631テスト（変化なし）、`tsc` 0、`lint` 0
**詳細**: `docs/decisions.md` D-097 を参照
