# 指示書BB — データ層・API の「止まる経路」を塞ぎ、削除済みの施設・清掃エリアの扱いを全画面で揃える

**日付**: 2026-10-19 / **担当**: Claude（★ユーザー決定 2026-10-19★）/ **基点**: develop `2fc3bbb`
**前提**: D-180（特に U8・D12・D3）／D-173（結果待ち）／D-159（ダッシュボードの直近モード）／D-109（連続日数の入力は1本）／D-091 項1／D-071（月跨ぎ週）／
指示書AK §2-1（対象顧客の決定表）・AW §11 Q3（PATCH の1件読みの fail-loud）・AX・AZ・BA／
`CLAUDE.md` §0（特に §0-1・§0-2）・§3（時刻は JST の壁時計値・ステージングは読み取り＋4カラムだけ）・§4・§6・§7・§9.3〜§9.5

★**ユーザー決定（2026-10-19。§2-1 の U1〜U4 と A1〜A20）は確定仕様。変えないこと**★（`CLAUDE.md` §0-1）。
★**期待値表（§4）は正。変えないこと**★（`CLAUDE.md` §0-2）。表の中で ★本書が導いた行★ は「導出」と書いた。★監督が変えるなら §10 で返す★（実装担当が直さない）。
★**本書の設計判断（§2-2 の D1〜D24）は監督の確認待ち**★ — ★実装は本書のとおりに進めてよい★（§10）。

★**行番号はすべて 2026-10-19 に develop `2fc3bbb` で確かめたもの**★。同じファイルを上から直すと後ろの行はずれるので、★2つ目以降の場所は関数名・引用した文で探し直すこと★（特に `lib/db/index.ts`（2,938行）・`lib/types/index.ts`・`d-watch-api.yaml`）。

★**本書は「書いてあることを全部やる」前提で、直す場所を1つずつ挙げてある**★。表に無い場所で同じ形を見つけたら ★§3 のルールで直し、作業報告に書く★（黙って直さない・黙って残さない）。

★**`git add -A`・`git add .` は禁止**★（`CLAUDE.md` §7）。★ファイルパスを1つずつ書く★。★push しない★（§11。監督のレビューの後）。

★★**`docs/decisions.md` は、本書の作成時点で別のセッションが未コミットの追記をしている**★★（D-179 の「追記（2026-10-19 同日・4回目）」の9行。2026-10-19 `git diff -- docs/decisions.md` で確認）。
★**同じ日に、別のセッションのコミット（`cc09649`）が作業ツリーの未コミットの追記ごと上書きして消した事故が起きている**★（その追記の末尾の「記録の事故」）。
→ ★段階0-2 と段階6-1 の手順を必ず守ること★（自分のものでない差分があるあいだは `docs/decisions.md` を書かない・`git add` しない）。

---

## 0. 何をするか

★**範囲はデータ層と API**★（`lib/db/**`・`app/api/**`・`lib/**/query.ts`・`lib/clean-logs`・`lib/dashboard`・`lib/reports`・`lib/settings`・`lib/prechecks`・`lib/judge`・`lib/format`・`lib/config`・`lib/types`・`lib/logger/events.ts`・`d-watch-api.yaml`・fixtures・テスト・`package.json`・`tsconfig.json`・資料）。
★**画面の見せ方は指示書BC**★（別のエージェントが並行して書いている。★`docs/handoff/指示書BC_*` には書かない・読んで合わせるだけ★）。境目は §8。

| # | やること | 種類 | 段階 |
|---|---|---|---|
| 1 | ★U2★ 有効なロボットがいない清掃エリアで、清掃詳細ログ・ダッシュボード・アラート（出発失敗）・スケジュールが500になる → ★例外にせず、シリアル・機種を `null`（画面は「-」。BC）にし、`data.anomaly` を出す★。`robots[0]` の並びを決める（`id` 昇順） | 不具合 | 段階1 |
| 2 | ★U3★ `robot_error_reports.status` が 0/1/2 以外の行で `/prechecks` が500になる → ★SQL と純粋関数の両方で外す★。`PATCH /prechecks/{id}/status`・`PATCH /errors/{error_id}/status` の1件読みも ★404★ | 不具合 | 段階1 |
| 3 | `/ds-devices`: 心拍（`control_reports`）がまだ無い制御盤・`controls.status` が 0/1 以外の制御盤で500 → ★心拍なしは `last_reported_at: null` で出す・状態不明は出さない★（備考の保存も404） | 不具合 | 段階1 |
| 4 | ダッシュボード直近モード: `cleanLogs()` と `cleanLogRunMeta()` を並列で読むため、間に行が増えると `pickLatestRunPerSection()` が例外 → ★その行を選ばず、ログに残す★ | 不具合 | 段階1 |
| 5 | ★U1★ ★削除済み（`deleted_at` が NULL でない）の施設・清掃エリアの行を、全画面で出さない★（D-180 U8 を全画面に広げる）。`PATCH /prechecks/{id}/status` の見えない行は ★404★ | 仕様（ユーザー決定） | 段階2 |
| 6 | エリア別完了率（`/clean-logs/{id}/areas`）が ★結果待ち（D-173）★ を見ていない（レポートがまだ来ていないエリアが ×・問題エリアに数えられる） | 不具合 | 段階3 |
| 7 | 清掃詳細ログの CSV の日付に年が無い（`8/27`） → ★`2026/08/27`★ | 不具合 | 段階3 |
| 8 | 入力の検証: ★存在しない日付（`2026-02-31`）が黙って `03-03` になる★／★その月に無い週（第6週）が 200・空★／★`/prechecks` の `date_from > date_to` が通る★／★`/reports/kpi` の `group_id` と `customer_id` が AND になっていない★ | 不具合 | 段階3 |
| 9 | 頑丈さ: `try` の外の例外（標準の形でない500・`api.error` なし）／JSON の本文が `null` で TypeError／`rule: "constructor"` が通る／JSON ストアの同時書き込みが同じ `.tmp` を使う／確定期間のサマリが0行でも何も残らない | 不具合 | 段階4 |
| 10 | 読みすぎ: 連続日数の入力（217日×全施設）を施設で絞る／ダッシュボードの `dsAlerts()` を未解消だけにする | 性能 | 段階4 |
| 11 | 掃除: R1 の閾値の直書き（`<= 1`）・`LAUNCH_GRACE_MINUTES` の説明・未使用の `weekStart()`・古いコメント・未使用の `CLEAN_ERROR_LABELS`・`package.json`（`@next/env`・`engines`・`@types/node`）・`tsconfig.json`（`.claude` を外す） | 掃除 | 段階5 |
| 12 | YAML の説明の食い違い（`date_from` の省略時・`settleMinutes`・`limit` の範囲）・資料・★決定 D-182 の追記★ | 資料 | 段階6 |
| 13 | ★U4★ 清掃ログの行を開くと全期間を読む件は ★今回は直さない★（TeamWorks の索引待ち）。★§7-1 に分析と依頼文の下書き★ | 記録 | — |

★**変えないもの**★:
- ★判定の規則★（◎○△× の閾値・丸め・L1〜L3・R1〜R4 の意味・結果待ちの定義 `isResultPending()`・連続日数の数え方 `computeConsecutiveProblemDays()`・遡り幅 `loadConsecutiveInputs()` の計算）。★U1 で入力の行が減ることによる値の変化だけは意図したもの★（§4 表1 V15）
- ★4カラム以外へ書かない★（`lib/db/robot-error-write.ts` を触らない。`CLAUDE.md` §7）／★メインDBのスキーマを変えない・`prisma db pull` しない★（`prisma/schema.prisma` を触らない。D-172）／生SQL を書かない（`CLAUDE.md` §3）
- ★施設名・分類 → 施設ID の解決は `deleted_at` で絞らない（罠7）★（`lib/db/facility-name-source.ts`・`facility-group-source.ts`・`clean-area-source.ts` の `loadCleanAreaFacilityMasters()`・`lib/facilities/resolve-by-*.ts`）。★U1 で外すのは「行」であって「名前の解決」ではない★（D3）
- ★エラー一覧・D-Sec アラート・バナーの削除済みの扱い★（D-180 で済み。`isVisibleErrorRow()` / `ERROR_ROW_VISIBLE_WHERE` の意味）
- `/facilities`（施設マスタ。既に `deleted_at IS NULL`）・`/groups`・`/customers`
- ★`computeSecurityBanner()` のコードとテスト★（D-180 U7）
- 画面のコンポーネント（★§8 の1行を除く★）・`d-watch-mock.jsx`・`CLAUDE.md`・`docs/handoff.md`

---

## 1. 背景

- 2026-10-19、監督がデータ層・API を総点検した（読み取りのみ・当方のコード）。★「1行の想定外のデータで画面全体が500になる経路」★ と ★「削除済みの施設・清掃エリアの行が画面によって出たり出なかったりする」★ がまとまって見つかった。ユーザーに諮り、§2-1 のとおり決まった。
- ★削除済みの扱いの今（2026-10-19 `2fc3bbb`。コードで確認）★:

  | 外している | 外していない |
  |---|---|
  | エラー一覧 `GET /errors`・`PATCH /errors/{error_id}/status`（`lib/db/error-report-mapping.ts:43-51`）／D-Sec アラート・バナー（`lib/db/ds-alert-mapping.ts:90` `:101`）／レポートの施設別パフォーマンスの施設（`lib/db/index.ts:2509-2515` の `deleted_at: null`）／連続問題施設の施設（`:2780-2785`） | ★アラート `/prechecks` の一覧と `PATCH /prechecks/{id}/status`★（`:1457-1477`・`:1680-1735`。★削除済みの行に書き込める★）／★清掃詳細ログ・ダッシュボード（数値カード・問題ログ・直近のメタ）・`/clean-logs/{id}/*`★（`mainTimetableWhere()` `:869-903`）／★`/schedules` とダッシュボードの予定件数★（`clean_schedules.deleted_at` だけ。`:2093`）／レポートKPI の当期・エラー種別内訳（確定・当期）・施設別パフォーマンスの当期の集計（清掃エリアの削除）／要レスキュー／総清掃面積（`/clean-area`）／エリア別完了率 |

- ★`lib/db/index.ts:924-929` のコメントは「論理削除された施設のログも返している（それが現状の挙動）」と明記している★。総清掃面積の `lib/db/clean-area-source.ts:41-60` は「★それでも消えた施設を出したくないと言われたら、`cleanLogs()` 側と一緒に決めること★」と書いている — ★今回ユーザーが全画面で外すと決めた★（U1）。
- ★テストの件数（`2fc3bbb`）★: ★130ファイル / 3,083件 全件通過★（2026-10-19 本書の作成者が `npx vitest run` で実測。作業ツリーは他のセッションの docs 2ファイルの変更だけ）。
- ★ステージングの実データでの影響件数は未測定★（段階7-6 で SELECT だけで測る。`CLAUDE.md` §6-6 の出所つき）。

---

## 2. 決定

### 2-1. ユーザー決定（2026-10-19。確定）

| # | 決定 | 帰結 |
|---|---|---|
| U1 | ★削除済み（`deleted_at` が NULL でない。日時の値によらない）の施設・清掃エリアの行は、★全画面で出さない★★（D-180 U8 を全画面に広げる） | §4 表1。★`PATCH /prechecks/{id}/status` で見えない行は404★（`/errors` と同じ）。★名前の解決（罠7）は変えない★。★サマリ（TeamWorks が集計）は外せる範囲だけ外し、外せないものは制約として記録★（D5） |
| U2 | ★有効なロボットがいない清掃エリアで落ちない★。「対象なし」か「-」を出す（★どちらにするかと理由は本書が決める★）。`data.anomaly` を出す。★`robots[0]` に並びが無い★ので決まった並びにする | D6・D7。§4 表2 |
| U3 | ★`/prechecks` は `status` が 0/1/2 以外の行で落ちない★ — エラー一覧と同じく SQL（`errorReportStatusCodes`）と純粋関数の両方で外す。★`errorReportById`・事前チェックの1件読みも 404（500 にしない）★ | D8。§4 表3。★AW §11 Q3（PATCH の1件読みは fail-loud で500）を上書きする★ |
| U4 | ★清掃ログの行を開くとき（`/advice`・`/areas`）の全期間読みは ★今回は直さない★★。TeamWorks が `clean_task_reports.clean_timetable_id` と `robot_error_reports.clean_timetable_id` に索引を足すまで待つ | §7-1 に分析と依頼文の下書き。★単一ID の連続日数の入力の絞り込みは索引の後★（段階4 の施設での絞り込みでも、単一ID の経路は今のまま） |
| A1 | `/ds-devices`: 心拍がまだ無い制御盤・`controls.status` が 0/1 以外で落ちない → `last_reported_at` を nullable に（★YAML も★）・例外にしない・ログ | D9。§4 表4 |
| A2 | ダッシュボードの直近モードの突き合わせの食い違い → ★飛ばしてログ★ | D10。§4 表5 |
| A3 | エリア別完了率が結果待ち（D-173）を見ていない → 期待値表を作って直す | D11。§4 表6 |
| A4 | 清掃詳細ログの CSV の日付に年を入れる（`YYYY/MM/DD`） | D12。§4 表7 |
| A5 | `try` の外の例外（設定の GET 2本・アドバイスの文言の読み込み）と、JSON の本文が `null` の TypeError（4ルート）→ ★標準の形の 422 / 500 とログ★ | D18・D19。§4 表8 |
| A6 | 読みすぎ: 連続日数の入力を `facility_ids` で絞る／★1リクエストで1回にできるなら★（★判定結果を変えないこと。期待値表で示す★）。ダッシュボードの `dsAlerts()` は未解消に絞るか読まない（★最小のほう★） | D16・D17。§4 表10 |
| A7 | `parseDate("2026-02-31")` が黙って `03-03` になる → 往復で確かめて422（★同じ関数を使う全経路★） | D13。§4 表8 |
| A8 | その月に無い週（例 第6週）→ ★422★（200・空にしない） | D14。§4 表8 |
| A9 | `/prechecks` の `date_from > date_to` → ★422★（画面の文言は BC） | §4 表8 |
| A10 | `PATCH /settings/advice` の `rule: "constructor"` が検証を通る → `Object.hasOwn` | §4 表8 |
| A11 | 確定期間のサマリが0行のとき KPI が黙って null → ★`data.anomaly` を出す（挙動は変えない）★ | §4 表13 |
| A12 | JSON ストアの同時書き込みが同じ `.tmp` を共有する → 一意の名前（pid＋乱数）か直列化 | D20。§4 表12 |
| A13 | R1 の閾値の直書き `consecutiveProblemDays <= 1` → `< ESCALATION_THRESHOLDS.level2Days`＋テスト（`level2Days = 3` の場合） | D21。§4 表11 |
| A14 | `LAUNCH_GRACE_MINUTES` は未使用なのに使っているように書いてある → 「未使用（D-091 項1 / D-173）」／`weekStart()` は未使用で D-071 と食い違う → 消すか明記（★テストだけが使うなら消す★）／`lib/judge/kpi.ts` の古い「暫定の式なので」→ 消す／`CLEAN_ERROR_LABELS`・`cleanErrorLabel` は本番で未使用 → テストだけなら消す（★`CLEAN_ERROR` はシードが使うので残す★） | D22 |
| A15 | `package.json`: devDependency に `"@next/env": "16.3.3"`／`tsconfig.json` の `exclude` に `.claude`（古いワークツリーの写しが型検査されている）／`"engines": { "node": ">=22.12" }` と `@types/node` を揃える（★版の選び方を書く★） | D23 |
| A16 | YAML の説明: `/clean-logs` の `date_from`「省略時は直近1日」→ 実際（全期間。D-159 追記）／`/dashboard` の `settleMinutes`（D-173 で削除）→「予定の終了 ≤ 現在」／`/prechecks` に「status 0/1/2 だけ」と削除済みの規則／`/clean-logs/{id}/areas` に結果待ちの扱い／`limit` の範囲 1〜200 と既定・範囲外の扱い | 段階6（挙動を変えた分は各段階で） |
| A17 | ★`/reports/kpi` の `group_id` と `customer_id` は YAML どおり AND にする★（実装は `customer_id` しか見ていない。`lib/reports/target-customer.ts:99-101`）＋テスト | D15。§4 表9。★指示書AK §2-1 の決定表1行目（`customer_id` が勝つ）を上書きする★ |
| A18 | （記録だけ）当期のエラー件数は事前チェック・予定なしのエラーを数えない（`lib/db/report-mapping.ts:412-424` `:1011-1027`）ので、週が締まった後の `facility_error_summaries` と食い違いうる → ★TeamWorks への質問の候補に足す★ | §7-4 |
| A19 | 決定は ★D-182★（空いていなければ次の番号）として `docs/decisions.md` の末尾に足す。U1〜U4 と本書の選択を書く | 段階6-1・付録A |
| A20 | 段階を分けてコミットする・確かめ方（tsc・lint・vitest の前後の件数・`USE_FIXTURES=true npm run build`・ローカルDB（任意）・ステージングの読み取り（SELECT だけ））・★push は監督のレビューの後★ | 段階0〜7・§11 |

### 2-2. 本書の設計判断（監督の確認待ち。実装は進めてよい）

| # | 決定 | 理由 |
|---|---|---|
| D1 | ★「見える行」の規則は1か所★: 新しい `lib/db/visibility.ts` に ★`isVisibleFacilitySection(row)`（正）と `FACILITY_SECTION_VISIBLE_WHERE`（SQL の写し）★を置く。★`isVisibleErrorRow()` / `ERROR_ROW_VISIBLE_WHERE`（`lib/db/error-report-mapping.ts:43-51`）は名前を残し、中身をこの2つに委ねる★ | D-180 の呼び出し元（約10か所・テスト）を書き換えずに済む。規則（施設と清掃エリアの `deleted_at` がどちらも NULL）は1つ |
| D2 | ★`clean_timetables` / `clean_timetable_histories` / `clean_schedules` / `clean_task_reports` / `facility_error_summaries` は SQL のリレーション条件だけで外す★（`deleted_at` の列を運ばない）。★`robot_error_reports` は D-180 と同じく「列を select して純粋関数で外す＋SQL は写し」★ | ★`IS NULL` は SQL と JS で意味が同じ★（AX D2 の ci・PAD SPACE の問題が起きない）。★これらの表には fixtures の削除の概念が無い★ので純粋関数の経路が無い。列を運ぶと `loadConsecutiveInputs()` の「列を増やしてよい条件」（`lib/db/consecutive-source.ts:300-340`）に触る。★テストは「`where` に条件が入っていること」を固定する★（壊すと赤になる。G3） |
| D3 | ★名前の解決は絞らない（罠7）を残す★。ただし理由を書き直す: 「★解決は行の可視性と別の責務★。削除済みの施設名で検索すると施設ID に解決され、その行は可視性の条件で外れて0件になる（絞っても0件。結果は同じだが、規則を1か所に保つ）」 | ユーザー指示（罠7 は残す）。旧理由「論理削除された施設のログは `cleanLogs()` に出ている」は U1 で失効するので、残すと嘘になる（G2） |
| D4 | ★連続日数（L1〜L3・R3/R4・要レスキュー・連続問題施設・施設別パフォーマンスの連続日数）の入力からも、削除済みの行を外す★（`loadConsecutiveInputs()` の `where`） | U1「全画面で出さない」。表示から消えた行が、生きている施設の連続日数を押し上げ続けると説明がつかない（施設単位で数える。D-103）。★値が短くなりうるのは意図した結果★（表1 V15）。★§10 Q1 で監督に返す★ |
| D5 | ★サマリ（確定期間）は外せる範囲だけ★: `facility_summaries`・`facility_error_summaries` は ★施設が削除済みの行を外す★（前者は今も `buildItem()` で外れている。後者は新たに `facility: { deleted_at: null }`）。★清掃エリアの削除は外せない★（施設単位で集計済み）。★`customer_summaries`（レポートKPI の確定期間）は何も外せない★ | TeamWorks の集計値を当方で再計算しない（D-102・`CLAUDE.md` §7）。★確定期間と当期で母集団が少し違いうる★ — 制約として D-182 と YAML に書き、TeamWorks への質問の候補（§7-4）に足す |
| D6 | ★U2 の表示は「-」★。★API は `null` を返し（`serial_number` / `model_name` を nullable に）、画面が「-」を出す（BC）、CSV は空のセル★ | ★「-」は画面で値が無いときの既存の約束★（`displayRate()`・D-171 の事前チェック列）。「対象なし」は判断を含む新しい語で、訳も要る。★API に「-」の文字列を入れない★ — 表示の整形は画面の仕事（`CLAUDE.md` §9.3）で、CSV では `csvCell()` の数式対策で `'-` になる（D-164 の事故と同じ） |
| D7 | ★`robots` の並びは `id` 昇順★（`orderBy: { id: "asc" }`）。★清掃エリアの有効なロボットが2台以上なら、1リクエストにつき清掃エリアごとに1回 `data.anomaly`★（P1 は1清掃エリア=1ロボット。`CLAUDE.md` §8） | ★意味を推測しない★（`created_at` の新しい順＝「入れ替えた新しい機体」と読むのは推測）。`id` は採番順で環境によらず決まる（`customers()` の `orderBy: { id: "asc" }` と同じ考え方）。2台は前提の崩れなので記録する |
| D8 | ★U3 の「0/1/2 か」は `lib/db/precheck-mapping.ts` の新しい純粋関数 `isKnownRobotErrorStatus(status)` の1本★（`robotErrorStatusFromResolutionStatus()` から作る。0/1/2 を直書きしない）。`ds-alert-mapping.ts` の `dsAlertStatusCodes()` はこれに置き換えてよい（同じ値） | `errorReportStatusCodes()`（`lib/error-reports/query.ts:123`）を `precheck-mapping.ts` から import すると循環する（`error-reports/query.ts` が `precheck-mapping.ts` を import している） |
| D9 | `/ds-devices`: ★心拍なし → 出す（`last_reported_at: null`）★／★`controls.status` が 0/1 以外 → 出さない（`summary` にも数えない）★。★`dsDeviceExists()` も同じ規則★（状態不明の制御盤の備考は404）。どちらも `data.anomaly` | 心拍なしは新しい制御盤で正当に起きる。状態不明を「異常」に寄せるのは推測（D-077 の考え方。D-Sec D3 と同じ「出さない・例外にしない」）。`dsDeviceExists()` を揃えないと、一覧に出ない制御盤の備考が保存できて孤児行が残る（`lib/db/index.ts:358-360` の注意書き） |
| D10 | ★`pickLatestRunPerSection()` はメタの無い行を ★選ばずに飛ばす★（例外にしない）。飛ばした id は新しい純粋関数 `missingRunMetaIds(rows, meta)` で数え、★ルートが `data.anomaly` を出す★。★メタの重複（E2）は今どおり例外★ | 戻り値の形を変えるとテストの `toEqual([...])` が全部変わる。重複は2表の重複除去（`findTimetables()`）の崩れで、競合では起きない |
| D11 | エリア別完了率: ★`AreaCompletionRow` と API の `AreaCompletionItem` に `result_pending: boolean` を足す★（判定は `isResultPending()` の1本）。★`grade` / `completion_rate` は変えない★（D-173 と同じ「別の旗」）。★`problem_count` と `filter=problem` から結果待ちを外す★。`total_count`・平均（null は元から分母外）・並びは今どおり | ダッシュボードの `problem_logs`（`app/api/dashboard/route.ts:203`）・`/clean-logs` の `result=problem`（`lib/clean-logs/query.ts:182`）と同じ扱い |
| D12 | CSV の日付は ★`YYYY/MM/DD`（ゼロ埋め）★。新しい `formatYmdSlash(iso)` を `lib/format/date.ts` に置き、★CSV だけが使う★（画面の `formatMd()` は変えない） | ユーザー指示の形。画面の日付列は BC の範囲 |
| D13 | `parseDate()` は ★作った `Date` を JST の `YYYY-MM-DD` に戻して入力と一致しなければ `null`★。★文言は今の形式エラーと同じ★（「YYYY-MM-DD形式で指定してください」） | 呼び出し元（`/clean-logs`・`/clean-logs/export`・`/dashboard`・`/prechecks`・`/errors`・`/clean-area`・`dbDateRange()`・`consecutiveLookbackFrom()`）の分岐を変えずに全部に効く。新しい文言を足すと訳と画面の対応が要る |
| D14 | 週の検証: `parseReportQuery()` で ★`periodRange()` が `null` なら422★（`details.week`）。`continuousProblems()` の `if (!range) return [];`（`lib/db/index.ts:2768`）は ★歯止めとして残す★（コメントだけ直す） | 週の数え方を知っているのは `lib/format/period.ts` だけ（D-071）。ルートで週数を数え直さない |
| D15 | KPI の対象顧客: ★`customer_id` と `group_id` が両方あり、その顧客がそのグループの傘下でなければ ★200・空の KPI（`emptyKpi()`）★。サマリも詳細も読まない★。両方あって傘下なら、その顧客（傘下が2社以上でも422 にしない）。★`customer_id` だけなら今どおり存在確認をしない★（AK 6-9） | YAML の `GroupId`（`d-watch-api.yaml:1756-1760`「AND。そうでなければ0件。404 にしない」）に合わせる（ユーザーの希望）。★傘下の判定には顧客マスタが要る★ので、ルートは ★`customer_id === null` または `group_id !== null` のとき★ `db.customers()` を読む |
| D16 | ★`ConsecutiveDbRange` に `facility_ids: number[] \| null` を足す（必須）★。`null` = 絞らない／`[]` = 0行（罠1）。呼び出し元は自分の `query.facility_ids` を渡す。★単一ID（`/advice`・`/areas`）は `null` のまま★（U4）。★「1リクエストで1回」は今回やらない★（§7-2） | ★連続日数は施設ごとに独立に数える★（`computeConsecutiveProblemDays()` は `facility_id` で分けてから数える。`lib/db/clean-log-mapping.ts:463-490`）ので、施設で絞っても絞った施設の値は変わらない（表10 で固定）。1回にするには `DataSource` をまたぐ仕組みが要り、直近モードでは2本の期間も違う |
| D17 | ★`dsAlerts()` に必須の引数 `{ open_only: boolean }` を足す★。ダッシュボードは `true`（SQL で `status IN (0,1)`・純粋関数でも解消を外す）、`/ds-alerts` は `false` | ★バナーは「解消でないもの」しか見ない★（`computeSecurityBanner()`）。読むのをやめる案は、バナーの絞り込みを固定しているテスト（`app/api/__tests__/master-filter-dashboard-reports.test.ts:174-198`）と U7（コードを残す）の意味を壊す。★引数は必須★（指示書AC C-4 項4: 任意引数は渡し忘れても動いてしまう） |
| D18 | `try` の外の例外は ★500・`internal_error`・「データの取得に失敗しました」・`logApiError()`★ | 文言は辞書にある（`lib/i18n.ts:381`）。他の GET の500と同じ |
| D19 | JSON の本文が ★オブジェクトでない（`null`・配列・数値・文字列）★なら ★422・`validation_failed`・「JSONの形式が不正です」★（4ルートとも今の「JSON が読めない」と同じ文言） | `lib/prechecks/status-update.ts:103` と同じ判定。★本文が数値・文字列のときの文言が今（例「tokenを指定してください」）から変わる★ — 承知のうえ |
| D20 | JSON ストア: ★一時ファイル名を `${filePath}.${process.pid}.${randomUUID()}.tmp` にする★（直列化はしない） | 呼び出しごとに `createJsonStore()` を作り直す（`lib/settings/advice.ts:77` 等）ので、インスタンスごとの直列化は効かない。★同時保存の後勝ち（読み→書きの間の上書き）は今までどおり★（ファイル冒頭の「ロックは入れません」） |
| D21 | `matchAdviceRules(input, thresholds = ESCALATION_THRESHOLDS)` の第2引数を足し、R1・R3・R4 がそれを使う | `level2Days = 3` のテストを `vi.mock` なしで書くため（`isResultPending()` の `waitMinutes` と同じ作法） |
| D22 | `weekStart()`・`weekStartMs()` は ★消す★（テストだけが使う）。`CLEAN_ERROR_LABELS`・`cleanErrorLabel()` は ★消す★（テストだけが使う。`CLEAN_ERROR` はシード `scripts/seed-local-db.ts:35` 以降が使うので残す） | A14 |
| D23 | `"engines": { "node": ">=22.12" }`・★`@types/node` は `^22`★・`"@next/env": "16.3.3"`（devDependency） | ★型は「動く版のうち一番古いもの」に合わせる★（`^24` の型だと 22 に無い API を書いても `tsc` が通ってしまう）。★`>=22.12` はユーザー指定★（Prisma 7 の要件の下限 22.12 と合う）。`@next/env` は `next`（16.3.3）と同じ版に固定（`prisma7.config.ts:3`・`scripts/check-env.ts:21` が直接 import している。今は `next` の依存として入っているだけ） |
| D24 | `DataAnomalyLogEvent`（`lib/logger/events.ts:319-331`）に ★任意の列 `section_id?: string`・`control_id?: string`・`error_id?: string`・`row_ids?: string[]`★ を足す | `BigInt` は JSON にできないので文字列。`FORBIDDEN_PII_FIELDS`（`lib/logger/redact.ts:12-26`）に当たらない名前 |

---

## 3. 共通ルール（本書だけでなく、今後のすべての指示書に当てはまる）

| # | ルール | 本書での当てはめ |
|---|---|---|
| G1 | ★API の挙動が変わったら、`d-watch-api.yaml` の ★影響を受けるパス・スキーマを全部★ 直す★（指示書に挙がっているものだけでなく）。読み・書き・件数（`meta.total`・`summary`）・404 / 422 の条件・値の例と説明を、`grep` で関係する語を探して確かめる。★YAML を先に直す★（`CLAUDE.md` §6-1） | 各段階の「YAML」の項。特に U1（削除済みの規則を各パスの説明に）・U2（`serial_number` / `model_name` nullable）・U3（404 の条件）・A1（`last_reported_at` nullable）・A3（`result_pending`）・A7〜A9（422 の条件）・A17（AND）。★段階7-5 の grep で確かめる★ |
| G2 | ★古い挙動を書いた docstring・コメントを残さない★。挙動を変えた関数だけでなく、★その関数を説明している別の場所（`DataSource` の docstring・呼び出し元のコメント・`docs/code-map.md`）★も直す | 特に `lib/db/index.ts:924-929`（論理削除のログも返している）・罠7 の理由の各所（D3。段階2-9 の表）・`lib/db/clean-log-mapping.ts:435-438`（`<= 1`）・`lib/db/index.ts:1093-1094` `:1626-1627` `:2104-2107`（0台なら例外）・`lib/db/index.ts:1900-1902`（心拍0件は例外）・`lib/dashboard/latest-run.ts:60-62`（E1 は例外） |
| G3 | ★テストは「守る挙動が壊れたら落ちる」ように書く★。空同士の比較や、別の理由でも同じ結果になる比較は検査にならない。★書いたら一度わざと壊して赤になるのを確かめ、戻す★（壊した変更はコミットしない） | 各段階の「壊して確かめる」。★`where` に入れた可視性の条件は、消して赤になるのを確かめる★ |
| G4 | ★決定の記録（`docs/decisions.md` の「実装の記録」）に、コミット・テスト件数の前後・確認の結果（出所つき。`CLAUDE.md` §6-6）・指示書と違う判断を書く★ | 段階7-8 の D-182 の「実装の記録」 |

---

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

★「導出」と書いた行は本書が決定から導いたもの（§10 で監督が確認する）。それ以外はユーザー決定から直接決まる★。

### 表1（U1。削除済みの施設・清掃エリア。画面・API ごと）

「施設削除」= 行の `facility_id` の `facilities.deleted_at` が NULL でない（清掃エリアは生きている）。「エリア削除」= 行の `section_id` の `sections.deleted_at` が NULL でない（施設は生きている）。「生存」= どちらも NULL。

| # | 画面・API | 行の出所 | 施設削除 | エリア削除 | 生存 | 今 | 直す場所 |
|---|---|---|---|---|---|---|---|
| V1 | `GET /clean-logs`・`/clean-logs/export` | 予定（2表）の本清掃 | ★出ない★（`meta.total` にも数えない） | ★出ない★ | 出る | 出る | `mainTimetableWhere()`（`lib/db/index.ts:869-903`） |
| V2 | `GET /clean-logs/{id}/advice`・`/{id}/areas`（`{id}` が V1 で見えない行） | 同上（単一ID） | ★404★ | ★404★ | 200 | 200 | 同上（`cleanLogs({ id })` が0件になる） |
| V3 | `/clean-logs/{id}/areas` の `items`（生きている行から開いた、同じ施設・同じ日の他のエリア） | 予定（2表）の本清掃 | （V2 で404） | ★そのエリアは `items` に出ない・`total_count` / `problem_count` / 平均に入らない★ | 出る | 出る | `areaCompletions()`（`:1330-1346`） |
| V4 | `/dashboard` の `summary`・`problem_logs`（直近モードのメタも） | 予定（2表）の本清掃 | ★数えない★ | ★数えない★ | 数える | 数える | V1 と同じ `mainTimetableWhere()`（`cleanLogRunMeta()` も共有。`:1238-1242`） |
| V5 | `/dashboard` の `schedule_progress.planned`・`/schedules` | `clean_schedules` | ★数えない・出ない★ | ★数えない・出ない★ | 数える | 数える（`clean_schedules.deleted_at` だけ見ている） | `schedules()`（`:2092-2102`） |
| V6 | `/dashboard` の `rescue_required` | 連続日数の入力（V15） | ★出ない★ | ★入力に入れない★ | 入力 | 入力 | `loadConsecutiveInputs()`（`lib/db/consecutive-source.ts:394-405`） |
| V7 | `GET /prechecks` の事前チェック異常の行（`severity: precheck`） | `robot_error_reports`（事前チェック行に紐づく）＋親の本清掃 | ★出ない★（★エラー行の施設・エリアか、親の本清掃の施設・エリアのどちらかが削除済みなら出ない★。`summary` にも数えない） | ★出ない★ | 出る | 出る | `prechecks()`（`:1457-1477` の予定の `where`・`:1540-1590` のループ） |
| V8 | `GET /prechecks` の出発失敗の行（`severity: launch_failed`） | 予定（2表）の本清掃 | ★出ない★ | ★出ない★ | 出る | 出る | 同上（予定の `where`） |
| V9 | `PATCH /prechecks/{id}/status`（V7 で見えない行の `error_id`） | 同上 | ★404★（`updateMany` を呼ばない） | ★404★ | 200 | ★200（書けてしまう）★ | `precheckItemById()`（`:1680-1735`） |
| V10 | `/reports/kpi` の確定期間 | `customer_summaries` | ★外せない（制約）★ | ★外せない（制約）★ | — | — | ★変えない★（D5。YAML と D-182 に書く） |
| V11 | `/reports/kpi` の当期 | 予定（2表）＋実績＋エラー | ★数えない★ | ★数えない★ | 数える | 数える | `reportKpis()`（`:2399-2409`） |
| V12 | `/reports/facility-performance`（＋export）の確定期間 | `facility_summaries` | 出ない（★今も★ `buildItem()`） | ★外せない（制約）★ | 出る | — | ★変えない★ |
| V13 | 同 当期 | 予定（2表）＋実績＋エラー | 出ない（今も）・★集計にも入れない★ | ★集計に入れない★ | 入れる | 入れる | `facilityPerformance()`（`:2671-2681`） |
| V14 | `/reports/error-breakdown` の確定期間 | `facility_error_summaries` | ★数えない（新たに条件を足す）★ | ★外せない（制約）★ | 数える | 数える | `errorBreakdown()`（`:2858-2865`） |
| V15 | 連続日数（詳細ログの `consecutive_problem_days`・L1〜L3・R3/R4・要レスキュー・連続問題施設・施設別パフォーマンスの連続日数） | `loadConsecutiveInputs()`（導出 D4） | 施設は出ない（連続問題施設は今も） | ★入力に入れない★（★生きている施設の連続日数が短くなりうる — 意図した結果★） | 入力 | 入力 | `loadConsecutiveInputs()` |
| V16 | `/reports/error-breakdown` の当期 | 予定（2表）の本清掃を指すエラー | ★数えない★ | ★数えない★ | 数える | 数える | `errorBreakdown()`（`:2895-2906`） |
| V17 | `/clean-area` | `clean_task_reports`（面積・実施回数・完了率・番人） | ★数えない★ | ★数えない★ | 数える | 数える | `cleanArea()`（`:2240-2252`）・`CleanAreaWhere`（`lib/db/clean-area-source.ts:113-116`） |
| V18 | `/errors`・`PATCH /errors/...`・`/ds-alerts`・バナー | `robot_error_reports` | 出ない・404（D-180） | 出ない・404（D-180） | 出る | 同左 | ★変えない★ |
| V19 | 施設名・分類 → 施設ID の解決（`facilityNameIndex()`・`facilityGroupIndex()`・要レスキューの施設名・総清掃面積の施設マスタ） | `facilities` | ★絞らない（罠7。変えない）★ | — | — | 同左 | ★変えない（理由のコメントだけ直す。D3）★ |
| V20 | `/facilities`・`/groups`・`/customers` | マスタ | 出ない（今も） | — | 出る | 同左 | ★変えない★ |

★テストの書き方★:
- V1〜V8・V11・V13〜V17 の SQL の条件は ★Prisma のスタブが受け取った `args.where`（2表とも）に `facility: { deleted_at: null }` と `section: { deleted_at: null }` が入っていること★を固定する（`FACILITY_SECTION_VISIBLE_WHERE` と `toMatchObject`）。V14 は `facility: { deleted_at: null }` だけ。
- V7・V9 の `robot_error_reports` の行は ★スタブが `where` を無視して全行返す形★ にし、`facility.deleted_at` / `section.deleted_at` 入りの行が ★出ない・404★ ことを確かめる（D-180 表5 と同じ書き方。`lib/db/__tests__/error-report-visibility.test.ts` を手本に）。
- ★fixtures には削除の概念が無い★（`lib/fixtures/` は `error-reports.ts` 以外 `deleted_at` を持たない）。★fixtures のデータは足さない★ — fixtures の件数は変わらない（`USE_FIXTURES` で件数が変わらない、は fixtures に削除済みが無いことで保たれる）。

### 表2（U2。有効なロボットがいない清掃エリア）

「有効な」= `robots.deleted_at IS NULL`（今の `where` のまま）。

| # | 入力 | 期待 |
|---|---|---|
| R1 | 清掃エリアに有効なロボット1台（`cleanLogs` / `prechecks` 出発失敗 / `schedules`） | 今どおり |
| R2 | ★0台・`cleanLogs()`★ | ★例外にならない★。その行は出る。`serial_number: null`・`model_name: null`。★`data.anomaly`（`metric: "active_robots_per_section"`・`expected: 1`・`actual: 0`・`section_id`）を清掃エリアごとに1回★（同じ清掃エリアの行が10行あっても1回） |
| R3 | ★0台・`prechecks()` の出発失敗の行★ | 例外にならない。`serial_number: null`。ログは R2 と同じ |
| R4 | ★0台・`schedules()`★ | 例外にならない。`serial_number: null`。ログは R2 と同じ（`clean_schedule` の行ごとではなく清掃エリアごと） |
| R5 | ★有効なロボット2台（id 10 と id 7）★ | ★id 7 を採る★（`orderBy: { id: "asc" }`）。`data.anomaly`（`actual: 2`）を清掃エリアごとに1回 |
| R6 | R2 の行を CSV に出す | 機種・シリアルは ★空のセル★（`'-` にしない。D-164） |
| R7 | R2 の行に `serial=abc` / `model=pudu` で絞る | ★その行は当たらない★（`null` は部分一致しない）。絞り込みなしなら出る |
| R8 | `schedules()` の R4 の行に `/schedules?q=abc` / `serial=abc` | 当たらない（R7 と同じ） |
| R9 | 施設別パフォーマンス（施設のロボット0台） | ★今どおり行ごと除外★（`lib/db/index.ts:2520-2527`。変えない）。2台以上は `id` 昇順の先頭（★ログは出さない★ — 施設に複数台は正常） |

### 表3（U3。`robot_error_reports.status` が 0/1/2 以外）

| # | 入力 | 期待 |
|---|---|---|
| S1 | `/prechecks` の事前チェック行に紐づくエラー・`status` 0/1/2 | 今どおり |
| S2 | ★同・`status` 3★ | ★200・その行は出ない・`summary` にも数えない★。★SQL の `status IN (0,1,2)` と純粋関数の両方で外す★（スタブが `where` を無視しても出ない）。外した件数があれば `data.anomaly`（`metric: "robot_error_status_unknown"`・`expected: "0/1/2"`・`actual: 件数`）を1回 |
| S3 | ★`PATCH /prechecks/{id}/status`・対象行が `status` 3★ | ★404★（`updateMany` を呼ばない） |
| S4 | ★`PATCH /errors/{error_id}/status`・対象行が `status` 3★ | ★404★（★旧: 500。AW §11 Q3 を上書き★）。`updateMany` を呼ばない |
| S5 | `GET /errors` に `status` 3 | 今どおり出ない（表2 F12。変えない） |
| S6 | `/ds-alerts` に `status` 3 | 今どおり出ない（D-180 R10。変えない） |
| S7 | エラー一覧の2段目で、1段目のあとに `status` が 3 に変わっていた行（`errorReportItemsInPageOrder()`） | ★飛ばす（例外にしない）★。`meta.total` は1段目のまま（今の「消えた行」と同じ扱い。`lib/db/index.ts:540-545`） |
| S8 | `isKnownRobotErrorStatus()` の単体: 0 / 1 / 2 → true、3 / 255 / -1 → false | — |

### 表4（A1。`/ds-devices`）

| # | 入力 | 期待 |
|---|---|---|
| D1 | `controls.status` 0・心拍あり | `connection_status: "ok"`・`last_reported_at` あり（今どおり） |
| D2 | 1・心拍あり | `"error"`（今どおり） |
| D3 | ★0・心拍0件★ | ★出る・`last_reported_at: null`★（例外にしない）。`data.anomaly`（`metric: "control_reports_missing"`・`control_id`） |
| D4 | ★2（0/1 以外）★ | ★出ない・`summary` に数えない★。`data.anomaly`（`metric: "controls_status_unknown"`・`actual: 2`・`control_id`）。★`PATCH /ds-devices/{serial}/note` は404★（`dsDeviceExists()` も同じ規則） |
| D5 | D3 と D4 が1台ずつ混ざる | ★200★。他の制御盤は今どおり出る |
| D6 | 並び（`sortDsDevices()`） | 今どおり（異常→正常、同順位はシリアル昇順。`last_reported_at` は並びに使っていない） |

### 表5（A2。直近モードの突き合わせ。`lib/dashboard/latest-run.ts`）

| # | 入力 | 期待 |
|---|---|---|
| E1′ | ★行の id に対応するメタが無い★ | ★例外にしない・その行は選ばない★。`missingRunMetaIds(rows, meta)` がその id を返す。★ルートが `data.anomaly`（`metric: "clean_log_run_meta_missing"`・`expected: 0`・`actual: 件数`・`row_ids`）を1回★。★200★ |
| E1″ | 全行にメタが無い | `[]`・200 |
| E2 | 同じ id のメタが2件 | ★今どおり例外★（`internal_error` の500） |
| E3〜E6 | — | 今どおり（`lib/dashboard/__tests__/latest-run.test.ts:31-36`） |

### 表6（A3。エリア別完了率の結果待ち。導出）

入力: 同じ施設・同じ日の4エリア。A = レポートあり・完了率 95／B = レポートあり・50／C = ★レポートなし・結果待ち★（予定の終了＋120分より前）／D = レポートなし・待ち時間を過ぎた（未実施）。section_id は A1・B2・C3・D4。

| # | 項目 | 期待 |
|---|---|---|
| A1 | `total_count` | 4（今どおり） |
| A2 | ★`problem_count`★ | ★2（B と D）★。★C は数えない★（今は3） |
| A3 | ★`filter=problem` の `items`★ | ★B・D★（C を出さない） |
| A4 | `filter=all` の `items` | 4件。★C は `result_pending: true`・`grade: "×"`・`completion_rate: null`★（`grade` は変えない） |
| A5 | `overall_completion_rate` / `overall_grade` | 72.5 / ○（A と B の平均。C・D は null で分母に入らない＝今どおり） |
| A6 | 並び | 今どおり（完了率の昇順・null が先頭・同値は section_id 昇順）→ C, D, B, A |
| A7 | 境界: 予定の終了＋120分ちょうど | `result_pending: false`（`isResultPending()` の境界。D 扱い） |
| A8 | `result_pending` はデータ層が `isResultPending({ startAt, endAt, now, hasReport })` で決める | ★`now` は `areaCompletions()` の中で1回だけ作り、未来の予定の除外と共有する★ |

### 表7（A4。CSV の日付）

| # | `scheduled_at` | 日付のセル |
|---|---|---|
| C1 | `2026-09-13T02:00:00+09:00` | `2026/09/13` |
| C2 | `2026-01-05T23:30:00+09:00` | `2026/01/05`（ゼロ埋め） |
| C3 | `2025-12-31T23:59:00+09:00` / `2026-01-01T00:00:00+09:00` | `2025/12/31` / `2026/01/01` |

見出し「日付」は変えない。予定時刻の列（`formatHm()`）も変えない。

### 表8（A5・A7〜A10。入力の検証と標準の形）

| # | 入力 | 期待 |
|---|---|---|
| Q1 | `parseDate("2026-02-31")` | ★`null`★ |
| Q2 | `"2026-02-29"`（2026 は閏年でない） | ★`null`★ |
| Q3 | `"2028-02-29"` | 有効 |
| Q4 | `"2026-04-31"` | ★`null`★ |
| Q5 | `"2026-12-31"` / `"2026-02-28"` | 有効 |
| Q6 | `/clean-logs`・`/clean-logs/export`・`/prechecks`・`/errors`・`/clean-area` の `date_from=2026-02-31`／`/dashboard?date=2026-02-31` | ★422★（今の形式エラーと同じ code・message・details） |
| W1 | `period=weekly&year=2026&month=9&week=5`（9月は5週） | 200 |
| W2 | ★`week=6`・2026年9月★ | ★422★。`details.week: ["2026年9月に第6週はありません"]` |
| W3 | `week=6`・2026年8月（6週ある。D-071 の例） | 200 |
| W4 | `period=monthly&week=6` | 200（`week` は今どおり読み捨て） |
| W5 | `/reports/facility-performance/export` の W2 | 422（同じ `parseReportQuery()`） |
| P1 | ★`/prechecks?date_from=2026-10-10&date_to=2026-10-01`★ | ★422★「date_fromはdate_to以前の日付を指定してください」（`/errors` と同じ文言・`details` なし） |
| P2 | `date_from` = `date_to` | 200 |
| H1 | ★`PATCH /settings/advice` の `rule: "constructor"`★ | ★422「ruleの値が不正です」★ |
| H2 | `"toString"` / `"__proto__"` / `"hasOwnProperty"` | 422 |
| H3 | `"R1"` | 今どおり |
| J1 | ★本文が `null`★: `POST /auth/login`・`PATCH /ds-devices/{serial}/note`・`PATCH /settings/advice`・`PATCH /settings/display-limits` | ★422・`validation_failed`・「JSONの形式が不正です」★（今は TypeError で標準でない500） |
| J2 | 本文が `[]` / `1` / `"x"` | 422（J1 と同じ） |
| J3 | 本文が `{}` | 今どおり（各ルートの項目の検証で422） |
| T1 | `GET /settings/advice` で `loadAdviceRules()` が例外 | ★500・`internal_error`・「データの取得に失敗しました」・`api.error` が1件★ |
| T2 | `GET /settings/display-limits` で `loadDisplayLimitSettings()` が例外 | T1 と同じ |
| T3 | `GET /clean-logs/{id}/advice` で `loadAdviceRules()` が例外 | T1 と同じ |

### 表9（A17。`/reports/kpi` の対象顧客。D15）

顧客マスタ: 顧客1（group 1）・顧客2（group 2）・顧客3（group 1）・顧客4（group なし）。

| # | `customer_id` | `group_id` | 期待 |
|---|---|---|---|
| K1 | 2 | なし | 顧客2（今どおり。存在確認しない） |
| K2 | 2 | 2 | 顧客2 |
| K3 | ★2★ | ★1★ | ★200・空のKPI（4指標とも `value/prev/diff` が null）。サマリも詳細も読まない★ |
| K4 | 4 | 1 | 200・空（顧客4はグループなし） |
| K5 | 999（無い） | 1 | 200・空（傘下に無い） |
| K6 | 999 | なし | 今どおり 200・空（AK 6-9。サマリを読んで0行） |
| K7 | 1 | 1（傘下2社） | ★顧客1★（422 にしない。顧客が名指しされている） |
| K8 | なし | 1（傘下2社） | 今どおり422（`details.customer_ids: [1, 3]`） |

### 表10（A6。読みすぎの手当てで判定結果が変わらないこと。D16・D17）

| # | 入力 | 期待 |
|---|---|---|
| N1 | `loadConsecutiveInputs(…, { …, facility_ids: null })` | 予定の `where` に `facility_id` の条件が ★無い★（今と同じ SQL） |
| N2 | `facility_ids: [1, 3]` | `where` に `facility_id: { in: [1n, 3n] }`（2表とも） |
| N3 | `facility_ids: []` | `where` に `facility_id: { in: [] }`（★`null` と取り違えない。罠1★） |
| N4 | ★不変性（純粋関数のテスト）★: 3施設（施設1 = 3日連続の L3、施設2 = 正常、施設3 = 予定の無い日を挟む2日連続）の入力 `rows` | ★`computeConsecutiveProblemDays(rows.filter(施設∈{1,3}))` の各 id の値が、`computeConsecutiveProblemDays(rows)` の同じ id の値と一致★ |
| N5 | `/clean-logs?customer_id=1`（施設 1〜3 に解決） | `cleanLogs()` が `loadConsecutiveInputs()` に `facility_ids: [1,2,3]` を渡す。★応答の `consecutive_problem_days`・`escalation_level` は手当ての前と同じ★ |
| N6 | 単一ID（`/clean-logs/{id}/advice`・`/areas`） | ★`facility_ids: null` のまま★（U4） |
| N7 | `/dashboard` | `db.dsAlerts({ open_only: true })`。SQL に `status: { in: [0, 1] }`。★バナーは今どおり★（`master-filter-dashboard-reports.test.ts:174-198` が通る） |
| N8 | `/ds-alerts` | `db.dsAlerts({ open_only: false })`。`summary.resolved` は今どおり数える |
| N9 | `dsAlertsFromErrorRows(rows, { open_only: true })` にスタブが解消の行を混ぜて返す | ★解消の行は出ない★（SQL の写しが外れても純粋関数で外れる） |

### 表11（A13。R1 の閾値。D21）

| # | 閾値 | `departed` | 連続日数 | R1 | R3 |
|---|---|---|---|---|---|
| T1 | 既定（level2Days 2） | false | 0 | 出る | 出ない |
| T2 | 既定 | false | 1 | 出る | 出ない |
| T3 | 既定 | false | 2 | ★出ない★ | 出る |
| T4 | ★`{ level2Days: 3, level3Days: 4 }`★ | false | 2 | ★出る★ | ★出ない★ |
| T5 | 同上 | false | 3 | 出ない | 出る |
| T6 | 同上 | true | 1 | 出ない（出発している） | 出ない |

### 表12（A12。JSON ストア）

| # | 入力 | 期待 |
|---|---|---|
| J1 | 同じストアに5回の `write()` を `Promise.all` で同時に | ★5回とも解決する★。最後のファイルは ★5つの値のどれか1つの JSON として読める★。★`*.tmp` が残らない★ |
| J2 | 一時ファイル名 | `<filePath>.<pid>.<uuid>.tmp` の形（2回の `write()` で別の名前） |
| J3 | 既存のテスト（`lib/settings/__tests__/json-store.test.ts:241-245` 等） | 今どおり通る |

### 表13（A11。確定期間のサマリが0行）

| # | 入力 | 期待 |
|---|---|---|
| L1 | 確定期間（当期でない）・`customer_summaries` が0行 | KPI は今どおり空（null）。★`data.anomaly`（`metric: "customer_summary_rows"`・`expected: 1`・`actual: 0`・`period_mode: "summary_table"`）を1回★ |
| L2 | 当期・サマリ0行 | ★ログを出さない★（当期にサマリが無いのは正常） |
| L3 | 確定期間・1行 | ログを出さない |

---

## 5. 落とし穴

| # | 罠 | どう避けるか |
|---|---|---|
| 5-1 | ★`git add -A`・`git add .`★ | 禁止。ファイルパスを1つずつ書く。各コミットの前に `git diff --cached --name-status` で中身を目で確かめる |
| 5-2 | ★他のセッションの変更を巻き込む・上書きする★ | ★着手前と各コミットの前に `git status --short` を見る★。§9 に無いファイルの変更は他のセッションのもの。触らない・含めない。★`docs/decisions.md` は段階0-2・段階6-1 のとおり★（同じ日に上書き事故が起きている） |
| 5-3 | ★指示書AY（D-179。制御盤のログ）の実装と同じファイルを触る★（`d-watch-api.yaml`・`lib/types/index.ts`・`lib/logger/events.ts`・`components/features/ds-logs/DsLogScreen.tsx`） | ★AY の実装が同じ作業ツリーで進んでいたら、本書の段階1（`/ds-devices`）と段階6 を AY のコミットの後にする★。行番号は引き直す |
| 5-4 | ★可視性の条件を `select` しないまま純粋関数に渡す★ | `robot_error_reports` の select 定数（`ROBOT_ERROR_PRECHECK_ROW_SELECT`）に `facility.deleted_at` と `section.deleted_at` を足す。`lib/db/__tests__/robot-error-select.test.ts` に「この定数も含む」を足す |
| 5-5 | ★`where` を2回書く★ | 予定は `findTimetables()` に1回だけ（2表に同じ `args`）。総清掃面積は集計と番人で ★同じ `where` オブジェクト★（`lib/db/index.ts:2238-2240` の注意書き）。`cleanLogs()` と `cleanLogRunMeta()` は `mainTimetableWhere()` の1本 |
| 5-6 | ★`facility_ids` の `null` と `[]` を取り違える★（罠1） | `!== null` で分岐する。`?.length` で書かない |
| 5-7 | ★`robots[0]` の0台の分岐で空文字を返す★ | ★`null` を返す★（D6）。空文字は画面で空白になり、絞り込みの部分一致では「全部に当たる」と「どれにも当たらない」が実装しだいで割れる |
| 5-8 | ★`resolutionStatusFromRobotErrorStatus()` の例外を消す★ | ★消さない★（`lib/db/precheck-mapping.ts:38-51`）。外すのは呼ぶ前（`isKnownRobotErrorStatus()`）。テスト（`precheck-mapping.test.ts:221-227`）は今どおり |
| 5-9 | ★`parseDate()` の往復で実行環境のタイムゾーンを使う★ | `toJstIso()`（`lib/format/date.ts:37`）で戻す。`getFullYear()` 等を使わない（CI で1日ずれる） |
| 5-10 | ★週の数を自分で数える★ | `periodRange()` の `null` で判定する（D-071 の帰属は `weekOfMonth()` だけが知っている） |
| 5-11 | ★U4 の単一ID の連続日数を、ついでに絞る★ | ★絞らない★（U4。`facility_ids: null` のまま。索引の後） |
| 5-12 | ★`loadConsecutiveInputs()` の `select` に列を足す★ | ★足さない★（可視性は `where` のリレーション条件だけ。D2） |
| 5-13 | ★名前の解決に `deleted_at` の条件を足す★ | ★足さない★（罠7。V19） |
| 5-14 | ★`@types/node` を変えた結果の型エラーを、型を緩めて黙らせる★ | 本書と関係ないエラーが出たら止まって報告 |
| 5-15 | ★`npm install` で `package-lock.json` 以外が変わる★ | `git status` で確かめる。`package.json` と `package-lock.json` だけをコミット |
| 5-16 | ★ステージングに書き込む・シードを向ける★ | 段階7-6 は ★SELECT だけ★（参照用の接続）。`scripts/seed-local-db.ts` はステージングに向けない（`lib/db/local-guard.ts`） |
| 5-17 | ★認証情報を文書・コード・コミット・報告に書く★ | 書かない（接続文字列・パスワード・トークン） |
| 5-18 | ★`docs/handoff/指示書BC_*` に書く★ | 書かない（別のエージェントの成果物）。境目は §8 |
| 5-19 | ★push する★ | 監督のレビューが終わるまで push しない |

---

## 段階0 — 着手前

1. ★develop の本体で作業する★（`.claude/worktrees/` の中ではない）。`git log --oneline -1` が `2fc3bbb`（またはその後の docs だけのコミット）であること。進んでいたら、本書の行番号を関数名で引き直しながら進め、ずれを作業報告に書く。
2. ★`git status --short` を控える★。2026-10-19 の本書作成時点で次があった — ★どれも他のセッションのもの。触らない・含めない★:
   - `docs/decisions.md`（変更。D-179 の「追記（2026-10-19 同日・4回目）」の9行）
   - `docs/handoff/指示書AY_制御盤のログをD-Secログ画面から取り出す_20261019.md`（変更）
   - 本書（未追跡。★本書はコミットに含めない★ — 監督がコミットする）。`docs/handoff/指示書BC_*`（未追跡。あれば別のエージェントのもの）
   ★`docs/decisions.md` に自分のものでない差分があるあいだは、段階6-1 の追記をしない★（段階1〜5 は進めてよい。`docs/decisions.md` を触らないので）。
3. `npx tsc --noEmit -p .` → `npm run lint` → `npm test`。★テストのファイル数と件数を控える★（期待: 130ファイル / 3,083件）。
4. 指示書AY の実装が始まっていないか確かめる（`git log --oneline -- lib/ds-logs lib/ds-agent` が空・`lib/ds-logs/` が無い）。始まっていたら 5-3。
5. 指示書BC の実装が始まっていないか確かめる（`git log --oneline -- components/features/ds-logs components/features/detail-log` に 2fc3bbb より新しいコミットが無いか）。★始まっていたら同じ作業ツリーで同時に進めない★（§8）。

---

## 段階1 — 止まる経路を塞ぐ（U2・U3・A1・A2）

### 1-1. YAML を先に（G1）

| # | 場所 | 直す |
|---|---|---|
| 1-1a | `d-watch-api.yaml:1940-1941`（`CleanLogItem` の `model_name` / `serial_number`） | `nullable: true` を足し、説明「★清掃エリアに有効なロボットがいないときは null（画面は「-」・CSV は空のセル）。U2・D-182★」 |
| 1-1b | `:2115`（`PrecheckItem.serial_number`） | `nullable: true`。説明「出発失敗の行で清掃エリアに有効なロボットがいないときは null（D-182）。事前チェック異常の行は `robot_error_reports.serial_number` の生値（null にならない）」 |
| 1-1c | `:2743`（`Schedule.serial_number`） | `nullable: true`。説明は 1-1a と同じ趣旨 |
| 1-1d | `:2607`（`DsDevice.last_reported_at`） | `nullable: true`。説明「最終通信日時。★まだ一度も通信が無い制御盤は null★（D-182）」 |
| 1-1e | `/ds-devices`（`:1381` 以降の `description`） | 「★`controls.status` が 0/1 以外の制御盤は出さない（`summary` にも数えない。例外にしない）★」を足す |
| 1-1f | `/ds-devices/{serial_number}/note`（`:1418` 以降）の404の説明 | 「一覧に出ない制御盤（論理削除・状態が 0/1 以外）は404」 |
| 1-1g | `/prechecks/{id}/status`（`:704` 以降）の404の説明 | 「★対象の `robot_error_reports.status` が 0/1/2 以外なら404★（D-182）」 |
| 1-1h | `/errors/{error_id}/status`（`:831` 以降）の404 / 500 の説明 | ★「0/1/2 以外は500」の記述があれば「404」に（`grep -n "0/1/2" d-watch-api.yaml` で探す）★ |
| 1-1i | `/prechecks`（`:669-702`）の `description` | 「★`robot_error_reports.status` が 0/1/2 以外の行は出さない（`summary` にも数えない。例外にもしない）★」 |

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

| # | 場所 | 直す |
|---|---|---|
| 1-2a | `:228-229`（`CleanLogItem.model_name` / `serial_number`） | `string \| null`。docstring に U2 |
| 1-2b | `:547`（`PrecheckItem.serial_number`） | `string \| null` |
| 1-2c | `:1154`（`Schedule.serial_number`） | `string \| null` |
| 1-2d | `:1029`（`DsDevice.last_reported_at`） | `string \| null` |

→ `tsc` が指す場所を直す（★想定★: `lib/clean-logs/query.ts:173` `:176`・`lib/schedules/query.ts:123` `:157`・`lib/clean-logs/csv.ts:71-72`・`lib/fixtures/areas.ts:198`・`lib/fixtures/clean-task-reports.ts:326`・`components/features/ds-logs/DsLogScreen.tsx:465`）:
- 部分一致（`includesFoldPlain(x, q)`）: ★`x !== null && includesFoldPlain(x, q)`★（R7・R8）。`includesFoldPlain()` 自体は変えない
- CSV: `r.model_name ?? ""`・`r.serial_number ?? ""`（R6）
- fixtures（`areas.ts:198`・`clean-task-reports.ts:326`）: fixtures の清掃ログは必ずシリアルを持つので ★`null` なら `throw new Error("[fixtures] …")`★（不変条件）。データは変えない
- ★`DsLogScreen.tsx:465` だけは画面のファイルだが、`tsc` を通すために本書で直す★: `{item.last_reported_at ? formatDateTime(item.last_reported_at) : "-"}`。★この1行だけ★（見た目の調整は BC。§8）

### 1-3. U2 — `lib/db/index.ts`

| # | 場所（今） | 今 | 直す |
|---|---|---|---|
| 1-3a | `:966-969`（`cleanLogs()` の `robots` の include） | `where: { deleted_at: null }, include: { device: true }` | `orderBy: { id: "asc" }` を足す（D7） |
| 1-3b | `:1093-1100`（0台なら例外） | `throw new Error(… ロボットが見つかりません …)` | ★例外をやめる★。`const robot = row.section.robots[0] ?? null;`。0台・2台以上は ★清掃エリアごとに1回★ `data.anomaly`（R2・R5。`section_id: String(row.section_id)`・`clean_timetable_id` は載せない — 清掃エリアの話なので）。重複を防ぐ `Set<string>` をこの関数の中に1つ |
| 1-3c | `:1149-1150` | `model_name: formatModelName(robot.device.maker_id, robot.device.model_name)`・`serial_number: robot.serial_number` | `robot === null ? null : …`（2列とも） |
| 1-3d | `:1475`（`prechecks()` の `robots`） | `robots: { where: { deleted_at: null } }` | `orderBy: { id: "asc" }` を足す |
| 1-3e | `:1626-1633`（出発失敗の行の0台の例外） | `throw` | 1-3b と同じ（例外をやめ、清掃エリアごとに1回ログ）。`serial_number: robot?.serial_number ?? null`（`:1644`） |
| 1-3f | `:2098`（`schedules()` の `robots`） | `robots: { where: { deleted_at: null } }` | `orderBy: { id: "asc" }` |
| 1-3g | `:2104-2113`（`schedules()` の0台の例外） | `throw` | 1-3b と同じ。`serial_number: robot?.serial_number ?? null`（`:2116`） |
| 1-3h | `:2514`（`facilityPerformance()` の `robots`） | `include: { robots: { where: { deleted_at: null } } }` | `orderBy: { id: "asc" }` を足す（R9。★除外の挙動とログ無しは変えない★） |

- ★ログの出し方は1本にする★: このファイルに小さな関数 `reportSectionRobotCount(sectionId: bigint, count: number, seen: Set<string>)` を置き、`count !== 1` かつ未報告のときだけ `logEvent("warn", { event: "data.anomaly", metric: "active_robots_per_section", expected: 1, actual: count, section_id })`。★3か所（1-3b・1-3e・1-3g）がこれを呼ぶ★
- `lib/logger/events.ts:319-331` の `DataAnomalyLogEvent` に D24 の4列を足す（各列に1行の説明）
- ★コメントを直す（G2）★: `:1093-1094`・`:1626-1627`・`:2104-2107` の「0台なら例外」→「★0台でも例外にしない（U2。D-182）。シリアル・機種は null、ログを1回★」。`:1308-1310`（`areaCompletions()` の「ロボット0台なら例外の処理は入れない」）は「`cleanLogs()` も例外にしなくなった（D-182）」に。`:1085`・`:1455` の「`section.robots[0]` が同じなので例外にもならない」は意味が変わらないので残す

### 1-4. U3 — `status` が 0/1/2 以外

1. `lib/db/precheck-mapping.ts` の `robotErrorStatusFromResolutionStatus()`（`:58-67`）の後に足す:

```ts
/** `robot_error_reports.status` の既知のコード（0/1/2）。★`PrecheckStatus` の3つから作る（直書きしない）★ */
const KNOWN_ROBOT_ERROR_STATUSES: readonly PrecheckStatus[] = ["unresolved", "in_progress", "resolved"];

/**
 * ★`robot_error_reports.status` が既知（0/1/2）か★（指示書BB U3・D-182）。
 * ★一覧は外す・1件読みは404★ — 1行の想定外の値で画面全体を500にしない（エラー一覧の表2 F12・D-Sec の D3 と同じ考え方）。
 * `resolutionStatusFromRobotErrorStatus()` は呼ぶ前にこれで外すこと（あちらの例外は残す）。
 */
export function isKnownRobotErrorStatus(status: number): boolean {
  return KNOWN_ROBOT_ERROR_STATUSES.map(robotErrorStatusFromResolutionStatus).includes(status);
}
```

2. `lib/db/ds-alert-mapping.ts:42-47`（`DS_ALERT_STATUSES`・`dsAlertStatusCodes()`）: ★`isKnownRobotErrorStatus()` に置き換えてよい★（同じ値。D8）。SQL の `status: { in: … }`（`:100`）には同じ値の配列が要るので、`precheck-mapping.ts` に `knownRobotErrorStatusCodes(): number[]` も置き、`isKnownRobotErrorStatus()` はそれを使う形にしてよい。★置き換えたら作業報告に書く★（置き換えないなら残す — どちらでも値は同じ）
3. `lib/db/index.ts`:

| # | 場所（今） | 直す |
|---|---|---|
| 1-4a | `:1507-1515`（`prechecks()` のエラー行の `findMany`） | `where` に `status: { in: knownRobotErrorStatusCodes() }` を足す（★写し★） |
| 1-4b | `:1540-1590`（事前チェック行のループ） | 先頭近く（`clean_timetable_id === null` の後）に `if (!isKnownRobotErrorStatus(report.status)) { unknownCount++; continue; }`（★正★）。ループの後で `unknownCount > 0` なら `data.anomaly` を1回（S2） |
| 1-4c | `:1694`（`precheckItemById()` の `report.error_id !== id` の後） | `if (!isKnownRobotErrorStatus(report.status)) return null;`（S3） |
| 1-4d | `:1809`（Prisma 版 `errorReportById()` の `report.error_id !== id` の後） | 同上（S4） |
| 1-4e | `:667-676`（fixtures 版 `errorReportById()`） | `if (!row \|\| !isVisibleErrorRow(row)) return null;` の条件に `\|\| !isKnownRobotErrorStatus(row.status)` を足す（★上書き表を反映した後の値で見る★: `withFixtureErrorReportOverride(row)` を先に通してから判定） |
| 1-4f | `:556`（`errorReportItemsInPageOrder()`） | `if (!row \|\| !isVisibleErrorRow(row) \|\| !isKnownRobotErrorStatus(row.status)) continue;`（S7）。docstring（`:535-545`）に1行「`status` が既知でなくなった行も飛ばす」 |

- ★docstring を直す（G2）★: `DataSource.precheckItemById()`（`:309-321`）の条件の列挙に「★`status` が 0/1/2 以外なら null（404）★」を足す。`DataSource.errorReportById()`（`:339-343`）も同じ。`lib/db/error-report-mapping.ts:156-157` の「PATCH の1件読みで来たら500（fail-loud。§11 Q3）」→「1件読みは呼ぶ前に `isKnownRobotErrorStatus()` で外して404（指示書BB U3。AW §11 Q3 を上書き）」
- fixtures の `/prechecks`（`filterFixturePrechecks()`）は `PrecheckItem` を持つだけで生の `status` を持たない — ★変えない★

### 1-5. A1 — `/ds-devices`

1. `lib/db/ds-status.ts` の `connectionStatusFromControlStatus()`（`:24-30`）の前に足す:

```ts
/** ★一覧に出す制御盤の `controls.status` か（0/1）★（指示書BB A1・D-182）。0/1 以外は一覧に出さない（例外にしない） */
export function isListedControlStatus(status: number): boolean {
  return status === 0 || status === 1;
}
```
   ★`connectionStatusFromControlStatus()` の例外は残す★（呼ぶ前に外す。5-8 と同じ考え方）。冒頭の docstring に1行。
2. `lib/db/index.ts` の `dsDevices()`（`:1875-1923`）:
   - `controls.map` を ★`flatMap`★ にし、`!isListedControlStatus(row.status)` なら `data.anomaly`（`metric: "controls_status_unknown"`・`expected: "0/1"`・`actual: row.status`・`control_id: String(row.id)`）を出して `[]`（D4）
   - 心拍なし（`:1899-1906`）: ★例外をやめ★、`data.anomaly`（`metric: "control_reports_missing"`・`expected: 1`・`actual: 0`・`control_id`）を出して `last_reported_at: null`（D3）
   - ★コメント `:1900-1902`（「例外で気づけるほうを選ぶ。メッセージも含めて変えないこと」）は書き直す（G2）★: 「★心拍0件は新しい制御盤で正当に起きる（D-182）。null で出してログ★」
3. `dsDeviceExists()`（`:1957-1966`）: `select` に `status: true` を足し、`rows.some((row) => row.serial_number === serialNumber && isListedControlStatus(row.status))`。docstring（`:1924-1956`・インターフェース側 `:350-369`）に「★一覧と同じく状態が 0/1 以外の制御盤は false（D-182）★」
4. fixtures（`lib/fixtures/ds-devices.ts`）は変えない（全台 0/1・心拍あり）

### 1-6. A2 — 直近モードの突き合わせ（`lib/dashboard/latest-run.ts`）

1. `pickLatestRunPerSection()`（`:67-111`）の `m === undefined` の分岐（`:89-93`）: `throw` をやめ `continue`（その行は選ばない）
2. 同じファイルに足す:

```ts
/**
 * ★メタの無い行の id★（指示書BB A2・D-182）— `cleanLogs()` と `cleanLogRunMeta()` は並列に読むので、間に行が増えると起きうる。
 * `pickLatestRunPerSection()` はその行を選ばない（例外にしない）。★ログはルートが出す★（純粋関数のまま）
 */
export function missingRunMetaIds(rows: readonly CleanLogItem[], meta: readonly CleanLogRunMeta[]): number[] {
  const ids = new Set(meta.map((m) => m.id));
  return rows.filter((row) => !ids.has(row.id)).map((row) => row.id);
}
```

3. docstring（`:60-62` の不変条件 D9「無い（E1）… 例外」）を「★無い（E1）は選ばない（`missingRunMetaIds()` で数え、ルートがログ）。2件以上（E2）は例外★」に（G2）
4. `app/api/dashboard/route.ts:165-168`: `meta !== null` のとき `missingRunMetaIds(logs, meta)` が空でなければ `logEvent("warn", { event: "data.anomaly", route: ROUTE, method: "GET", metric: "clean_log_run_meta_missing", expected: 0, actual: ids.length, row_ids: ids.map(String) })`。★`try` の中のまま★
5. コメント `:111`（「メタだけ漏れると … 例外になる」）→「… その行が選ばれず `data.anomaly` が出る」

### 1-7. テスト（★期待値表を先に★）

| # | ファイル | 足す・直す |
|---|---|---|
| 1-7a | `lib/db/__tests__/precheck-mapping.test.ts` | 表3 S8（`isKnownRobotErrorStatus`） |
| 1-7b | `app/api/prechecks/__tests__/route.test.ts` | 表3 S2（`status: 3` の行を `where` を無視するスタブに混ぜ、200・出ない・`summary` に数えない）・表2 R3（0台の清掃エリアの出発失敗 → 200・`serial_number: null`・`data.anomaly` が1回） |
| 1-7c | `app/api/prechecks/[id]/status/__tests__/route.test.ts` | 表3 S3（404・`updateMany` 0回） |
| 1-7d | `app/api/errors/[error_id]/status/__tests__/route.test.ts:434-437` | ★「status が 0/1/2 以外の行 → 500（fail-loud。§11 Q3）」を「→ 404（指示書BB U3）」に書き換える★。`updateMany` 0回も確かめる |
| 1-7e | `lib/db/__tests__/error-report-mapping.test.ts` か `fixture-error-reports.test.ts` | 表3 S7（`errorReportItemsInPageOrder()` は非公開なので、fixtures 版 `errorReportById()` と、Prisma 版の2段目のスタブで確かめる） |
| 1-7f | `app/api/clean-logs/__tests__/route.test.ts` | 表2 R2・R5（0台・2台。応答の値・`orderBy` が `{ id: "asc" }`・ログが清掃エリアごとに1回）・R7（`serial=` / `model=` の絞り込み） |
| 1-7g | `app/api/clean-logs/export/__tests__/route.test.ts` か `lib/clean-logs/__tests__/csv.test.ts` | 表2 R6（空のセル） |
| 1-7h | スケジュールのルートのテスト（無ければ `lib/schedules/__tests__/query.test.ts` に R8・`schedules()` の R4 は `lib/db/__tests__/` に新しいファイル `schedules-source.test.ts`） | 表2 R4・R8 |
| 1-7i | `lib/db/__tests__/ds-devices-source.test.ts:237-243`（#3 心拍0件 → 例外） | ★表4 D3 に書き換える★（null・ログ）。D4・D5 を足す。`dsDeviceExists()` の D4 を足す |
| 1-7j | `app/api/ds-devices/[serial_number]/note/__tests__/route.test.ts` | 表4 D4（状態 2 の制御盤 → 404） |
| 1-7k | `lib/db/__tests__/ds-status.test.ts` | `isListedControlStatus` の 0 / 1 → true、2 / -1 / 255 → false |
| 1-7l | `lib/dashboard/__tests__/latest-run.test.ts:296-299`（E1 → 例外） | ★表5 E1′ に書き換える★（例外にならず選ばれない・`missingRunMetaIds` がその id を返す）。E1″ を足す。表2 の docstring（`:31`）も直す |
| 1-7m | `app/api/dashboard/__tests__/route.test.ts` | 表5 E1′（`db.cleanLogRunMeta` のスタブが1行ぶんのメタを返さない → 200・`data.anomaly` が1回・`row_ids`） |

### 1-8. 壊して確かめる（G3）— 壊した変更はコミットしない

1. 1-4b の `if (!isKnownRobotErrorStatus(…)) continue;` を消す → ★S2 が赤（500）★ → 戻す
2. 1-3b の `?? null` を元の `throw` に戻す → ★R2 が赤★ → 戻す
3. `missingRunMetaIds()` の中身を `return [];` にする → ★1-7m が赤★ → 戻す

### 1-9. 確かめてコミット

- `npx tsc --noEmit -p .`・`npm run lint`・`npm test`（件数を控える）
- コミット（★`git add` はファイルを1つずつ★。`git diff --cached --name-status` で確かめる）:
  `d-watch-api.yaml`・`lib/types/index.ts`・`lib/logger/events.ts`・`lib/db/index.ts`・`lib/db/precheck-mapping.ts`・`lib/db/ds-alert-mapping.ts`（置き換えたなら）・`lib/db/ds-status.ts`・`lib/db/error-report-mapping.ts`・`lib/dashboard/latest-run.ts`・`app/api/dashboard/route.ts`・`lib/clean-logs/query.ts`・`lib/clean-logs/csv.ts`・`lib/schedules/query.ts`・`lib/fixtures/areas.ts`・`lib/fixtures/clean-task-reports.ts`・`components/features/ds-logs/DsLogScreen.tsx`・テスト（1-7 で触ったもの）
  - メッセージの例: `fix: ロボットのいない清掃エリア・想定外の対応状況・心拍のない制御盤・直近モードの突き合わせの食い違いで500にせず、null・除外・ログにする(指示書BB 段階1・U2・U3・D-182)`
  - ★2つに分けてもよい★（`/ds-devices` を別のコミットに。5-3 で AY を待つ場合）

---

## 段階2 — 削除済みの施設・清掃エリアを全画面で外す（U1）

### 2-1. 規則を1か所に — `lib/db/visibility.ts`（新規）

```ts
/**
 * ★削除済み（論理削除）の施設・清掃エリアの行は、全画面で出さない★（ユーザー決定 2026-10-19。指示書BB U1・D-182。D-180 U8 を全画面に広げた）。
 * ★`deleted_at` が NULL でなければ削除済み（日時の値によらない）★。
 *
 * - ★正★: `isVisibleFacilitySection()`（列を運ぶ読み取り — `robot_error_reports` — が使う。D-180 と同じ）
 * - ★写し★: `FACILITY_SECTION_VISIBLE_WHERE`（Prisma のリレーション条件。`clean_timetables` / `clean_timetable_histories` /
 *   `clean_schedules` / `clean_task_reports` / `robot_error_reports` の `where` に混ぜる）。
 *   ★列を運ばない読み取りは、この写しだけで外す★（`IS NULL` は SQL と JS で意味が同じ。fixtures に削除の概念が無い。指示書BB D2）
 * ★施設名・分類 → 施設ID の解決は `deleted_at` で絞らない（罠7）★ — 解決は行の可視性と別の責務。
 *   削除済みの施設名で検索すると施設ID に解決され、その行はこの条件で外れて0件になる（指示書BB D3）
 */
export interface FacilitySectionVisibilitySource {
  facility: { deleted_at: Date | null };
  section: { deleted_at: Date | null };
}

export function isVisibleFacilitySection(row: FacilitySectionVisibilitySource): boolean {
  return row.facility.deleted_at === null && row.section.deleted_at === null;
}

export const FACILITY_SECTION_VISIBLE_WHERE = {
  facility: { deleted_at: null },
  section: { deleted_at: null },
} as const;
```

- `lib/db/error-report-mapping.ts:30-51`: `ErrorRowVisibilitySource` は `FacilitySectionVisibilitySource` の別名（`export type ErrorRowVisibilitySource = FacilitySectionVisibilitySource;`）、`isVisibleErrorRow()` は `return isVisibleFacilitySection(row);`、`ERROR_ROW_VISIBLE_WHERE` は `FACILITY_SECTION_VISIBLE_WHERE satisfies Prisma.RobotErrorReportWhereInput`。docstring に「規則は `lib/db/visibility.ts`（指示書BB D1）」
- ★各モデルの `where` に混ぜるところで `satisfies`（または型注釈のある変数）を通し、`tsc` に確かめさせる★。`as const` の読み取り専用で型が合わなければ、`visibility.ts` に `satisfies` 付きの定数をモデルごとに置いてよい（★中身は同じオブジェクトを指すこと★）

### 2-2. YAML（G1。説明だけ・スキーマは変えない）

各パスの `description` に1行「★削除済みの施設・清掃エリアの行は出さない（件数・`summary` にも数えない。D-182）★」を足す:
`/clean-logs`（`:404`）・`/clean-logs/export`（`:464`）・`/clean-logs/{id}/advice`（`:535`）と `/{id}/areas`（`:591`）（「★見えない行の `{id}` は404★」も）・`/dashboard`（`:216`）・`/prechecks`（`:669`）・`/prechecks/{id}/status`（`:704`。「★見えない行は404★」）・`/schedules`（`:1557`）・`/clean-area`（`:1161`）・`/reports/kpi`（`:895`）・`/reports/facility-performance`（`:936`）と export（`:994`）・`/reports/continuous-problems`（`:1085`）・`/reports/error-breakdown`（`:1125`）。
★確定期間の制約を書く★（D5）: `/reports/kpi` に「★確定期間（`customer_summaries`）は TeamWorks の集計値のため、削除済みの施設・清掃エリアの実績も含みうる★」。`/reports/facility-performance` と `/reports/error-breakdown` に「★確定期間は削除済みの施設の行は出さないが、削除済みの清掃エリアの実績は施設の値に含みうる★」。

### 2-3. `lib/db/index.ts` — 予定（2表）の `where`

| # | 場所（今） | 直す |
|---|---|---|
| 2-3a | `mainTimetableWhere()` `:879-902` の `return { … }` | `...FACILITY_SECTION_VISIBLE_WHERE,` を足す（V1〜V4）。docstring `:862-868` の「中身は `cleanLogs()` にあった `where` をそのまま移したもの（1文字も変えていない）」の後に「★2026-10-19 に削除済みの施設・清掃エリアの条件を足した（指示書BB U1）★」 |
| 2-3b | `areaCompletions()` `:1331-1343` | `...FACILITY_SECTION_VISIBLE_WHERE,` を足す（V3） |
| 2-3c | `prechecks()` `:1458-1471` | 同上（V7・V8） |
| 2-3d | `reportKpis()` `:2400-2405` | 同上（V11） |
| 2-3e | `facilityPerformance()` `:2672-2678` | 同上（V13） |
| 2-3f | `errorBreakdown()` `:2896-2903` | 同上（V16） |
| 2-3g | `lib/db/consecutive-source.ts:394-405`（`loadConsecutiveInputs()` の `findTimetables`） | 同上（V6・V15。★`select` は変えない★ — 5-12） |

### 2-4. `lib/db/index.ts` — その他の読み取り

| # | 場所（今） | 直す |
|---|---|---|
| 2-4a | `schedules()` `:2092-2093` | `where: { deleted_at: null, ...FACILITY_SECTION_VISIBLE_WHERE }`（V5。`clean_schedules.deleted_at` の条件は残す） |
| 2-4b | `errorBreakdown()` `:2858-2865`（`facilityErrorSummary.findMany`） | `facility: { deleted_at: null },` を足す（V14。★清掃エリアは外せない★ — D5） |
| 2-4c | `cleanArea()` `:2240-2252`（`CleanAreaWhere`） | `...FACILITY_SECTION_VISIBLE_WHERE,` を足す（V17）。★集計と番人で同じオブジェクトのまま★（5-5） |
| 2-4d | `lib/db/clean-area-source.ts:113-116`（`CleanAreaWhere`） | `facility?: { deleted_at: null }; section?: { deleted_at: null };` を足す。`CleanAreaStatsSourcePrisma` 等の手書きの型（`:130-180`）が受けられるか `tsc` で確かめる |

### 2-5. `prechecks()` の事前チェック異常の行と `precheckItemById()`

1. `lib/db/robot-error-select.ts` の `ROBOT_ERROR_PRECHECK_ROW_SELECT`（`:51-64`）: `facility: { select: { facility_name: true, deleted_at: true } }`・`section: { select: { deleted_at: true } }` にする（5-4）
2. `prechecks()` のループ（`:1540-1590`）: `clean_timetable_id === null` の判定の後に `if (!isVisibleErrorRow(report)) continue;`（V7。★親の本清掃の可視性は 2-3c の `where` で効いている★ — 親が見えなければ `precheckRows` に入らず `row` が無いので飛ぶ）
3. `precheckItemById()`（`:1680-1735`）:
   - `report.error_id !== id` の後（1-4c の後）に `if (!isVisibleErrorRow(report)) return null;`
   - 親の本清掃（`:1714-1718` の `findTimetableById(… { id: true, precheck: true, start_at: true })`）の `select` に `facility: { select: { deleted_at: true } }, section: { select: { deleted_at: true } }` を足し、`mainRow` が決まった後（`:1721-1725` の例外の後）に `if (!isVisibleFacilitySection(mainRow)) return null;`（V9。★一覧に出ない ⇔ 404★）
   - `DataSource.precheckItemById()` の docstring（`:309-321`）に「★施設・清掃エリアが削除済み（エラー行・親の本清掃のどちらか）なら null（404）★」
4. `lib/db/precheck-mapping.ts:233` の `PrecheckErrorRowSource` が `facility` の形を固定していれば、`deleted_at` を受けられるように広げる（構造的な型なので余分な列は通るはず。`tsc` で確かめる）
5. `lib/db/__tests__/robot-error-select.test.ts`: 「`ROBOT_ERROR_PRECHECK_ROW_SELECT` も `facility.select.deleted_at` と `section.select.deleted_at` を含む」を足す（AX 段階2 の 11 と同じ形）

### 2-6. コメントを直す（G2・D3）

| # | 場所 | 今（要旨） | 直す |
|---|---|---|---|
| 2-6a | `lib/db/index.ts:924-929` | 「`deleted_at` はこの4テーブルのどれにも無いので論理削除で絞らない。…論理削除された施設のログも返している（それが現状の挙動）。だから施設名の解決も絞らない」 | 「★予定の4テーブルに `deleted_at` は無いが、行の施設・清掃エリアが削除済みなら出さない（`mainTimetableWhere()` の `FACILITY_SECTION_VISIBLE_WHERE`。指示書BB U1・D-182）★。★施設名の解決は絞らない（罠7。理由は `lib/db/visibility.ts`）★」 |
| 2-6b | `:250-256`・`:263-265`（`DataSource.facilityNameIndex()` / `facilityGroupIndex()`） | 「論理削除された施設のログが検索から静かに消えます」 | 「★解決は行の可視性と別の責務★（D3）。行は各メソッドの可視性の条件で外れる」 |
| 2-6c | `:591`・`:603`（fixtures 版） | 「Prisma 版も絞らない。罠7」 | 変えない（意味は同じ）。「罠7」の後に「（行の可視性は別。D-182）」 |
| 2-6d | `:773-774`（fixtures 版 `cleanArea()`） | 「Prisma 版も `deleted_at` で絞らない」 | 「★施設マスタは★ Prisma 版も絞らない（罠7）。★面積の行は Prisma 版では削除済みの施設・清掃エリアを外す（fixtures に削除の概念は無い）★」 |
| 2-6e | `:1252-1255`・`:1265-1268`（Prisma 版） | 「論理削除された施設のログは `cleanLogs()` にいま出ている」 | 2-6b と同じ趣旨に |
| 2-6f | `:1418-1419`（`prechecks()` の docstring） | 「`robot_error_reports` に `deleted_at` は無いので論理削除で絞らない」 | 「★施設・清掃エリアが削除済みの行は出さない（予定の `where` とエラー行の `isVisibleErrorRow()`。指示書BB U1）★」 |
| 2-6g | `:2153-2155`（`rescueRequired()`） | 「`facilities()` で代用しないこと（論理削除された施設が要レスキューから静かに消える）」 | 「★施設名の解決は `loadFacilityNameIndex()`（罠7）★。削除済みの施設・清掃エリアの行は入力（`loadConsecutiveInputs()`）の段階で外れる（D-182）」 |
| 2-6h | `:2174-2179`（「`loadConsecutiveInputs()` に `facility_id` の条件を足していない…申し送り: SQL に降ろすなら4経路まとめて直すこと」） | — | ★段階4-1 で直すので、そこで書き直す★（ここでは触らない） |
| 2-6i | `:2264-2265`（`cleanArea()`） | 「★`deleted_at` で絞らない理由は…★」 | 「★施設マスタを★ `deleted_at` で絞らない理由は…（面積の行は `where` で外す。D-182）」 |
| 2-6j | `:2506-2508`（`facilityPerformance()`） | 「罠7 の扱いは `cleanLogs()` とは事情が違う」 | 「★全画面で削除済みを出さない（D-182）。`cleanLogs()` とも揃った★」 |
| 2-6k | `lib/db/clean-area-source.ts:41-60` | 「施設マスタを `deleted_at` で絞らない理由…（清掃詳細ログも論理削除された施設のログを返している）…それでも消えた施設を出したくないと言われたら、`cleanLogs()` 側と一緒に決めること」 | ★理由1・2（絞るとマスタに無い `facility_id` が `unmatched` に回り合計が減る）は残す★。「清掃詳細ログも返している」と最後の段落を「★2026-10-19 ユーザー決定で、削除済みの施設・清掃エリアの★面積の行★は全画面で外すことになった（`CleanAreaWhere` の条件。指示書BB U1・D-182）。★マスタは今までどおり絞らない★（絞ると `unmatched` の誤報になる）」に |
| 2-6l | `lib/db/facility-name-source.ts`・`lib/db/facility-group-source.ts` の docstring（罠7 の理由） | 「論理削除された施設のログは出ている」等 | D3 の理由に（★`where` は足さない★） |

### 2-7. テスト（★表1 を先に★）

| # | ファイル | 確かめること |
|---|---|---|
| 2-7a | `lib/db/__tests__/visibility.test.ts`（新規） | `isVisibleFacilitySection()` の4通り（両方 NULL → true／施設だけ／清掃エリアだけ／両方 → false。日時は過去・未来とも false） |
| 2-7b | `app/api/clean-logs/__tests__/route.test.ts`・`lib/db/__tests__/clean-log-run-meta-source.test.ts` | V1・V4: `cleanTimetable.findMany` と `cleanTimetableHistory.findMany` の両方の `args.where` に `FACILITY_SECTION_VISIBLE_WHERE` が入る。`cleanLogs()` と `cleanLogRunMeta()` で同じ |
| 2-7c | `app/api/clean-logs/[id]/__tests__/route.test.ts` | V2（スタブが where を見て0件を返す形で → advice / areas とも404）・V3（`areaCompletions()` の `where`） |
| 2-7d | `app/api/prechecks/__tests__/route.test.ts` | V7（`where` を無視するスタブに施設削除・エリア削除のエラー行 → 出ない・`summary` に数えない）・V8（予定の `where`） |
| 2-7e | `app/api/prechecks/[id]/status/__tests__/route.test.ts` | V9（エラー行の施設削除・エリア削除・★親の本清掃の施設削除★の3通り → 404・`updateMany` 0回） |
| 2-7f | `lib/db/__tests__/consecutive-source.test.ts` | V15（`where` に入る。`select` は変わらない — 既存の select のテストが通る） |
| 2-7g | `app/api/reports/__tests__/route.test.ts` | V11・V13・V16（当期の予定の `where`）・V14（`facilityErrorSummary.findMany` の `where.facility`） |
| 2-7h | `app/api/clean-area/__tests__/route.test.ts`・`lib/db/__tests__/clean-area-source.test.ts` | V17（集計・完了率の行・番人の3本が同じ `where` を受け取り、可視性が入っている） |
| 2-7i | スケジュール（1-7h のファイル）・`app/api/dashboard/__tests__/route.test.ts` | V5（`cleanSchedule.findMany` の `where`） |
| 2-7j | `lib/db/__tests__/facility-name-source.test.ts`・`facility-group-source.test.ts` | ★V19: `where` が無いままであること★（既存のテストがあれば通ることを確かめるだけ） |

### 2-8. 壊して確かめる（G3）

1. `mainTimetableWhere()` の `...FACILITY_SECTION_VISIBLE_WHERE` を消す → ★2-7b が赤★ → 戻す
2. `precheckItemById()` の `if (!isVisibleFacilitySection(mainRow)) return null;` を消す → ★2-7e の親の本清掃の場合が赤★ → 戻す
3. `cleanArea()` の where から消す → ★2-7h が赤★ → 戻す

### 2-9. 確かめてコミット

- `npx tsc --noEmit -p .`・`npm run lint`・`npm test`（件数）
- ★fixtures の件数が変わらないこと★: `app/api/**/route.fixtures.test.ts` が全部通る（fixtures に削除の概念が無いので変わらないはず。変わったら止まって報告）
- コミット: `lib/db/visibility.ts`・`lib/db/error-report-mapping.ts`・`lib/db/index.ts`・`lib/db/consecutive-source.ts`・`lib/db/clean-area-source.ts`・`lib/db/robot-error-select.ts`・`lib/db/precheck-mapping.ts`（型を広げたなら）・`lib/db/facility-name-source.ts`・`lib/db/facility-group-source.ts`・`d-watch-api.yaml`・テスト
  - メッセージの例: `feat: 削除済みの施設・清掃エリアの行を全画面(詳細ログ・ダッシュボード・アラート・スケジュール・レポート・総清掃面積)から外し、見えない行への PATCH /prechecks を404にする(指示書BB 段階2・U1・D-182)`

---

## 段階3 — 結果待ち・CSV の年・入力の検証（A3・A4・A7〜A9・A17）

### 3-1. A3 エリア別完了率の結果待ち（D11・表6）

1. YAML（先に）: `AreaCompletionItem`（`d-watch-api.yaml:2069` 以降）に `result_pending: { type: boolean }`（`required` にも）。説明「★レポートがまだ届いていないだけかもしれない清掃（D-173。`isResultPending()`）★。`grade` は × のまま・`completion_rate` は null のまま」。`/clean-logs/{id}/areas`（`:591-667`）の `description` に「★結果待ちのエリアは `problem_count` に数えず、`filter=problem` に出さない（`total_count` には数える）★」、`problem_count`（`:658-661`）の説明に「結果待ちを除く」
2. 型: `lib/types/index.ts:350-366`（`AreaCompletionItem`）と `:394-415`（`AreaCompletionRow`）に `result_pending: boolean`（docstring つき）
3. `lib/db/index.ts` の `areaCompletions()`（`:1362-1393`）: `.map` の中で `result_pending: isResultPending({ startAt: fromDbJst(row.start_at), endAt: fromDbJst(row.end_at), now, hasReport: !notExecutedFromReport(report) })`（★`now` は `:1363` の同じもの★。`end_at` は `include` で読めている）
4. fixtures: `lib/fixtures/areas.ts` の `build()`（`:173` 以降）が作る行に `result_pending: false`（★今日の結果待ちの清掃ログ（`lib/fixtures/clean-logs.ts:346` の施設11）に対応するエリアの行があれば `true`★ — `build()` が清掃ログから行を作っているなら、ログの `result_pending` を写す。★データの値を新しく作らない★）。`lib/db/fixture-area-completions.ts` が行を作り直していればそこも
5. `lib/clean-logs/areas.ts` の `buildAreaCompletions()`（`:145-170`）:
   - `items` の各行に `result_pending: r.result_pending`
   - `problemCount` を ★`items.filter((i) => !i.result_pending && isProblemGrade(i.grade)).length`★
   - `filter === "problem"` の絞り込みも ★`!i.result_pending && isProblemGrade(i.grade)`★
   - ★判定を2回書かないため、`isProblemArea(item)` をこのファイルに1つ置いて両方が使う★
   - docstring（`:114-144`）に「★結果待ちは問題に数えない（D-173 と同じ。指示書BB A3）★」
6. テスト: `lib/clean-logs/__tests__/areas.test.ts` に表6 A1〜A6、`lib/db/__tests__/fixture-area-completions.test.ts` か `app/api/clean-logs/[id]/__tests__/route.test.ts` に A7・A8（Prisma のスタブで `end_at` を境界に）
7. 壊して確かめる: `problemCount` の `!i.result_pending &&` を消す → ★A2 が赤★ → 戻す

### 3-2. A4 CSV の年（D12・表7）

1. `lib/format/date.ts`（`formatMd()` `:7-12` の後）に:

```ts
/** 2026-08-27T02:00:00+09:00 → 2026/08/27（★CSV の日付列★。指示書BB A4・D-182。画面は `formatMd()` のまま） */
export function formatYmdSlash(iso: string): string {
  return `${iso.slice(0, 4)}/${iso.slice(5, 7)}/${iso.slice(8, 10)}`;
}
```
2. `lib/clean-logs/csv.ts:12`（import）と `:73`（`formatMd(r.scheduled_at)`）を `formatYmdSlash` に
3. YAML `/clean-logs/export`（`:464` 以降）の 200 の説明に「日付の列は `YYYY/MM/DD`（D-182。旧: `M/D`）」
4. テスト: `lib/format/__tests__/date.test.ts` に表7 C1〜C3、`lib/clean-logs/__tests__/csv.test.ts` に「`cells(base)[3]` が `2026/09/13`」（`:96` の `base` の `scheduled_at`）
5. `docs/spec.md:48` は段階6 で直す

### 3-3. A7 存在しない日付（D13・表8 Q1〜Q6）

1. `lib/clean-logs/query.ts:41-46` の `parseDate()`:

```ts
export function parseDate(value: string | null): Date | null {
  if (!value) return null;
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
  const d = new Date(`${value}T00:00:00+09:00`);
  if (Number.isNaN(d.getTime())) return null;
  // ★存在しない日付（2026-02-31 は 03-03 になる）を弾く★ — JST の日付に戻して入力と一致するか（指示書BB A7・D-182）。
  //   ★`toJstIso()` で戻す（実行環境のタイムゾーンに依存しない）★
  return toJstIso(d).slice(0, 10) === value ? d : null;
}
```
   docstring（`:38-40`）に「存在しない日付も null」。
2. 呼び出し元は変えない（各 `parseXxxQuery()` は `parseDate()` が null なら既存の422を返す）。`/dashboard`（`app/api/dashboard/route.ts:60`）も同じ
3. YAML: 日付のパラメータの説明（`/clean-logs`・`/clean-logs/export`・`/prechecks`・`/errors`・`/clean-area`・`/dashboard` の `date`）の 422 の説明か `ValidationError` の説明に「★存在しない日付（例 2026-02-31）も422★」（G1。`grep -n "format: date" d-watch-api.yaml` で全部見る）
4. テスト: `lib/clean-logs/__tests__/query.test.ts` に表8 Q1〜Q5、各ルートのテストのどれか1本ずつに Q6（`/clean-logs`・`/dashboard`・`/prechecks`・`/errors`・`/clean-area`）
5. 壊して確かめる: 往復の行を消す → ★Q1 が赤★ → 戻す

### 3-4. A8 その月に無い週（D14・表8 W1〜W5）

1. `lib/reports/query.ts` の週の検証（`:209-219`）の後（`week = weekRaw;` の後）に:

```ts
    // ★その月に存在しない週は422★（200・空にしない。指示書BB A8・D-182）—
    //   ★週の数え方を知っているのは `periodRange()`（`weekOfMonth()`）だけ★（D-071）。ここで週数を数え直さない
    if (periodRange("weekly", year, month, week) === null) {
      return fail(422, "validation_failed", "weekの値が不正です", {
        week: [`${year}年${month}月に第${week}週はありません`],
      });
    }
```
   （`periodRange` を `@/lib/format/period` から import）
2. `lib/db/index.ts:2763-2768`（`continuousProblems()` の「★例外にせず空配列を返す★」）のコメントを「★ルートの検証（`parseReportQuery()`）で422 にしたので通常は来ない。歯止めとして残す★（D-182）」に。`reportKpis()` `:2295-2296` の「`null` ＝ その月に存在しない週…当期にはなりえない」も同じ趣旨を1行足す
3. YAML `Week`（`:1828` 以降）の説明に「★その月に存在しない週（例 2026年9月の第6週）は422★（D-182）」
4. テスト: `lib/reports/__tests__/query.test.ts` に W1〜W4、`app/api/reports/facility-performance/export/__tests__/route.test.ts` に W5
5. ★既存のテストで「第6週 → 200・空」を固定しているものがあれば、表8 W2 に書き換える★（`grep -rn "week=6\|week: 6" app lib --include=*.test.ts`）

### 3-5. A9 `/prechecks` の `date_from > date_to`（表8 P1・P2）

1. `lib/prechecks/query.ts:67-76` の後に、`lib/error-reports/query.ts:76-78` と同じ形で:

```ts
  if (dateFromRaw && dateToRaw && dateFromRaw > dateToRaw) {
    return fail(422, "validation_failed", "date_fromはdate_to以前の日付を指定してください");
  }
```
   （★両方とも `parseDate()` を通った `YYYY-MM-DD` なので文字列で比べてよい★。気になるなら `parseDate()` の戻りで比べる — どちらでも結果は同じ）
2. YAML `/prechecks` の 422 の説明に「`date_from > date_to` も422」
3. テスト: `lib/prechecks/__tests__/query.test.ts` に P1・P2
4. ★画面の文言は BC★（§8）

### 3-6. A17 KPI の対象顧客を AND に（D15・表9）

1. `lib/reports/target-customer.ts`:
   - 型に足す: `export interface TargetCustomerNoMatch { ok: true; customerId: null; }`（★「顧客とグループが食い違うので該当なし」★）。`TargetCustomerResult` に加える。`TargetCustomerResolved.customerId` は `number` のまま
   - `resolveTargetCustomer()` の `:99-101` を:

```ts
  if (filters.customerId !== null) {
    // ★`group_id` も指定されていたら AND★（YAML の `GroupId`。指示書BB A17・D-182。★AK §2-1 の決定表1行目「customer_id が勝つ」を上書き★）—
    //   その顧客がそのグループの傘下でなければ「該当なし」（200・空）。★`group_id` が無ければ今どおり存在確認をしない（6-9）★
    if (filters.groupId !== null) {
      const inGroup = customers.some((c) => c.id === filters.customerId && c.group_id === filters.groupId);
      if (!inGroup) return { ok: true, customerId: null };
    }
    return { ok: true, customerId: filters.customerId };
  }
```
   - ファイル冒頭の決定表（`:22-29`）の1行目を「`customer_id` が指定されている → ★`group_id` も指定されていて傘下でなければ 200・空。それ以外はその顧客★（存在確認はしない。6-9）」に、`:79-80` の「`groupId` と食い違っていても `customerId` を優先する — 決定表の1行目がそう定めている」と「`customerId` が指定されていたら、顧客マスタを一切見ない」を新しい規則に書き直す（G2）
2. `app/api/reports/kpi/route.ts`:
   - `:80`（`if (parsed.query.customerId === null)`）を ★`if (parsed.query.customerId === null || parsed.query.groupId !== null)`★ に。`:72-78` のコメントも「`customer_id` だけのときは読まない」に
   - `resolveTargetCustomer()` の結果が `customerId: null` なら ★データ層を呼ばずに `emptyKpi()` を200で返す★（`:96` の後。`selectKpiRow` 等は通らない）
3. YAML `/reports/kpi`（`:903-906`）の「対象顧客の決め方」に1行: 「customer_id と group_id の両方 … その顧客がそのホールディングスの傘下ならその顧客、そうでなければ 200・空（AND。`GroupId` の説明どおり。D-182）」
4. テスト: `lib/reports/__tests__/target-customer.test.ts:38-45`（「customer_id と group_id の同時指定は customer_id が勝つ」）を ★表9 K2・K3・K7 に書き換える★。K4・K5 を足す。`app/api/reports/__tests__/route.test.ts` に K3（200・空・`customerSummary.findMany` 0回）
5. 壊して確かめる: `if (!inGroup) …` を消す → ★K3 が赤★ → 戻す

### 3-7. 確かめてコミット

- `npx tsc --noEmit -p .`・`npm run lint`・`npm test`（件数）
- コミット（2つに分けてよい: 3-1〜3-2 と 3-3〜3-6）。ファイル: `d-watch-api.yaml`・`lib/types/index.ts`・`lib/db/index.ts`・`lib/fixtures/areas.ts`（と `lib/db/fixture-area-completions.ts`）・`lib/clean-logs/areas.ts`・`lib/format/date.ts`・`lib/clean-logs/csv.ts`・`lib/clean-logs/query.ts`・`lib/reports/query.ts`・`lib/prechecks/query.ts`・`lib/reports/target-customer.ts`・`app/api/reports/kpi/route.ts`・テスト
  - メッセージの例: `fix: エリア別完了率で結果待ちを問題に数えず、CSV の日付に年を入れ、存在しない日付・その月に無い週・逆の期間を422にし、KPI の顧客とグループを AND にする(指示書BB 段階3・D-182)`

---

## 段階4 — 頑丈さと読みすぎ（A5・A6・A10〜A12）

### 4-1. A6 連続日数の入力を施設で絞る（D16・表10 N1〜N6）

1. `lib/db/consecutive-source.ts:167-178`: `ConsecutiveDbRange` に `facility_ids: number[] | null`（★必須★。docstring「`null` = 絞らない／`[]` = 0行（罠1）。★連続日数は施設ごとに独立に数えるので、施設で絞っても絞った施設の値は変わらない（`computeConsecutiveProblemDays()`）★。★単一ID（`/advice`・`/areas`）は `null`（U4。索引の後）★」）。`CONSECUTIVE_RANGE_UNBOUNDED` に `facility_ids: null`
2. `loadConsecutiveInputs()` の `where`（`:395-402`）に `...(range.facility_ids !== null ? { facility_id: { in: range.facility_ids.map((id) => BigInt(id)) } } : {}),`
3. 呼び出し元（`tsc` が指す）:
   - `cleanLogs()` `:1195-1198`: `facility_ids: query.facility_ids`（★単一ID のときは `query.facility_ids` が `null`★ — `advice` / `areas` のルートが `null` を渡している。N6）
   - `rescueRequired()` `:2164-2167`: `facility_ids: query.facility_ids`。★`filterRowsByFacilityIds()`（`:2181`）は正として残す★
   - `facilityPerformance()` `:2573-2577`: `facility_ids: query.facility_ids`
   - `continuousProblems()` `:2799-2802`: `facility_ids: query.facility_ids`
4. コメント `:2173-2179`（「`loadConsecutiveInputs()` に `facility_id` の条件を足していない…申し送り: 4経路まとめて直すこと」）を「★2026-10-19 に4経路まとめて `facility_ids` を渡すようにした（指示書BB A6）。`filterRowsByFacilityIds()` は正として残す★」に（G2）。`lib/db/consecutive-source.ts:370-380` の `@param range` にも1行
5. テスト: `lib/db/__tests__/consecutive-source.test.ts` に N1〜N3（2表の `where`）、`lib/db/__tests__/clean-log-mapping.test.ts` に ★N4（不変性）★、`app/api/clean-logs/__tests__/route.test.ts` に N5、`app/api/clean-logs/[id]/__tests__/route.test.ts` に N6
6. 壊して確かめる: `cleanLogs()` で `facility_ids: query.facility_ids` を `facility_ids: []` にする → ★N5 が赤★ → 戻す

### 4-2. A6 ダッシュボードの `dsAlerts()`（D17・表10 N7〜N9）

1. `lib/types/index.ts` に `export interface DsAlertDbQuery { /** ★true = 解消でないものだけ（ダッシュボード）★。false = 全件（`/ds-alerts`。summary に解消も数える） */ open_only: boolean; }`
2. `lib/db/index.ts:370-375`（インターフェース）: `dsAlerts(query: DsAlertDbQuery): Promise<DsAlert[]>;`。docstring に「★引数は必須（C-4 項4）★。ダッシュボードはバナー（解消でないものだけ見る）のため `open_only: true`」
3. `lib/db/ds-alert-mapping.ts`:
   - `dsAlertWhere(query: DsAlertDbQuery)`（`:97-103`）: `status: { in: query.open_only ? <未解消・対応中のコード> : <0/1/2> }`（★コードは `robotErrorStatusFromResolutionStatus()` から作る★）
   - `dsAlertsFromErrorRows(rows, query)`（`:89-91`）: `open_only` なら変換後の `resolution_status !== "resolved"` だけ（★正★。N9）
4. fixtures 版 `dsAlerts()`（`:692-696`）・Prisma 版（`:1977-1986`）に `query` を通す
5. 呼び出し元: `app/api/ds-alerts/route.ts:48` → `db.dsAlerts({ open_only: false })`／`app/api/dashboard/route.ts:152` → `db.dsAlerts({ open_only: true })`（コメント `:149-150`・`:189` の「この2本は引数を取らない契約」を直す）
6. テスト: 既存の `db.dsAlerts()` の呼び出し（テスト内）を引数つきに。`lib/db/__tests__/ds-alert-mapping.test.ts` に N9、`app/api/ds-alerts/__tests__/route.test.ts` に N8（`where` の `status`）、`app/api/dashboard/__tests__/route.test.ts` に N7。★`app/api/__tests__/master-filter-dashboard-reports.test.ts:174-198` がそのまま通ること★（`vi.spyOn(db, "dsAlerts")` の差し替えは引数を見ない）

### 4-3. A5 `try` の外の例外（D18・表8 T1〜T3）

| # | 場所 | 直す |
|---|---|---|
| 4-3a | `app/api/settings/advice/route.ts:37`（GET の `loadAdviceRules()`） | `try { … } catch (e) { 500・internal_error・「データの取得に失敗しました」・logApiError({ route: ROUTE, method: "GET", … }) }`。`items` の組み立てまで `try` に入れてよい |
| 4-3b | `app/api/settings/display-limits/route.ts:99`（GET の `loadDisplayLimitSettings()`） | 同上 |
| 4-3c | `app/api/clean-logs/[id]/advice/route.ts:115`（`loadAdviceRules()`） | 同上（`method: "GET"`） |

- テスト: 各ルートのテスト（無ければ `app/api/settings/**/__tests__/route.test.ts` を新規）で T1〜T3（`vi.spyOn` で例外を投げさせ、500・本文の形・`api.error` が1件）

### 4-4. A5 JSON の本文がオブジェクトでない（D19・表8 J1〜J3）

1. 共通の小さな関数を `lib/api/json-body.ts`（新規）に置く:

```ts
/** JSON の本文がオブジェクト（null・配列でない）か（指示書BB A5・D-182。`lib/prechecks/status-update.ts:103` と同じ判定） */
export function isJsonObject(body: unknown): body is Record<string, unknown> {
  return body !== null && typeof body === "object" && !Array.isArray(body);
}
```
2. 4ルートの `body = await request.json();` の直後（`try` の後）で `if (!isJsonObject(body)) return errorResponse(422, "validation_failed", "JSONの形式が不正です");`:
   - `app/api/auth/login/route.ts:31-37`（`body.token` の前）
   - `app/api/ds-devices/[serial_number]/note/route.ts:61-67`（`body.note` の前）
   - `app/api/settings/advice/route.ts:70-76`（`body.rule` の前）
   - `app/api/settings/display-limits/route.ts:113-122`（`body.key` の前）
   - ★`body` の型注釈（`let body: { token?: unknown }` 等）は `unknown` で受けて `isJsonObject()` で狭める形にする★（型で嘘をつかない）
3. YAML: 4本の 422 の説明に「本文が JSON のオブジェクトでない（`null`・配列など）も422」（G1）
4. テスト: `app/api/ds-devices/[serial_number]/note/__tests__/route.test.ts`・`app/api/auth/login` のテスト（無ければ新規）・settings の2本で J1・J2

### 4-5. A10 `rule: "constructor"`（表8 H1〜H3）

- `app/api/settings/advice/route.ts:77`: `if (!ruleId || !DEFAULT_ADVICE_RULES[ruleId])` → ★`if (typeof body.rule !== "string" || !Object.hasOwn(DEFAULT_ADVICE_RULES, body.rule))`★、その後で `const ruleId = body.rule as AdviceRuleId;`（`as` は検証の後だけ）
- `lib/settings/advice.ts`（`saveAdviceRule()` / `resetAdviceRule()`）が `rules[ruleId]` を同じ形で引いていれば、同じ穴が無いか確かめる（`grep -n "\[ruleId\]\|\[id\]" lib/settings/advice.ts`）。あれば同じく直し作業報告に
- `display-limits` は既に `hasOwnProperty`（`lib/settings/display-limits.ts:348-353`）— 変えない
- テスト: H1〜H3

### 4-6. A12 JSON ストアの一時ファイル（D20・表12）

- `lib/settings/json-store.ts:85`: `const tmpPath = \`${filePath}.tmp\`;` → `const tmpPath = \`${filePath}.${process.pid}.${randomUUID()}.tmp\`;`（`import { randomUUID } from "node:crypto";`）。コメント（`:86-93` 付近）に「★一時ファイルは書き込みごとに別の名前（同時保存で同じ .tmp を共有して中身が混ざるのを防ぐ。指示書BB A12）。同時保存の後勝ちは今までどおり★」。ファイル冒頭（`:11-13`）の「同時保存は後勝ち」の段落に1行
- テスト: `lib/settings/__tests__/json-store.test.ts` に表12 J1・J2。★Windows で `rename` が `EPERM` で揺れたら、リトライを足さずに止まって報告★（§10 Q6）

### 4-7. A11 確定期間のサマリが0行（表13）

- `lib/db/index.ts:2366-2371`（`reportKpis()` の「当期でなければ詳細テーブルを1行も読まない」）: `return confirmedKpiRows;` の前に、★`range` があり当期でなく `confirmedKpiRows.length === 0`★ なら `logEvent("warn", { event: "data.anomaly", metric: "customer_summary_rows", expected: 1, actual: 0, period_mode: "summary_table" })`（★挙動は変えない★）
- テスト: `app/api/reports/__tests__/route.test.ts` に L1〜L3（`logEvent` の spy）

### 4-8. 確かめてコミット

- `npx tsc --noEmit -p .`・`npm run lint`・`npm test`（件数）
- コミット: `lib/db/consecutive-source.ts`・`lib/db/index.ts`・`lib/types/index.ts`・`lib/db/ds-alert-mapping.ts`・`app/api/ds-alerts/route.ts`・`app/api/dashboard/route.ts`・`app/api/settings/advice/route.ts`・`app/api/settings/display-limits/route.ts`・`app/api/clean-logs/[id]/advice/route.ts`・`lib/api/json-body.ts`・`app/api/auth/login/route.ts`・`app/api/ds-devices/[serial_number]/note/route.ts`・`lib/settings/json-store.ts`（と 4-5 で直したなら `lib/settings/advice.ts`）・`d-watch-api.yaml`・テスト
  - メッセージの例: `fix: 連続日数の入力を施設で絞り、ダッシュボードの D-Sec は未解消だけ読み、try の外の例外・オブジェクトでない本文・constructor・一時ファイルの共有を直し、サマリ0行をログに残す(指示書BB 段階4・D-182)`

---

## 段階5 — 掃除（A13〜A15）

### 5-1. A13 R1 の閾値（D21・表11）

1. `lib/judge/advice.ts`:
   - `matchAdviceRules(input: AdviceInput, thresholds: { level2Days: number; level3Days: number } = ESCALATION_THRESHOLDS)`
   - `:66` `if (!departed && consecutiveProblemDays <= 1)` → ★`if (!departed && consecutiveProblemDays < thresholds.level2Days)`★。コメント `:65` に「★2日目（level2Days）以降は R3 / R4 が担当（閾値は設定から。直書きしない — 指示書BB A13）★」
   - R3（`:76`）・R4 も `thresholds.` を使う
2. `lib/db/clean-log-mapping.ts:435-438` の「`lib/judge/advice.ts` の `consecutiveProblemDays <= 1`」→「`consecutiveProblemDays < level2Days`」（G2）。他に `<= 1` を書いた説明が無いか `grep -rn "<= 1" lib docs/code-map.md | grep -i "advice\|R1"`
3. テスト: `lib/judge/__tests__/advice.test.ts` に表11 T1〜T6
4. 壊して確かめる: `< thresholds.level2Days` を `<= 1` に戻す → ★T4 が赤★ → 戻す

### 5-2. A14 コメントと未使用のコード（D22）

| # | 場所 | 直す |
|---|---|---|
| 5-2a | `lib/config/index.ts:191-195`（`LAUNCH_GRACE_MINUTES`） | docstring を「★未使用（D-091 項1 / D-173）★。出発失敗は予定＋猶予では判定しない（レポートの行の有無と結果待ち `RESULT_WAIT_MINUTES`）。`CLAUDE.md` §7 の『猶予30分は設定値として外出し』に従って値だけ残している」に。★定数は消さない★ |
| 5-2b | `lib/format/period.ts:95-98`（`weekStartMs()`）と `:122-125`（`weekStart()`） | ★消す★（テストだけが使う。D-071 の帰属は `weekOfMonth()` だけ）。`:83` の「`weekStart` / `weekOfMonth` は」→「`weekOfMonth` は」 |
| 5-2c | `lib/format/__tests__/period.test.ts:11`（import）・`:36-66`（`describe("weekStart…")`）・`:128-129`（H-3 の中の2行） | 消す。H-3 は `weekOfMonth()` の3つの `expect` だけで趣旨が成り立つ（コメント `:128` も消す） |
| 5-2d | `lib/judge/kpi.ts:66-68`（「…暫定の式なので、戻すときにも名前を戻さずに済む」） | 「暫定の式なので、戻すときにも名前を戻さずに済む。」の1文を消す（★式は第11回 P-2=A で確定。D-158★）。前の文は残す |
| 5-2e | `lib/config/index.ts:333-362`（`CLEAN_ERROR_LABELS` と `cleanErrorLabel()` とその docstring） | ★消す★。`CLEAN_ERROR`（`:321-332`）とその docstring は残す。docstring の中で `cleanErrorLabel()` を引いている行（`:311-314`・`:300-308` 付近）を「（表示名の関数は 2026-10-19 に削除。指示書BB A14）」に |
| 5-2f | `lib/config/__tests__/clean-error.test.ts:14`（import）・`:35-70`（`describe("cleanErrorLabel")` と `CLEAN_ERROR_LABELS` を使う `it`） | 消す。`describe("CLEAN_ERROR")`（`:16-33`）は残す |
| 5-2g | コメントで `CLEAN_ERROR_LABELS` を引いている場所（`lib/clean-logs/__tests__/csv.test.ts:279`・`lib/format/clean-result.ts:7`・`lib/format/__tests__/clean-result.test.ts:24`・`lib/config/index.ts:373` `:394`） | ★「旧 `CLEAN_ERROR_LABELS`（2026-10-19 削除）」★ と読めるように直す（消えた名前を今あるように書かない。G2） |

### 5-3. A15 `package.json`・`tsconfig.json`（D23）

1. `npm install -D @next/env@16.3.3 @types/node@^22`（★ネットワークが要る★）。`package.json` に `"engines": { "node": ">=22.12" }` を手で足す（`"private": true` の後）
   - ★`git diff -- package.json` が3か所（`engines`・`@next/env`・`@types/node`）だけ、`package-lock.json` が対応する変更だけ★であること（5-15）
2. `tsconfig.json:33`: `"exclude": ["node_modules"]` → `"exclude": ["node_modules", ".claude"]`
3. `npx tsc --noEmit -p .` — ★`@types/node` を 20 → 22 にしたことによる型エラーが出たら、型を緩めて黙らせず止まって報告★（5-14）
4. ★サーバーの Node の版は TeamWorks が構築する VM（D-143）で、まだ分からない★ — `engines` は ★`npm` が警告を出すだけ★（`engine-strict` でない）。§7-5 に書く

### 5-4. 確かめてコミット

- `npx tsc --noEmit -p .`・`npm run lint`・`npm test`（★件数は減る★ — `weekStart` と `cleanErrorLabel` のテストのぶん。前後を控える）
- コミット: `lib/judge/advice.ts`・`lib/db/clean-log-mapping.ts`・`lib/config/index.ts`・`lib/format/period.ts`・`lib/judge/kpi.ts`・`lib/format/clean-result.ts`・テスト（`advice.test.ts`・`period.test.ts`・`clean-error.test.ts`・`csv.test.ts`・`clean-result.test.ts`）・`package.json`・`package-lock.json`・`tsconfig.json`
  - ★2つに分けてよい★（コードの掃除と `package.json`・`tsconfig.json`）
  - メッセージの例: `chore: R1 の閾値を設定から読み、未使用の weekStart・cleanErrorLabel と古い説明を消し、engines・@types/node・@next/env と tsconfig の除外を整える(指示書BB 段階5・D-182)`

---

## 段階6 — YAML の説明・資料・D-182（A16・A19）

### 6-1. ★`docs/decisions.md` の前に（5-2）★

1. `git diff -- docs/decisions.md` が ★空★ であること（★他のセッションの D-179 の追記が未コミットのままなら、書かない・`git add` しない★）。
   - 空になるまで、段階6-2〜6-4 を先に進めてよい
   - ★空にならないまま本書の残りが終わったら、D-182 を書かずに止まり、作業報告に「D-182 は未追記（`docs/decisions.md` に他のセッションの未コミットの差分があるため）」と書いて、付録A の下書きをそのまま監督に渡す★
   - ★`docs/decisions.md` を丸ごと書き直さない（Write で上書きしない）★ — 末尾に足すだけ（同じ日に上書き事故が起きている）
2. `grep -n "^### D-182" docs/decisions.md` が0件であること（2026-10-19 時点は空き。末尾は D-181 `:13090`〜`:13125`）。★埋まっていたら次の空き番号を使い、本書・コミットメッセージ・コード中の「D-182」をすべてその番号に読み替え、作業報告に書く★

### 6-2. YAML の説明（A16。挙動を変えた分は各段階で済んでいる）

| # | 場所 | 今 | 直す |
|---|---|---|---|
| 6-2a | `d-watch-api.yaml:412`（`/clean-logs` の `date_from`） | 「清掃開始日時の範囲(from)。省略時は直近1日」 | 「清掃開始日時の範囲(from)。★省略時は下限なし（全期間。D-159 追記）★」。`/clean-logs/export` の `date_from`（`:477`）に説明があれば同じに |
| 6-2b | `:224`（`/dashboard`） | 「★予定の終了 + 落ち着き時間(30分) ≤ 現在★」 | 「★予定の終了 ≤ 現在★（D-173。終わってからレポートが届くまでは `result_pending` で結果待ち）」 |
| 6-2c | `:228` | 「日数・分は lib/config の DASHBOARD_LATEST_RUN(lookbackDays / settleMinutes)が正」 | 「日数は lib/config の DASHBOARD_LATEST_RUN(lookbackDays)が正（settleMinutes は D-173 で削除）」 |
| 6-2d | `:233-234` | 「境界(終了 + 30分ちょうど付近)で数値カードと表の母集団がずれうるため」 | 「境界(予定の終了ちょうど付近)で…」 |
| 6-2e | `:976`・`:1108`（`/reports/facility-performance` と `/reports/continuous-problems` の `limit`） | `schema: { type: integer, default: 10 }` | `schema: { type: integer, minimum: 1, maximum: 200, default: 10 }`。説明に「★範囲外は丸める（0 以下 → 1、200 超 → 200）・整数でなければ既定の10。422 にしない★（`lib/reports/query.ts:233-236`・`PAGINATION.maxPerPage`）」 |
| 6-2f | `grep -n "settle\|落ち着き\|直近1日" d-watch-api.yaml` | — | ★0件になること★ |

### 6-3. 資料

| # | ファイル | 直す |
|---|---|---|
| 6-3a | `docs/spec.md:48`（CSV） | 「★日付は年つき（`YYYY/MM/DD`。D-182）★」を足す |
| 6-3b | `docs/spec.md` の F-01〜F-07・レポート・総清掃面積の節 | ★「削除済みの施設・清掃エリアは出さない（D-182）」を各画面に1行★（`grep -n "^## \|^### " docs/spec.md` で節を見て、画面ごとの箇条に足す。確定期間の制約は レポートの節に1行） |
| 6-3c | `docs/fixtures.md` | ★fixtures に削除の概念が無いこと（エラー一覧の FX-ERR-18・19 を除く）★を、エラー一覧の節の近くに1行。エリアの fixtures に `result_pending` を足したならその説明 |
| 6-3d | `docs/code-map.md` | ★本書で変えた関数・型・設定の行を引き直す★（G2）。少なくとも: `CLEAN_ERROR_LABELS` / `cleanErrorLabel()`（`:2174-2175`・`:4409`。消した）・`weekStart`・`parseDate`・`pickLatestRunPerSection`・`resolveTargetCustomer`・`dsAlerts()` の引数・`ConsecutiveDbRange`・`isVisibleErrorRow`（`lib/db/visibility.ts` を足す）・`/ds-devices` の `last_reported_at`。冒頭の改訂の履歴に「第4版改訂7: 2026-10-19（D-182 / 指示書BB）」を1項目 |

### 6-4. 確かめてコミット（資料）

- `git diff --stat` で `d-watch-api.yaml`・`docs/spec.md`・`docs/fixtures.md`・`docs/code-map.md`（と、6-1 が満たされていれば `docs/decisions.md`）だけ
- メッセージの例: `docs: YAML の説明の食い違い(date_from の省略時・settleMinutes・limit)を直し、spec・fixtures・code-map を指示書BB に追従させる(D-182)`

### 6-5. D-182 を追記する（6-1 が満たされたときだけ）

- 末尾（D-181 の「実装の記録」の最後の行の後）に、空行・`---`・空行を置いてから、付録A を足す（★D-180 と D-181 の間の区切りの実物を見て合わせる★）
- ★D-180・D-181 とそれ以前の本文は変えない★（D-180 U8 の「エラー一覧と D-Sec だけ」は経緯として残し、上書きの関係は D-182 に書く）
- 「実装の記録」は段階7 の後に埋める（一度 `<…>` のままコミットしてもよい）

---

## 段階7 — 全体の確認

1. `npx tsc --noEmit -p .` — 0件
2. `npm run lint` — 0件（★`eslint-disable` を足していない★）
3. `npm test` — ★全件通過★。ファイル数と件数を控える（段階0 の 130 / 3,083 から、足したテストと消したテスト（`weekStart`・`cleanErrorLabel`）の差）
4. ★`USE_FIXTURES=true` で `npm run build`★（PowerShell: `$env:USE_FIXTURES='true'; npm run build`）— 成功（★本体で★）
5. 最後の grep（★結果を作業報告に貼る★）:
   ```
   grep -rn "ロボットが見つかりません\|control_reports が1件もありません" lib app --include=*.ts
   grep -rn "weekStart\b\|cleanErrorLabel\|CLEAN_ERROR_LABELS" lib app components scripts --include=*.ts --include=*.tsx
   grep -n "settle\|落ち着き\|直近1日" d-watch-api.yaml
   grep -rn "consecutiveProblemDays <= 1" lib
   grep -rn "FACILITY_SECTION_VISIBLE_WHERE" lib/db --include=*.ts | grep -v __tests__
   git diff 2fc3bbb --stat -- prisma lib/db/robot-error-write.ts CLAUDE.md d-watch-mock.jsx docs/handoff.md
   ```
   - 1〜4行目: 0件（2行目はコメントの「旧 `CLEAN_ERROR_LABELS`（2026-10-19 削除）」だけなら可 — その行を貼る）
   - 5行目: `mainTimetableWhere`・`areaCompletions`・`prechecks`・`schedules`・`cleanArea`・`reportKpis`・`facilityPerformance`・`errorBreakdown`・`loadConsecutiveInputs`・`error-report-mapping.ts` の各1か所以上
   - 6行目: 何も出ない
6. ★ステージングの読み取り（任意・SELECT だけ。5-16・5-17）★ — U1 で消える件数を測る（`CLAUDE.md` §6-6: 出所は「先方投入」）。★参照用の接続で★、一時スクリプトは作業ツリーの外（スクラッチ）に置いてコミットしない。★監督に頼んでもよい★:
   ```sql
   SELECT COUNT(*) FROM facilities WHERE deleted_at IS NOT NULL;
   SELECT COUNT(*) FROM sections   WHERE deleted_at IS NOT NULL;
   -- 予定（本清掃）・スケジュール・実績・エラーのうち、施設か清掃エリアが削除済みの行
   SELECT 'clean_timetables' t, COUNT(*) FROM clean_timetables x JOIN facilities f ON f.id = x.facility_id JOIN sections s ON s.id = x.section_id
     WHERE x.precheck = 0 AND (f.deleted_at IS NOT NULL OR s.deleted_at IS NOT NULL)
   UNION ALL SELECT 'clean_timetable_histories', COUNT(*) FROM clean_timetable_histories x JOIN facilities f ON f.id = x.facility_id JOIN sections s ON s.id = x.section_id
     WHERE x.precheck = 0 AND (f.deleted_at IS NOT NULL OR s.deleted_at IS NOT NULL)
   UNION ALL SELECT 'clean_schedules', COUNT(*) FROM clean_schedules x JOIN facilities f ON f.id = x.facility_id JOIN sections s ON s.id = x.section_id
     WHERE x.deleted_at IS NULL AND (f.deleted_at IS NOT NULL OR s.deleted_at IS NOT NULL)
   UNION ALL SELECT 'clean_task_reports', COUNT(*) FROM clean_task_reports x JOIN facilities f ON f.id = x.facility_id JOIN sections s ON s.id = x.section_id
     WHERE f.deleted_at IS NOT NULL OR s.deleted_at IS NOT NULL
   UNION ALL SELECT 'facility_error_summaries', COUNT(*) FROM facility_error_summaries x JOIN facilities f ON f.id = x.facility_id WHERE f.deleted_at IS NOT NULL;
   -- U2: 生きている清掃エリアの有効なロボットの台数が1でないもの
   SELECT s.id, COUNT(r.id) AS robots FROM sections s LEFT JOIN robots r ON r.section_id = s.id AND r.deleted_at IS NULL
     WHERE s.deleted_at IS NULL GROUP BY s.id HAVING COUNT(r.id) <> 1;
   -- U3: robot_error_reports.status の分布
   SELECT status, COUNT(*) FROM robot_error_reports GROUP BY status;
   -- A1: 制御盤の状態と心拍の有無
   SELECT c.id, c.status, COUNT(cr.id) AS reports FROM controls c LEFT JOIN control_reports cr ON cr.control_id = c.id
     WHERE c.deleted_at IS NULL GROUP BY c.id, c.status;
   ```
   ★結果は D-182 の実装の記録に「先方投入・2026-10-19 SELECT」と書く★。★MySQL 互換の SQL だけ★（`CLAUDE.md` §3）。測れなかったらそう書く
7. ★ローカルDB（任意）★: `dwatch_local` で施設1つの `deleted_at` を一時的に入れて（★ローカルDB だけ★）`/clean-logs`・`/prechecks`・`/schedules`・`/dashboard` からその施設が消えることを確かめ、`npm run seed:local` で元に戻す（`lib/db/local-guard.ts` がローカル以外を拒む）。★やらなかったらそう書く★
8. D-182 の「実装の記録」を埋める（G4。6-1 が満たされていなければ下書きに埋めて監督に渡す）

---

## 7. 記録だけ（★直さない★。作業報告に「確認した・直していない」と書く）

### 7-1. ★U4 清掃ログの行を開くときの全期間読み（今回は直さない）★

★今の流れ★（2026-10-19 `2fc3bbb`）:
1. `/clean-logs/{id}/advice`（`app/api/clean-logs/[id]/advice/route.ts:58`）と `/{id}/areas`（`app/api/clean-logs/[id]/areas/route.ts:83`）は `db.cleanLogs({ id, date_from: null, date_to: null, facility_ids: null })` を呼ぶ。
2. 本体は主キーで1行だけ引く（`mainTimetableWhere()` の `id`）ので軽い。★重いのは連続日数★: `cleanLogs()`（`lib/db/index.ts:1195-1201`）が `loadConsecutiveInputs(prisma, now, { date_from: null, date_to: null })` を呼び、★下限が無いので遡りも付かず、予定の2表を全期間・全施設★ 読む（`lib/db/consecutive-source.ts:386-405`。保持2年で本清掃 約26万行×2表）。
3. その全 id を `DB_IN_CLAUSE_MAX_ITEMS`（1,000。`lib/config/index.ts:577`）ずつに分けて `clean_task_reports` を `clean_timetable_id IN (…)` で引く（`consecutive-source.ts:418-436`）。★`clean_task_reports.clean_timetable_id` には索引も FK も無い★（`prisma/schema.prisma:554-556` の `@@index` は `facility_id` / `robot_id` / `section_id` だけ）ので、★塊ごとに全表走査★（26万行なら約260回）。
4. 同じく `cleanLogs()` は事前チェック行のエラーを `robot_error_reports.clean_timetable_id IN (…)` で引く（`lib/db/index.ts:1025-1033`）— ★`robot_error_reports.clean_timetable_id` にも索引が無い★（`prisma/schema.prisma:349-351`）。こちらは1行ぶんなので塊は1つ。
- ★索引が付けば★: 3. と 4. は索引の範囲検索になる。さらに ★単一ID の連続日数の入力を「その行の施設＋その行の日付から遡り」に絞れる★（2段にする: 先に行を引いて施設と日付を得てから `loadConsecutiveInputs(…, { date_from: 日付, date_to: 日付, facility_ids: [施設] })`。★D-109 の「遡り幅は表示期間から」の約束を保てる★ — 一覧の連続日数と同じ値になる）。★索引なしでこれをやっても 3. の全表走査は残る★ので、ユーザー決定で索引の後にした（U4）。
- ★同じ列の `IN` は他の経路でも使っている★（`cleanLogs()`・`prechecks()`・`areaCompletions()`・`reportKpis()` / `facilityPerformance()` / `errorBreakdown()` の当期・`loadConsecutiveInputs()`）— 索引の効果はこれら全部に及ぶ。

★TeamWorks への依頼文の下書き★（監督が送るときに使う。★第9回で候補に挙げた「`robot_error_reports.clean_timetable_id` の索引」と、取得元の切り替え（指示書AQ）で後継になった `clean_task_reports` 側を1つにまとめたもの。依頼済みかどうかは監督が回答待ち一覧で確かめる★）:

> **件名: 索引の追加のお願い（`clean_task_reports` / `robot_error_reports` の `clean_timetable_id`）**
>
> 次の2つの索引の追加をお願いします（どちらも読み取りの性能のため。列の型・値は変えません）。
>
> | 表 | 列 | 索引名（例） |
> |---|---|---|
> | `clean_task_reports` | `clean_timetable_id` | `clean_task_reports_clean_timetable_id_index` |
> | `robot_error_reports` | `clean_timetable_id` | `robot_error_reports_clean_timetable_id_index` |
>
> **理由**: ビューアーは清掃の予定（`clean_timetables` / `clean_timetable_histories`）を期間で読み、その id で実績とエラーを `clean_timetable_id IN (…)` で引いています（1回に最大1,000件ずつ）。この列には FK も索引も無いため、引くたびに表全体を読んでいます。特に清掃詳細ログの1行を開いたときの連続日数の計算は、2年ぶんの予定の id で実績を引くため、表全体の読み取りが数百回になります。
> **期待する効果**: `IN` の引き直しが索引の範囲検索になり、清掃詳細ログ・ダッシュボード・アラート・レポートの当期の読み取りが表の行数に比例しなくなります。
> **補足**: 以前お願いした FK（D-094）を付けていただける場合は、MariaDB（InnoDB）が FK の列に索引を自動で作るため、この依頼は不要になります。`NULL`（予定外のタスク）の行はそのままで構いません。

### 7-2. 「1リクエストで1回」は今回やらない（A6 の後半）

- ダッシュボードは `cleanLogs()`（直近モードは表示期間 8日＋遡り）と `rescueRequired()`（当日＋遡り）の2本が `loadConsecutiveInputs()` を呼ぶ。直近モードでは前者の範囲が後者を含むが、★2本は別の `DataSource` のメソッドで、共有するにはリクエスト単位の入れ物（または `DataSource` の契約の変更）が要る★。日付指定のときは範囲が同じ。
- ★判定結果を変えずにできるが、契約を変える大きさの割に得が小さい★（段階4-1 で施設の絞り込みは入った）。必要になったら別の指示書で。

### 7-3. サマリ（確定期間）は削除済みを完全には外せない（D5）

- `customer_summaries`（レポートKPI の確定期間）は何も外せない。`facility_summaries`・`facility_error_summaries` は施設の削除だけ外せる（清掃エリアの削除は施設の値に含まれる）。
- ★帰結★: 同じ施設の「当期（その場集計。削除済みの清掃エリアを外す）」と「確定期間（TeamWorks の集計）」で母集団が違いうる。★TeamWorks への質問の候補★（§7-4）。

### 7-4. TeamWorks への質問の候補（次回の確認事項へ）

1. ★サマリの集計は、論理削除された施設・清掃エリアの実績を含むか★（含むなら、当方の当期の集計と母集団が違う。D-182）
2. ★当期のエラー件数（当方）と週が締まった後の `facility_error_summaries` の数え方の違い★（A18）: 当方は ★本清掃行を指すエラーだけ★ を数え（`lib/db/report-mapping.ts:412-424` の `errorCountByMainTimetable()`・`:1011-1027` の `countMainErrorTypesInRange()`）、★事前チェック行を指すもの・`clean_timetable_id` が NULL のもの（制御盤・セキュリティ・清掃開始失敗。第11回 P-8〜P-10）は数えない★。サマリ側がそれらを数えるなら、週が締まった瞬間にエラー件数・エラー発生率・種別内訳が変わる。
3. 7-1 の索引の依頼

### 7-5. その他

| # | 内容 | なぜ直さないか |
|---|---|---|
| 7-5a | 施設別パフォーマンスはロボット0台の施設を行ごと除外する（`lib/db/index.ts:2520-2527`） | U2 は「落ちない」こと。除外は指示書T 3-1 の決定で、落ちていない |
| 7-5b | `/facilities` の施設に、削除済みの清掃エリアのロボットが載りうる（`facilities()` の `robots` は `robots.deleted_at` だけ） | 施設マスタ画面は U1 の対象画面の一覧（ユーザー指示）に無い。気になれば監督が判断 |
| 7-5c | JSON ストアの同時保存の後勝ち（読み → 書きの間の上書き） | ファイル冒頭の方針（ロックは入れない）。A12 は一時ファイルの共有だけ |
| 7-5d | 本番のサーバーの Node の版は未確定（TeamWorks が構築。D-143） | `engines` は警告だけ。本番を組むときに確かめる |
| 7-5e | `@types/node` を `^22` にしたため、ローカル（v24）にしか無い API を書くと `tsc` が指す | 意図どおり（D23） |

---

## 8. 指示書BC（画面）との境目

★本書（BB）がやること★: データ・API・型・YAML・CSV（`lib/clean-logs/csv.ts`）・テスト。★画面のファイルは `components/features/ds-logs/DsLogScreen.tsx:465` の1行だけ★（`last_reported_at` を nullable にしたので `tsc` を通すため。`null` なら `"-"`）。

★BC に渡すもの★（BC の作成者・実装担当が読む。★本書の側では直さない★）:

| # | 画面 | 何を出すか | 本書のどの変更による |
|---|---|---|---|
| B1 | 清掃詳細ログの表（`components/features/detail-log/CleanLogTable.tsx:248-249`）・ダッシュボードのコンパクト版 | `model_name` / `serial_number` が `null` なら ★「-」★（今は空白になる） | 段階1 U2（D6） |
| B2 | アラート（`components/features/alerts/AlertsScreen.tsx:559`） | 出発失敗の行の `serial_number` が `null` なら「-」 | 同上 |
| B3 | スケジュール（`components/features/schedules/SchedulesScreen.tsx:158`） | `serial_number` が `null` なら「-」 | 同上 |
| B4 | D-Sec ログ（`DsLogScreen.tsx:465`） | `last_reported_at` が `null` なら「-」（★本書で最小の形は入れる★。見た目を変えるなら BC） | 段階1 A1 |
| B5 | エリア別完了率のモーダル | `items[].result_pending` が `true` の行は ★「結果待ち」★（`CLEAN_RESULT_PENDING_LABEL`。判定・完了率の見せ方は詳細ログの結果待ちに揃える）。「問題エリアのみ」には出てこない | 段階3 A3 |
| B6 | アラートの期間 | `date_from > date_to` の422 の文言の出し方 | 段階3 A9 |
| B7 | 全画面 | 削除済みの施設・清掃エリアは API が返さなくなるだけ（★画面の作業は無い★） | 段階2 U1 |

★BC と同じファイルを同じ作業ツリーで同時に触らない★。BB の段階1 のコミットが先に入っていれば、BC は `DsLogScreen.tsx:465` を引き直す。

★本書の作成時点（2026-10-19）の BC（`docs/handoff/指示書BC_画面の古い応答・失敗表示・設定の保存を直す_20261019.md`。未追跡・読んだだけ）との食い違い★ — ★監督が両方を揃える★（実装担当は BC の文書を直さない）:
- BC の §2-7 の表（`:450-452` 付近）は `CleanLogItem.serial_number` / `model_name`・`PrecheckItem.serial_number`・`Schedule.serial_number` を ★「文字列のまま」★ としている。★本書の段階1 の後は `null` がありうる★（B1〜B3 の「-」が要る）
- BC は `DsLogScreen.tsx` を大きく書き換える（段階1 の古い応答・段階4 の備考の編集モード）。★本書の `:465` の1行は、どちらが先に入っても `formatDateTime(item.last_reported_at)` を grep して引き直す★。BC が先に入っていたら、BC の新しい形の中で同じ `null` の扱い（「-」）にする
- BC は「API・型を変えない」前提。★本書の段階1（型の nullable）・段階3（`AreaCompletionItem.result_pending`）は型を変える★ — BC の実装が本書より先なら、BC の画面は本書の後に B1〜B5 を足す追加の作業が要る

---

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

### 触ってよい

- `lib/db/**`（★`robot-error-write.ts`・`local-guard.ts`・`prisma.ts` を除く★）・`lib/db/visibility.ts`（新規）
- `app/api/**`（`dashboard`・`ds-alerts`・`ds-devices/[serial_number]/note`・`settings/advice`・`settings/display-limits`・`clean-logs/[id]/advice`・`auth/login`・`reports/kpi` のルートと、テスト）
- `lib/clean-logs/**`・`lib/dashboard/latest-run.ts`・`lib/reports/query.ts`・`lib/reports/target-customer.ts`・`lib/prechecks/query.ts`・`lib/schedules/query.ts`・`lib/settings/json-store.ts`・`lib/settings/advice.ts`（4-5 で要るときだけ）・`lib/judge/advice.ts`・`lib/judge/kpi.ts`（コメントだけ）・`lib/format/date.ts`・`lib/format/period.ts`・`lib/format/clean-result.ts`（コメントだけ）・`lib/config/index.ts`・`lib/types/index.ts`・`lib/logger/events.ts`・`lib/api/json-body.ts`（新規）
- `lib/fixtures/areas.ts`・`lib/fixtures/clean-task-reports.ts`（`tsc` の不変条件と `result_pending` だけ）・`lib/db/fixture-area-completions.ts`
- 上の各ファイルのテスト（`__tests__/`）
- `components/features/ds-logs/DsLogScreen.tsx`（★`:465` の1行だけ★）
- `d-watch-api.yaml`・`package.json`・`package-lock.json`・`tsconfig.json`
- `docs/decisions.md`（★段階6-1 の条件を満たしたときに、末尾への D-182 の追記だけ★）・`docs/spec.md`・`docs/fixtures.md`・`docs/code-map.md`

### 触ってはいけない

- ★`docs/handoff/指示書BC_*`★（別のエージェント）・★`docs/handoff/指示書AY_*`・`docs/handoff/設計_制御盤ログの取り出し_*`・`docs/handoff/TeamWorks確認事項_第13回_*`・`docs/handoff/回答待ち一覧_*`・`docs/handoff/pdf/*`★（他のセッションの作業）・本書（指示書BB）
- ★`docs/decisions.md` の既存の行（D-181 とそれ以前・他のセッションの未コミットの追記）★
- `prisma/schema.prisma`・`lib/db/robot-error-write.ts`・`lib/db/local-guard.ts`・`scripts/seed-local-db.ts`
- 他の画面のコンポーネント（`components/**`。★§8 の1行を除く★）・`app/globals.css`・`lib/i18n.ts`
- `CLAUDE.md`・`d-watch-mock.jsx`・`docs/handoff.md`
- ★認証情報（パスワード・接続文字列・生成したトークン）を文書・コード・テスト・コミット・作業報告に書くこと★
- ★`git add -A` / `git add .`・push★

---

## 10. 確認事項（監督へ。★実装は本書のとおりに進めてよい★）

| # | 内容 | 本書の判断 | 監督が変えるなら |
|---|---|---|---|
| Q1 | ★連続日数の入力からも削除済みの行を外すか★（D4・表1 V15） | ★外す★（U1「全画面で出さない」。表示から消えた行が生きている施設の L1〜L3 を押し上げると説明がつかない） | 外さないなら 2-3g（`loadConsecutiveInputs()` の `where`）だけ戻す。★その場合、詳細ログに出ない行のせいで L3 が出る★ことを D-182 に書く |
| Q2 | U2 の表示は「-」か「対象なし」か（D6） | ★「-」★（既存の約束・訳が要らない・API は null） | 「対象なし」なら BC の文言と `lib/i18n.ts` の訳（BC 側）。API は null のまま |
| Q3 | 状態不明の制御盤（`controls.status` が 0/1 以外）を一覧から外す（D9） | ★外す★（「異常」に寄せるのは推測） | 「異常」で出すなら `connectionStatusFromControlStatus()` の変換表を変える話になる（D-065 Q1-2 の確認が要る） |
| Q4 | KPI の顧客とグループが食い違うときは200・空（D15） | ★200・空★（YAML の `GroupId`「0件。404 にしない」） | 422 にするなら `TargetCustomerNoMatch` を `ok: false` にし、`details` の形を決める |
| Q5 | 確定期間のサマリの制約（D5）を YAML・D-182 に書くだけでよいか | ★書くだけ★（再計算しない。D-102） | — |
| Q6 | JSON ストアの同時書き込みのテストが Windows で揺れたら | ★止まって報告★（リトライを足さない） | 直列化（ファイルパスごとのモジュール内の待ち行列）を足すか判断 |
| Q7 | `CLAUDE.md` への反映（§4.4 の「猶予30分」・§4.7 の取得元の表・§3 のセキュリティのリスト） | ★本書では触らない★（監督が直す） | — |

---

## 11. コミットの計画

| 順 | 中身 | 段階 |
|---|---|---|
| 1 | 止まる経路（U2・U3・`/ds-devices`・直近モード）。`/ds-devices` は分けてよい | 1 |
| 2 | 削除済みの施設・清掃エリアを全画面で外す（U1） | 2 |
| 3 | 結果待ち・CSV の年・入力の検証・KPI の AND（2つに分けてよい） | 3 |
| 4 | 頑丈さと読みすぎ | 4 |
| 5 | 掃除（R1・未使用のコード・コメント・`package.json`・`tsconfig.json`。2つに分けてよい） | 5 |
| 6 | YAML の説明・資料（・D-182。6-1 の条件しだい） | 6 |
| 7 | D-182 の実装の記録（6 と一緒でもよい） | 7 |

- ★`git add` はファイルパスを明示する★。各コミットの前に `git diff --cached --name-status` で中身を確かめる（★他のセッションのファイルが入っていないこと★）
- メッセージには ★何を変えたか★ と ★なぜ（指示書BB・どの項目か・D-182）★ を書き、末尾にこのセッションの決まりの共著者の行を付ける
- ★push しない★。★監督がコミットを見てから、監督の指示で develop に push する★。PR の作成とマージは daichi のレビューの後（D-154）

---

## 12. 期待される結果

- ロボットのいない清掃エリア・想定外の対応状況の行・心拍の無い制御盤・直近モードの突き合わせの食い違いで、★どの画面も500にならない★（シリアル・機種は null で画面は「-」、行は外れるか null、`data.anomaly` が残る）
- ★削除済みの施設・清掃エリアの行は、詳細ログ・CSV・ダッシュボード・アラート・スケジュール・レポート（当期と、外せる範囲の確定期間）・総清掃面積・エリア別完了率・要レスキュー・連続日数のどこにも出ない★。見えない行への `PATCH /prechecks/{id}/status` は404。名前の解決は今までどおり
- `PATCH /errors/{error_id}/status` で状態が 0/1/2 以外の行は404
- エリア別完了率で結果待ちのエリアが問題に数えられない（`result_pending` が付く）
- CSV の日付が `2026/08/27`
- 存在しない日付・その月に無い週・逆の期間（アラート）が422、KPI の顧客とグループが AND
- `try` の外の例外・オブジェクトでない本文が標準の形の 500 / 422 になり、`constructor` が通らず、JSON ストアの一時ファイルが共有されない、確定期間のサマリ0行がログに残る
- 連続日数の入力は分類で絞ったときにその施設だけを読み（値は同じ）、ダッシュボードの D-Sec は未解消だけを読む
- R1 の閾値が設定から読まれる。未使用の `weekStart`・`cleanErrorLabel` が無い。`engines`・`@types/node ^22`・`@next/env 16.3.3`・`tsconfig` の `.claude` 除外
- YAML・spec・fixtures・code-map が食い違わない。D-182 がある（または下書きが監督に渡っている）
- `tsc` 0件・`lint` 0件・`npm test` 全件通過・`USE_FIXTURES=true npm run build` 成功・`prisma/`・4カラムの書き込み・`CLAUDE.md` に差分なし

---

## 13. 完了条件

- [ ] 段階0: `2fc3bbb` の上・`git status` を控えた（他のセッションの2ファイル）・テスト件数を控えた（130 / 3,083）・AY の実装の有無を確かめた
- [ ] 段階1: 表2〜表5 のテストを足し・書き換え、★3通りに壊して赤を確かめて戻した★。YAML を先に直した
- [ ] 段階2: 表1 の各行を直し、`lib/db/visibility.ts` に規則を1か所。★罠7 の理由のコメントを書き直した（2-6）★。3通りに壊して赤を確かめた。fixtures の件数が変わっていない
- [ ] 段階3: 表6〜表9 のテスト。壊して赤を確かめた（A2・Q1・K3）
- [ ] 段階4: 表8 J・H・T、表10、表12、表13 のテスト。N5 を壊して赤を確かめた
- [ ] 段階5: 表11 のテスト。T4 を壊して赤を確かめた。`package.json` / `package-lock.json` / `tsconfig.json` の差分が想定どおり
- [ ] 段階6: YAML の説明・資料を直した。★D-182 を追記した（番号を確かめた）、または `docs/decisions.md` に他のセッションの差分が残っていたので追記せず下書きを渡した★
- [ ] 段階7: `tsc`・`lint`・`npm test`（件数の前後）・`USE_FIXTURES=true npm run build`・最後の grep・ステージングの SELECT（任意。出所つき）・ローカルDB（任意）
- [ ] §7 は直していない。§8 の画面の作業は `DsLogScreen.tsx:465` の1行以外していない
- [ ] ★push していない★
- [ ] 作業報告に: コミット一覧（hash とメッセージ）／テスト件数（前・後）／壊して確かめた結果／最後の grep の結果／ステージング・ローカルDB の確認の結果（出所）／D-182 を追記したか／本書と違う判断をしたところ（D8 の置き換え・4-5 の追加の修正・段階の分け方を含む）

---

## 付録A — D-182 の下書き（段階6-5 で `docs/decisions.md` の末尾に足す）

```markdown
### D-182 `[決定]` 削除済みの施設・清掃エリアを全画面で出さない／1行の想定外のデータで画面を止めない（データ層・API の総点検）

**日付**: 2026-10-19 / 実装: Claude（指示書BB。★実装担当はユーザー決定 2026-10-19★）

★根拠★: 監督のデータ層・API の総点検（2026-10-19。読み取りのみ・当方のコード）／ユーザー決定 2026-10-19（U1〜U4 と点検項目の扱い）。

1. ★U1: 削除済み（`deleted_at` が NULL でない）の施設・清掃エリアの行は、全画面で出さない★（★D-180 U8（エラー一覧と D-Sec アラートだけ）を全画面に広げる★）。
   対象: 清掃詳細ログ・CSV・`/clean-logs/{id}/*`（見えない行は404）・ダッシュボード（数値カード・問題ログ・直近のメタ・予定件数・要レスキュー）・アラート（事前チェック異常・出発失敗。★見えない行への `PATCH /prechecks/{id}/status` は404★）・スケジュール・
   レポート（KPI の当期・施設別パフォーマンスの当期・エラー種別内訳の確定と当期）・連続日数（L1〜L3・R3/R4・連続問題施設）・総清掃面積・エリア別完了率。
   ★規則は `lib/db/visibility.ts` の1か所★（`isVisibleFacilitySection()` が正・`FACILITY_SECTION_VISIBLE_WHERE` が写し。`isVisibleErrorRow()` はこれに委ねる）。
   ★`deleted_at` を運ばない読み取り（予定2表・スケジュール・実績・エラーのサマリ）は SQL のリレーション条件だけで外す★（`IS NULL` は SQL と JS で同じ・fixtures に削除の概念が無い）。
   ★施設名・分類 → 施設ID の解決は絞らない（罠7）★ — 解決は行の可視性と別の責務（削除済みの施設名で検索すると0件になる）。
   ★連続日数の入力からも外す★（表示から消えた行が生きている施設の連続日数を押し上げないため。値が短くなりうるのは意図した結果）。
   ★制約★: 確定期間のサマリは TeamWorks の集計値で再計算しない（D-102）— `customer_summaries` は何も外せない／`facility_summaries`・`facility_error_summaries` は施設の削除だけ外せる（清掃エリアの削除は施設の値に含まれる）。当期と確定期間で母集団が違いうる（TeamWorks への質問の候補）
2. ★U2: 有効なロボットがいない清掃エリアで落ちない★ — `serial_number` / `model_name` を `null` で返し（API を nullable に）、画面は「-」（指示書BC）・CSV は空のセル。`data.anomaly` を清掃エリアごとに1回。
   ★`robots` の並びは `id` 昇順★（意味を推測しない）。2台以上も `data.anomaly`。旧: 例外（`[db] … ロボットが見つかりません`）で一覧全体が500
3. ★U3: `robot_error_reports.status` が 0/1/2 以外の行は、一覧は外し、1件読みは404★（`isKnownRobotErrorStatus()`）。`/prechecks` は SQL と純粋関数の両方で外す。
   ★`PATCH /errors/{error_id}/status`・`PATCH /prechecks/{id}/status` は404★（★指示書AW §11 Q3 の「500（fail-loud）」を上書き★）
4. ★U4: 清掃ログの行を開くとき（`/advice`・`/areas`）の連続日数の全期間読みは直さない★ — TeamWorks に `clean_task_reports.clean_timetable_id` と `robot_error_reports.clean_timetable_id` の索引を頼んでから、単一ID の入力を「その行の施設＋日付から遡り」に絞る（指示書BB §7-1 に分析と依頼文の下書き）
5. `/ds-devices`: ★心拍がまだ無い制御盤は `last_reported_at: null` で出す★／★`controls.status` が 0/1 以外の制御盤は出さない（`summary` にも数えない。備考の保存も404）★。どちらも `data.anomaly`。旧: 例外で一覧全体が500
6. ダッシュボード直近モード: `cleanLogs()` と `cleanLogRunMeta()` の間で行が増えてメタが無い行は ★選ばずにログ★（`missingRunMetaIds()`）。メタの重複は今どおり例外
7. エリア別完了率: ★結果待ち（D-173）のエリアは `problem_count` に数えず `filter=problem` に出さない★（`total_count` には数える）。API の `AreaCompletionItem` に `result_pending`
8. 清掃詳細ログの CSV の日付は ★`YYYY/MM/DD`★（旧 `M/D`）
9. 入力の検証: ★存在しない日付（`2026-02-31`）は422★（`parseDate()` の往復）／★その月に無い週は422★（旧: 200・空）／★`/prechecks` の `date_from > date_to` は422★
10. ★`/reports/kpi` の `customer_id` と `group_id` は AND★ — 顧客がグループの傘下でなければ 200・空（★指示書AK §2-1 の決定表1行目「customer_id が勝つ」を上書き。YAML の `GroupId` に合わせた★）
11. 頑丈さ: `try` の外の例外（設定の GET 2本・アドバイスの文言）は標準の500／JSON の本文がオブジェクトでなければ422／`rule: "constructor"` は422（`Object.hasOwn`）／JSON ストアの一時ファイルは書き込みごとに別の名前／確定期間のサマリが0行なら `data.anomaly`（挙動は変えない）
12. 読みすぎ: 連続日数の入力を `facility_ids` で絞る（施設ごとに独立に数えるので値は変わらない。単一ID は U4 で据え置き）／ダッシュボードの `dsAlerts()` は `open_only: true`（バナーは未解消しか見ない）。★「1リクエストで1回」は見送り★
13. 掃除: R1 の閾値は `< level2Days`（旧 `<= 1` の直書き）／`LAUNCH_GRACE_MINUTES` は「未使用（D-091 項1 / D-173）」と明記（値は残す）／`weekStart()`・`CLEAN_ERROR_LABELS`・`cleanErrorLabel()` を削除（`CLEAN_ERROR` はシードが使うので残す）／`engines: node >=22.12`・`@types/node ^22`（動く版の下限の型）・`@next/env 16.3.3`・`tsconfig` の `exclude` に `.claude`
14. YAML の説明の食い違いを直した（`/clean-logs` の `date_from` の省略時＝全期間・`/dashboard` の `settleMinutes` の削除・`limit` の範囲 1〜200 と丸め）

#### 実装の記録

- ★2026-10-19 指示書BB（Claude）★:
  - ★コミット★: <段階ごとの hash>
  - ★テスト★: BB 前 130ファイル / 3,083件（2026-10-19・`2fc3bbb`・作成者の実測）→ BB 後 <ファイル数> / <件数>（全件通過。足した・消したテストの内訳）。壊して確かめた: <段階1〜5 の結果>
  - ★ステージングの影響件数★: <段階7-6 の結果（先方投入・2026-10-19 SELECT）。測れなかったならそう書く>
  - ★ローカルDB★: <段階7-7 の結果（当方のシード由来）。やらなかったならそう書く>
  - ★指示書と違う判断★: <あれば。無ければ「無し」>
```
