# 指示書U — 清掃結果を `clean_status_histories.clean_error` で判定する

| 項目 | 内容 |
|---|---|
| 実行環境 | ★**Cursor 内の Claude が実装します**★（`.claude/commands/d-watch.md` の役割分担表を参照） |
| 実行状態 | ★★**実行済み（2026-09-10。D-107）**★★ |
| 実行順 | ★**指示書S（D-105）と `942225a`（`isFutureSchedule()` の追加）の続き**★。★指示書M / P / R / S が作った `scripts/seed-local-db.ts` / `lib/db/prisma.ts` / `lib/db/local-guard.ts` / `lib/db/use-fixtures.ts` / `lib/db/ds-status.ts` / `lib/db/clean-log-mapping.ts` / `lib/db/precheck-mapping.ts` / `lib/prechecks/label.ts` の上に積む作業★です。★`prismaSource.facilities()` / `schedules()` / `dsDevices()` / `dsAlerts()` は1文字も変えません★（`cleanLogs()` / `prechecks()` は**本指示書の対象**です）。★触るコードが重なる未実行の指示書は、着手直前に自分で確認すること★（作成時点では、指示書T（第5歩・レポートサマリ）が `docs/decisions.md` / `docs/handoff.md` を共通で触るので、記録の段階だけは順に行うこと） |
| 取得する決定番号 | ★**着手直前に `grep -nE "^### D-(0\|1)[0-9][0-9]" docs/decisions.md \| tail -3` で最終番号を実測すること**★。★★改訂時点で別担当が `D-106`（★本指示書の判定方針を確定した決定★）を `docs/decisions.md` に追記中なので、最初の空き番号は **`D-107`** の見込み★★。★それでも**必ず自分で実測してから採番する**こと★。★★`^### D-0` だけでは3桁台（D-100 以降）を拾えない★★。★2026-09-07 に `D-068` の二重採番が実際に起きています★ |
| 前提 | ★★**D-106**★★（★**本指示書の判定方針を確定した決定**★。★別担当が改訂時点で追記中なので、着手直前に自分で行番号と内容を確認すること★。★「履歴の行が無い過去の本清掃は**出発失敗として残す**」という方針の根拠★） / ★**D-091 項1**★（`docs/decisions.md:3442`。★出発判定は DB の値で行う。猶予30分を当方で計算しない★） / ★**D-099 §1**★（`:4442`。★現行の「履歴の行が無い＝未出発」という**推測**の出所★） / ★**D-103 項1**★（`:5222`。★`launch_failed` と `departed === false` は同じ出来事。同じ純粋関数を共有する★） / ★**D-104**★（`:5335`。出発失敗の行の埋め方 ／ ★「その他」「不明」という語を使わない★） / ★**D-105**★（`:5435`。指示書S の実行記録） / **D-077**（`:1847`。★エラー値を変換しない★） / **D-089 項3**（`:3225`。★「その他」は使わない★） / **D-092**（`:3599`。1清掃 = 3行の仕組み） / **D-093**（`:3719`。`clean_statuses` の役割） / **D-094**（`:3822`。率・面積の小数化） / **D-042**（`:226`。書き込み許可4カラム） / **D-050**（`:308`。段階ごとに検証してからコミット） / **D-056**（`:353`。`schema.prisma` は生成物） / **D-026**（`:122`。連続はスケジュール存在日ベース） |
| 段階数 | ★**7段階**★（段階0〜段階6） |
| ★着手前確認★ | ★★**なし**★★ — ★判定に使う値・コード表・ラベルはすべてユーザー回答（2026-09-10）と受領済みの定義書で確定しています★。★★ユーザーの回答を待たずに段階0 から通しで実行してよい★★。★未決が1件残りますが（`clean_error` が 2/3/4 の行を**画面のどこに出すか**）、★★本指示書は「値を API まで運ぶ」ところで止めるので、実装は止まりません★★（→「申し送り」） |
| 着手前の基準 | ★**着手直前に自分で `npm test` / `npx tsc --noEmit` / `npm run lint` を実行し、その実測値を基準にすること**★（作成時点の実測は ★43ファイル / 813テスト / `tsc` 0 / `lint` 0★ だが、★鵜呑みにしない★） |
| 作成 | 2026-09-10（作成時点: 決定事項最終番号 **D-105** / `docs/handoff.md` の最新は **追記30**（`docs/handoff.md:2434`）/ HEAD `942225a`） |
| ★改訂★ | 2026-09-10（★**設計3 の方針を反転**★: 「履歴の行が無い＝除外」→ ★★「履歴の行が無い＝**出発失敗として残す**」★★。★fixtures の値は1行も変更しないことを明記★。★決定番号を `D-106` → `D-107` にずらした★。HEAD `942225a` のまま） |

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

> ★**この指示書を読んでいるあなたへ**★ — `.claude/commands/d-watch.md` の「実装は Cursor 側」は
> **2つの環境の役割分担**です。**Cursor の中で動いている Claude は実装担当**です。
> ★調査や改訂ではなく実装を行ってください。★
> ★★**うまくいかなければ途中で止めて構いません**★★ — ただし ★★段階3 はシードと読み出しを
> **同一コミット**で変えます★★（★特に**シードだけ**コミットすると `/detail-log` の完了率が壊れます★。
> → ★「作業の順序」末尾の注記★）。
> ★止めるなら段階の切れ目（コミット単位）で止めること★。

---

## ★★この指示書の核心 — 推測を1つ消す★★

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

| 箇所 | 実測内容 |
|---|---|
| ★`lib/db/clean-log-mapping.ts:80`★ | `export function departedFromHistory<T>(history: T \| undefined): boolean { return history !== undefined; }` — ★**「履歴の行が無い＝未出発」**★ |
| ★同 `:63-79` のコメント★ | ★「`clean_status`（-1/0/1/2）に『出発しなかった』を表す値が無く、行が作られないことが未実施を意味する、という D-091 項1 後段の**読み**を採っている」★ / ★「実データでどのカラムが未実施を表すかは**要確認**」★ |
| ★`lib/db/index.ts:304`★ | `departed: departedFromHistory(history)`（`cleanLogs()`） |
| ★`lib/db/index.ts:461`★ | `if (departedFromHistory(historyByTimetableId.get(key(row.id)))) continue;`（`prechecks()` の `launch_failed`） |
| ★`scripts/seed-local-db.ts:779`★ | `// ★未実施（departed=false）は clean_status_histories を作らない（設計判断1）★` — ★**シードもこの推測に合わせて作られている**★ |

★★つまり「行の有無」で判定しているのは**当方の推測**であり、根拠のあるカラムを読んでいませんでした★★
（D-099 §1 が「実データでどのカラムが未実施を表すかは依然として不明」と自ら記録しています）。

> ★★消すのは「★行が**ある**から出発した★」という推測のほう★★です。
> ★★「★行が**無い**なら出発失敗★」という側は**残します**★★ — ★ただし推測としてではなく、
> ★★「過去なのに結果行が無いのはデータ収集処理の障害だから、黙って消さずに見せる」という**決定**★★
> として残します（→ ★設計3★）。★行の有無を**両方向に**使っていたのを、片方向に絞る作業だと思ってください★。

### ★★根拠が受領済みの定義書に載っていた★★

★`docs/handoff/analysis/table-definitions-02-clean-error.md:213`★（`clean_status_histories` の
カラム6「結果」`clean_error`）の ★**原文**★:

```
0: 正常, 1: 異常(清掃未実施), 2: 異常(セキュリティ有効), 3: 異常(D-S制御盤異常), 4: 異常(清掃ロボット異常)
```

- ★同じコード表が `clean_statuses` 側（同ファイル `:181`。カラム7）にもある★。
  ★論理カラム名だけが違う（履歴側は「結果」、`clean_statuses` 側は「エラー」）★ — ★原文ママ★
- ★Prisma 上の型★: `clean_error Int @default(0) @db.UnsignedTinyInt`
  （`prisma/schema.prisma:479` が履歴側 / `:438` が `clean_statuses` 側）
- ★`unsigned tinyint` なので 0〜255 が入りうる★（→ ★コード表に無い値の扱いを決める。落とし穴3★）

### ★★ユーザーが確定させたこと（2026-09-10。この前提で実装すること）★★

| # | 確定内容 |
|---|---|
| ★1★ | ★★**清掃が実施されなかった場合も履歴の行は作成される**★★（★原文: 「書いてある以上は作成すると思うので、それを前提で実装してください」★） |
| ★2★ | ★★**出発失敗（未実施）は `clean_error = 1` のみ**★★ |
| ★3★ | ★★`2`（セキュリティ有効）/ `3`（D-S制御盤異常）は「未実施(出発失敗)とは違う扱い」★★（★原文どおり★）。★`4`（清掃ロボット異常）も同様に「未実施ではない」側★ |
| ★4★ | ★`clean_timetables` には**1週間分の予定がまとめて登録される**★ |
| ★5★ | ★**予定が無い場合は「清掃予定なし」であって異常ではない**★ |

★確定 #1 は「定義書にそう書いてあるから、そう作られるはず」という★**推論に基づく前提**★です★ —
★実データでの確認は取れていません★。★★外れたときに何が起きるかを「申し送り」に最上位で書いてあります★★。

---

## ★設計（これをそのまま実装すること）★

### 1. ★コード表は `lib/config/` の1箇所に定義する★

★`0/1/2/3/4` をロジックに直書きしないこと★（`CLAUDE.md` §7 / 禁止事項4）。
★TeamWorks の回答で分類が変わっても**1箇所の変更で済む形**にする★。

`lib/config/index.ts` に追加する（★`CLEAN_TIMETABLE_PRECHECK` の隣が自然★）:

```ts
export const CLEAN_ERROR = {
  /** 0: 正常 */
  normal: 0,
  /** ★1: 異常(清掃未実施) — 出発失敗（未実施）はこの値のみ（ユーザー回答 2026-09-10）★ */
  notExecuted: 1,
  /** 2: 異常(セキュリティ有効) — ★未実施とは違う扱い★ */
  securityEnabled: 2,
  /** 3: 異常(D-S制御盤異常) — ★未実施とは違う扱い★ */
  controlBoardError: 3,
  /** 4: 異常(清掃ロボット異常) — ★未実施とは違う扱い★ */
  robotError: 4,
} as const;
```

★ラベル表も同じ場所に置く★（コード → ★定義書の原文★）:

| コード | ★ラベル（原文ママ。★言い換え禁止★）★ |
|---|---|
| 0 | `正常` |
| 1 | `異常(清掃未実施)` |
| 2 | `異常(セキュリティ有効)` |
| 3 | `異常(D-S制御盤異常)` |
| 4 | `異常(清掃ロボット異常)` |

- ★★括弧は**半角**のまま★★（定義書の原文が半角）。★全角に直さないこと★
- ★★コード表に無い値（5以上）は `String(code)` を返す★★ — ★生値をそのまま出す★。
  ★★「その他」「不明」という語を使わないこと★★（★**D-089 項3** / **D-104 項2**★。
  「その他」は破棄済み D-067 の「未知の値をその他に寄せる」方針と紛らわしい）。
  ★前例: `MAKER_NAMES[maker_id] ?? String(maker_id)`（`lib/config/index.ts` / `lib/db/index.ts` の
  `facilities()`）と `formatModelName()`（`lib/db/clean-log-mapping.ts:27`）★
- ★ラベルは本指示書では**どの画面にも出しません**★（→ 段階4 / 申し送り）。
  ★それでも★ **ここで定義してテストで固定する**★理由★: ★後から「読みやすく」書き換えられるのを止めるため★。
  ★原文が正であることをテストで宣言しておく★（`CLAUDE.md` §6-2）

### 2. ★判定は純粋関数4本に閉じ込め、`cleanLogs()` と `prechecks()` が共有する★

★置き場所は `lib/db/clean-log-mapping.ts`★（★新設しない★ — ★D-103 が「同じ出来事は同じ判定で出す。
中身が1行でも、定義がここ1箇所であることに意味がある」と定めた場所★）。

| # | 関数 | 中身 | 使う側 |
|---|---|---|---|
| ★1★ | ★`hasCleanResult<T>(history: T \| undefined): boolean`★ | `history !== undefined` — ★★意味は「出発したか」ではなく**「清掃結果が記録されているか」**★★ | ★#2 / #3 の**内部でだけ**使う（`lib/db/index.ts` からは直接呼ばない）★ |
| ★2★ | ★`notExecutedFromHistory(history: CleanResultLike \| undefined): boolean`★ | ★★`!hasCleanResult(history) \|\| history.clean_error === CLEAN_ERROR.notExecuted`★★ — ★★行が無い場合も `true`（＝出発失敗）★★（設計3） | `cleanLogs()` / `prechecks()` |
| ★3★ | ★`completionRateFromHistory(history: (CleanResultLike & { clean_ratio: number }) \| undefined): number \| null`★ | ★行が無い → `null` / 未実施 → `null` / それ以外 → `clean_ratio` の**丸めない生値**★ | `cleanLogs()` |
| ★4★ | ★`latestHistoryByTimetableId<T extends { clean_timetable_id: bigint; ended_at: Date }>(histories: readonly T[]): Map<string, T>`★ | ★`clean_timetable_id.toString()` をキーに、★`ended_at` が最新の1件★を残す（同着は入力順の先頭）★ | `cleanLogs()` / `prechecks()` |

★★#1 は既存の `departedFromHistory()` を**改称**したもの★★（中身は同じ1行）。
★★`departed` は `!notExecutedFromHistory(history)` から作る★★ —
★「行があれば出発した」という推測を、関数名ごと消すことが本指示書の目的★。

★★#2 に「行が無い」条件を**含める**理由★★ — ★D-103 項1 が「`launch_failed` と `departed === false` は
同じ出来事。同じ純粋関数を共有する」と定めている★。★もし `departed = hasCleanResult(h) && !notExecuted(h)` の
ような **2項の AND を呼び出し側に書く**と、`cleanLogs()` と `prechecks()` の**2箇所に同じ規則が生まれる**★。
★★出発失敗の定義は `notExecutedFromHistory()` **1本だけ**にすること★★。
★帰結: `hasCleanResult()` は外から呼ばれなくなるが、★改称して残す★（★「行があれば出発した」という
推測の名前を消すこと自体が目的であり、テストで意味を固定しておく★）。

★★#4 が必要な理由（★これは見落としやすい。実測で確認済み★）★★:

| 現状 | 実測 |
|---|---|
| `cleanLogs()`（`lib/db/index.ts:221-229`） | ★`ended_at` が最新の1件を残している★ |
| `prechecks()`（`lib/db/index.ts:384-386`） | ★`new Map(histories.map(...))` = **DB返却順の後勝ち**★ |

★★「行があるか」だけを見ていた間はどちらでも同じ答えだったが、`clean_error` を読むと
**どの行を選ぶかで答えが変わる**★★。★選び方を1箇所に固定しないと `/detail-log` と `/alerts` で
出発失敗の件数が食い違う★（D-103 が禁じた状態そのもの）。

### 3. ★履歴の行がそもそも無い場合 — ★★「出発失敗として残す」★★（★決定。実装はこの通り★）

★★判定式（これが本指示書の1行まとめ）★★:

```
departed = 履歴の行がある かつ その行の clean_error !== 1
```

| 選択肢 | 内容 | 採否 |
|---|---|---|
| A | 履歴の行が無い本清掃は、`cleanLogs()` の結果に**含めない** ／ `launch_failed` にも**しない** | ★★**不採用**★★ |
| B | 行は返し、`departed: true` / `completion_rate: null` にする | ★不採用★ |
| C ✅ | ★★履歴の行が無い（かつ予定時刻を過ぎている）本清掃は、**これまでどおり「出発失敗」として残す**★★（★現行の13件が維持される★） | ★★**採用**★★ |

★★C を採る理由★★:

1. ★★**fail-silent より fail-loud を採る**★★（★これが決め手★）。
   ★ユーザー確定 #1「★未実施でも履歴の行は作成される★」を前提に置くのだから、
   ★★過去なのに履歴の行が無い ＝ **データ収集処理の障害**★★である★。
   ★★行ごと消すと（A 案）、運用担当は「何かが足りない」ことにすら気づけない★★ —
   ★画面には何も出ず、件数がそっと減るだけ★。★出発失敗として並べば、
   「翌朝確認型」の運用の中で**異常として目に入る**★（`CLAUDE.md` §1）
2. ★★現行の挙動を変えないので、「変えてはいけない数字」#2（`departed === false` が **13件**）が
   そのまま維持される★★。★A 案は 13件を 0件にしうる（しかも例外もテスト失敗も出ない）★
3. ★★B を採ると `departed: true` という**根拠の無い断定**を作ることになる★★
   （★出発したという証拠も無い★）。★D-098 / D-099 項2 が `Standby.` について
   「実際に読んだ値ではない」ことをわざわざコメントに残したのと同じ問題を、
   より目立たない形で作ってしまう★。★C は「出発した」と断定しない側に倒れている★
4. ★★不変条件 `departed === false` ⟺ `completion_rate === null` は **C でも同じように守られる**★★ —
   ★履歴の行が無い行は `completion_rate` も `null` になるため★
   （`completionRateFromHistory(undefined) === null`）。
   ★`lib/dashboard/aggregate.ts:39` / `:96`（ダッシュボードの「予定 / 実施」）と
   `isProblemCompletion()`（連続日数）がこの対応を前提にしているが、★C はこれを崩さない★
   > ★★注意（★起草時の誤りの訂正★）★★ — ★「不変条件を守るため A を採る」という論旨は**成立しない**★。
   > ★不変条件は A でも C でも守られる（崩れるのは B だけ）★。★不変条件は A の根拠にはならないので、
   > **A / C の選択理由として使わないこと**★

★★C の帰結（★段階5 で実測し、申し送りに書くこと★）★★:
★前提（未実施でも行が作られる）が外れていても、★★出発失敗が画面から消えることはない★★★ —
★★代わりに「結果がまだ書かれていないだけの清掃」まで出発失敗として出る（**過剰検知**）★★。
★★どちらに倒すかの判断として、意図的に過剰検知の側を選んでいる★★（理由1）。
★段階5 では「`clean_status_histories` の行数（253）＝ `clean_timetables` の本清掃行数（253）」を
実測し、★★差があればそれは「出発失敗として出ている行」の件数である★★ことを確認する。

### 4. ★`isFutureSchedule()` は残す（★消さないこと。★C 案の下では**必須**★）★

- ★★C 案では `isFutureSchedule()` が**必須**★★ — ★未来の予定にはまだ履歴の行が無いので、
  ★★外すと未来の清掃が**全部「出発失敗」になる**★★★
- ★★加えて、逆向きの事故も止める★★ — ★もし TeamWorks が「予定の行を作った時点で履歴の行も先に作る」
  実装だった場合、未来の予定に `clean_error = 0`（default）の履歴が付き、★未来の清掃が
  「完了率0%・×」として並ぶ★。`isFutureSchedule()` はその事故も1行で止める★
- ★`942225a`（`fix: 予定時刻が来ていない本清掃を出発失敗から除外する`）で入ったもの★。
  ★同関数の docstring（`lib/db/clean-log-mapping.ts:85-112`）にある
  「★`LAUNCH_GRACE_MINUTES` を足さない★」という注意も**そのまま残すこと**★

### 5. ★`clean_error` が 2/3/4 の行の扱い★

- ★★`departed` は `true`★★（未実施ではない。ユーザー確定 #3）。★`completion_rate` は `clean_ratio` の生値★
  → ★★つまり `/detail-log` では「出発して、その完了率で終わった清掃」として今と同じように出る★★
- ★★どこに「セキュリティ有効だった」「D-S制御盤異常だった」を出すかは**未決**★★ →
  ★本指示書は ★★「値を API まで運ぶ」ところまで★★ をやる（段階4）。★画面への出し方は別途★
- ★★推測で 2/3/4 を出発失敗やエラー種別7分類に寄せないこと★★（禁止事項5）

---

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

### 1. ★★変えてはいけない数字（★2026-09-10 実測★）★★

| # | 対象 | 値 |
|---|---|---|
| ★1★ | ★`cleanLogs()` の件数★ | ★**253**★ |
| ★2★ | ★`cleanLogs()` の `departed === false`★ | ★**13**★（★`completion_rate === null` と完全一致★。D-099 §1 で実測済み） |
| ★3★ | ★`prechecks()` の件数★ | ★**27**★（★`precheck` **14** ＋ `launch_failed` **13**★） |
| ★4★ | ★`/dashboard`★ | ★**9 / 9**★（D-099） |
| ★5★ | ★テスト★ | ★**43ファイル / 813テスト**★（★増えるのは正しい。減ってはいけない★） |
| ★6★ | `clean_timetables` の行数 | ★`precheck=0` が **253** / `precheck=1` が **253** / `precheck=2` が **253**★ |
| ★7★ | `clean_status_histories` の行数 | ★**240**★（= 253 − 13）→ ★段階3 で **253** になる★ |
| ★8★ | ★`clean_status_histories.clean_error` の分布★ | ★**全件 0**★（240行すべて。★だから今は検証できない★→ 段階3 でシードに 1/2/3/4 を足す） |

★★#1〜#4 は本指示書の前後で**変わってはいけません**★★。
★変わったら設計どおりに実装できていません★（★★特に #2（`departed === false` が 13）が
**13 以外**になるのは、シードだけを変えて読み出しを変えなかったとき★★ — ★13行が `departed: true` /
`completion_rate: 0` に化ける（落とし穴1）★）。

### 2. ★落とし穴★

| # | 落とし穴 |
|---|---|
| ★1★ | ★★**未実施行に履歴を作ると `completion_rate` が `null` → `0` に化ける**★★。★現行は `completion_rate: history ? history.clean_ratio : null`（`lib/db/index.ts:306`）★なので、★行を作った瞬間に 13行が `0`（＝「0%で実施した」）になる★。★判定（grade）は両方 × なので**画面もテストも赤くならない**★。★★`completionRateFromHistory()` に置き換えるのが必須★★ |
| ★2★ | ★★履歴を選ぶ規則が `cleanLogs()` と `prechecks()` で違う★★（設計2 の #4）。★同一 `clean_timetable_id` に複数行あると答えが分かれる★ |
| ★3★ | ★`clean_error` は `unsigned tinyint`（0〜255）★。★コード表に無い値（5以上）が来ても**例外は出ない**★。★★「★履歴の行がある場合に★未実施と判定できるのは 1 のときだけ」という形（`=== CLEAN_ERROR.notExecuted`）にしておけば、未知の値は自動的に「未実施ではない」側に落ちる★★。★逆に「`!== 0` なら異常」のような書き方をすると未知の値の扱いが変わるので**しないこと**★。★★「履歴の行が無い」ケースはこれとは**別の条件**（`!hasCleanResult(history)`）で、★そちらは未実施（出発失敗）側★★★（設計3-C） |
| ★4★ | ★`clean_status_histories.started_at` / `ended_at` は ★**NOT NULL**★（`prisma/schema.prisma:490-491`。`DateTime`、`?` が付いていない）★。★未実施行を作るとき `null` にできない★ → ★定義書の原文「★清掃スケジュールの清掃開始日時を格納する★」（`table-definitions-02-clean-error.md:217-218`）に従い、★**予定の時刻をそのまま入れる**★★ |
| ★5★ | ★★未実施行の `task_area` を 0 にすると、面積からの率の再計算が `0 / 0` になる★★（D-091 項2 が「境界の厳密さが要る場面では面積から計算する」と書いている手が使えない）。★再計算する側は**先に `clean_error` を見ること**★。★コメントに残すこと★ |
| ★6★ | ★`clean_ratio` は `Float`（`double(4,1)`。D-094 で小数化済み）★。★丸めないこと★（`CLAUDE.md` §4.3） |
| ★7★ | ★`clean_status_histories.id` は AUTO_INCREMENT ではない★（`prisma/schema.prisma:466` 付近のコメント）。★シードで明示採番する（既存の設計判断3 = 本清掃 id と同値 を崩さない）★ |
| ★8★ | ★`BigInt` → `Number()`★ を忘れると `JSON.stringify` が例外を投げる（`tsc` は捕まえない） |
| ★9★ | ★`clean_status_histories` / `clean_statuses` に `deleted_at` は無い★ → ★論理削除の絞り込みを書かない★ |

### 3. ★シードの現状（`scripts/seed-local-db.ts`。★実測して確認すること★）★

| # | 箇所 | 内容 |
|---|---|---|
| 1 | ★`:157-165`（設計判断1）★ | ★「`departed: false`（未実施）は `clean_status_histories` の行を作らない」★ — ★★本指示書で書き換える★★ |
| 2 | ★`:779`★ | `if (log.completion_rate === null) continue;` — ★★履歴も `clean_statuses` も作らずに次の行へ飛ぶ★★ |
| 3 | ★`:782-785`★ | `clean_status` は `SECURITY_DISABLED_EVERY`（=5）の剰余で `-1` / `2` を散らしている（★決定的・冪等★） |
| 4 | ★`:209-210`★ | `CLEAN_STATUS_FINISHED_SECURITY_ENABLED = 2` / `CLEAN_STATUS_FINISHED_SECURITY_DISABLED = -1`。★★`0`（清掃前）の定数は無い★★ |
| 5 | ★`:788-797`★ | `clean_area / task_area * 100 ≒ clean_ratio` になるよう `clean_area` を逆算している（`TASK_AREA_SQM = 1000`） |
| 6 | ★`:812-826`★ | `clean_statuses` は ★当日分だけ★ 作る（D-093 項1。「今の状態」用） |

★シードを触る作法★: ★`deleteMany` の逆順（`:436-437` 付近）を崩さない★ /
★2回続けて実行しても結果が同じであること（冪等）★ / ★ランダムを使わないこと★ /
★実在の住所・電話番号・社名・人名を入れないこと★（`CLAUDE.md` §7）。

---

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

| # | ★してはいけないこと★ | 理由 |
|---|---|---|
| ★1★ | ★★**`git add -A` を使うこと**★★ | `CLAUDE.md` §7。★過去に別作業の途中ファイルを巻き込む事故が起きている★。★必ずファイルパスを明示★ |
| ★2★ | ★★**`docs/handoff/管理画面_設計検討_20260910.md` に触ること**★★ | ★★**未追跡ファイル（別作業の途中）**★★。★`git status` に `??` で出るが、**本指示書のコミットに含めない・編集しない・削除しない**★ |
| ★3★ | ★★**メインDBのスキーマ変更（`prisma migrate` / `db push` / `db pull` / `prisma/schema.prisma` の手書き）**★★ | `CLAUDE.md` §7 / D-056 / D-059。★`clean_error` は**既にスキーマにある**（`prisma/schema.prisma:438` / `:479`）ので、スキーマ変更は1つも要らない★ |
| ★4★ | ★★**書き込み許可4カラム以外への DB 書き込み**★★ | D-042。★本作業の読み出しは**読むだけ**★。★書き込みは `scripts/seed-local-db.ts`（`dwatch_local` のみ）だけ★。★メインDB / `dwatch_ddl` に接続しないこと★ |
| ★5★ | ★★**コード値（0/1/2/3/4）・閾値をハードコードすること**★★ | `CLAUDE.md` §7。★`lib/config/` の `CLEAN_ERROR` から読む★。★シードも同じ定数を使う★（★「シードだから直書きでいい」は無し。分類が変われば両方直す必要がある★） |
| ★6★ | ★★**推測で分類すること**★★ | D-077 / §4.8。★7分類に寄せない★ / ★2/3/4 を出発失敗に読み替えない★ / ★ラベルを言い換えない（原文ママ）★ / ★★「その他」「不明」という語を使わない★★（D-089 項3 / D-104 項2） |
| ★7★ | ★★**`isFutureSchedule()` を消すこと・猶予を足すこと**★★ | ★設計4★。★`LAUNCH_GRACE_MINUTES` を `lib/db/` から読まないこと★（D-091 項1。★設定値を変えると過去のログの判定まで変わる★） |
| ★8★ | ★**タイムゾーン（DBがJSTかUTCか）の話に手を付けること**★ | ★★別件★★。★本指示書は `toJstIso()` の既存の使い方を1文字も変えない★ |
| ★9★ | ★**`prismaSource` の `facilities` / `schedules` / `dsDevices` / `dsAlerts` を変更すること**★ | ★指示書M / P の成果物★。★本作業が触るのは `cleanLogs` / `prechecks` の2本だけ★ |
| ★10★ | ★**サマリ系4本（`reportKpis` / `facilityPerformance` / `errorBreakdown` / `continuousProblems`）/ 点検記録2本に手を付けること**★ | ★指示書T 以降の範囲★。★`notImplemented` のまま★ |
| ★11★ | ★**`lib/db/prisma.ts` / `local-guard.ts` / `use-fixtures.ts` / `ds-status.ts` / `precheck-mapping.ts` を変更すること**★ | ★完成しており、そのまま効く★ |
| ★12★ | ★★**`lib/fixtures/` の既存の値を変更すること**★★ | ★`CLAUDE.md` §9.5★。★★許可するのは**型追従（`clean_error` の導出フィールド追加）だけ**★★。★既存の `departed` / `completion_rate` / ★`status_1h` / `status_30m`★ / 施設名 / 件数を**1つも変えないこと**★。★★特に `:139` / `:157` の `status_1h: "セキュリティ有効"` + `departed: false` は★新仕様と矛盾しない★ので、「矛盾している」と思って直さないこと★★（→ 段階4-3 の注意書き） |
| ★13★ | ★**テストを DB 依存にすること**★ | ★遅く・不安定になり、DB が空の環境で落ちる★。★テストは fixtures と純粋関数だけ★ |
| ★14★ | ★**画面・CSV に `clean_error` を出すこと**★ | ★★出し方が未決★★（設計5 / 申し送り）。★`components/features/detail-log/` と `app/api/clean-logs/export/route.ts` に差分を出さない★ |
| ★15★ | ★**`npm run dev` を Bash で起動すること**★ | ★Browser pane の `preview_start` を使う★。★無ければ curl で代替し、その旨を記録する★ |
| ★16★ | ★**`.env.local` をコミットすること / DB の認証情報を文書やコードに書くこと**★ | `CLAUDE.md` §9.3。★確認後は `USE_FIXTURES=true` に戻す★ |

---

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

★段階0（前提の確認。★読むだけ・コミットなし★）
→ 段階1（`lib/config/` のコード表）
→ 段階2（★期待値表 → テスト → 純粋関数★。呼び出し側は変えない）
→ 段階3（★★シードと読み出しを**同時に**切り替える★★）
→ 段階4（`clean_error` を API まで運ぶ）
→ 段階5（実測での検証。★コミットなし★）
→ 段階6（記録と push）★

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

> ★★**なぜ段階2 と段階3 を分けるのか**★★ — ★段階2 で関数を「足す」だけなら挙動が変わらないので、
> ★期待値表とテストを落ち着いて作れる★。★逆に、シード（書き込み側）と読み出し側は
> **同時に変えなければならない**★ — ★★シードだけ変えると、13行に `clean_error = 1` の履歴ができるのに
> 読み出しは古い `departedFromHistory()`（行があれば出発）のままなので、★13行が `departed: true` /
> `completion_rate: 0` に化ける★★★（落とし穴1）。★`tsc` / `lint` / `npm test` は通ってしまう★
> （DB を見ていないため）。
> ★参考: 逆向き（読み出しだけ変える）は、★設計3-C のおかげで件数が壊れない★（★履歴が無い13行は
> `notExecutedFromHistory()` が `true` を返すので、`departed === false` / `launch_failed` 13件が維持される★）。
> ★★それでも同一コミットにすること★★ — ★2/3/4 の検証データが揃わず、段階5 の実測が半分しかできないため★。

---

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

| # | 対象 | 見る点 |
|---|---|---|
| 1 | ★`docs/handoff/analysis/table-definitions-02-clean-error.md:170-228`★ | ★`clean_statuses`（`:181`）と `clean_status_histories`（`:213`）の `clean_error` の**原文**★。★ラベルを原文のまま写すため★ |
| 2 | ★`lib/db/clean-log-mapping.ts` 全文（197行）★ | ★`departedFromHistory()`（`:80`）の docstring が何を「推測」と認めているか★ / ★`isFutureSchedule()`（`:113`）の docstring★ |
| 3 | ★`lib/db/index.ts:193-320`（`cleanLogs()`）★ / ★`:360-506`（`prechecks()`）★ | ★履歴 Map の作り方が**2箇所で違う**こと（落とし穴2）を自分の目で確認する★ |
| 4 | ★`scripts/seed-local-db.ts:150-230` / `:770-830`★ | ★設計判断1〜4★ と ★履歴・`clean_statuses` の作り方★ |
| 5 | ★D-091 項1（`docs/decisions.md:3442`）/ D-099 §1（`:4442`）/ D-103 項1（`:5222`）★ | ★「行の有無で判定する」と決めた経緯と、そこで「要確認」と残されていたもの★ |
| 6 | ★D-104（`:5335`）★ | ★「その他」「不明」を使わない理由★ / ★出発失敗の行の埋め方（★本指示書では変えない★）★ |
| 7 | ★`lib/config/index.ts:1-80`★ | ★コード表の置き方の作法（`CLEAN_TIMETABLE_PRECHECK` / `ROBOT_STATUS_STANDBY` の docstring）★ |

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

- ★`.env.local` の `DATABASE_URL` が `dwatch_local` を指していることを確認する★。
  ★**値そのものを画面・ログ・文書に出さないこと**★（`CLAUDE.md` §9.3）
- ★`.env.local` に `ALLOW_DIRECT_ACCESS=true` があることを確認する★（D-101 項2。
  ★無いと `proxy.ts` が全ルートを止めるので段階5 で詰まる★）
- ★★着手前の基準値を自分で実測して控えた★★（`npm test` のファイル数・テスト数 / `tsc` / `lint`）
- ★★DBの現状を実測して控えた★★:

```
"/c/Program Files/MySQL/MySQL Server 8.4/bin/mysql.exe" -u root --default-character-set=utf8mb4 dwatch_local -e "
SELECT clean_error, COUNT(*) FROM clean_status_histories GROUP BY clean_error;
SELECT COUNT(*) AS main_rows FROM clean_timetables WHERE precheck = 0;
SELECT COUNT(*) AS histories FROM clean_status_histories;"
```

★作成時点の実測★: `clean_error` は ★0 が240件のみ★ / `main_rows` = ★253★ / `histories` = ★240★。

---

## 段階1 — ★`lib/config/` に `clean_error` のコード表を定義する★

### 1-1. `lib/config/index.ts`

- ★`CLEAN_ERROR`（設計1 のオブジェクト）を追加する★
- ★ラベル表を追加する★（★原文ママ。半角括弧★）。★形は既存の作法に合わせる★
  （`ROBOT_STATUS_TONES` / `MAKER_NAMES` のような `Record` か、`CLEAN_ERROR` と対になる定数）
- ★docstring に必ず書くこと★:

| # | 書く内容 |
|---|---|
| 1 | ★出所★ — ★`docs/handoff/analysis/table-definitions-02-clean-error.md:213`（履歴側）/ `:181`（`clean_statuses` 側）の原文★ |
| 2 | ★★`1`（異常(清掃未実施)）だけが出発失敗（未実施）★★（ユーザー回答 2026-09-10） |
| 3 | ★`2` / `3` / `4` は「未実施とは違う扱い」であり、★どこに出すかは未決★（申し送り） |
| 4 | ★ラベルは原文ママ。★言い換え禁止★（D-077 の思想）★ |
| 5 | ★コード表に無い値は `String(code)`。★「その他」「不明」に寄せない★（D-089 項3 / D-104 項2）★ |
| 6 | ★`unsigned tinyint`（0〜255）なので5以上が入りうる★ |
| 7 | ★1箇所に閉じ込める理由★ — ★`lib/db/`（DB→API）と `scripts/seed-local-db.ts`（fixtures→DB）の
  両方が同じ値を必要とする★（`ROBOT_STATUS_STANDBY` と同じ形） |

### 1-2. ★ラベルを固定するテスト★

- ★置き場所★: ★`lib/config/__tests__/` が無ければ新設してよい★（★既存の慣例を確認すること★）。
  ★`lib/db/__tests__/clean-log-mapping.test.ts` に混ぜないこと★（★別の関心事★）
- ★内容★: ★5つのコードに対するラベルが**定義書の原文と1文字も違わない**こと★ /
  ★未知の値（例 `5` / `255`）で `String(code)` が返ること★ /
  ★★「その他」「不明」という語がラベル表に出てこないこと★★

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

- `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス
- ★★挙動が1つも変わっていないこと★★（★まだ誰もこの定数を使っていない★）
- ★`0/1/2/3/4` の直書きが `lib/config/index.ts` 以外に増えていないこと★
- ★**ここで1コミット**★

---

## 段階2 — ★期待値表 → テスト → 純粋関数★（★この順序を崩さないこと★）

★`CLAUDE.md` §6-2 / §9.4★: ★**期待値表を先に作り、それをテストで固定してから実装する**★。
★目視確認だけで済ませないこと★。

### 2-1. ★期待値表（★これをそのままテストにすること★）★

**★`notExecutedFromHistory(history)`★** — ★★この7ケースを**必ず全部**テストにすること★★

| # | 入力 | 期待 | 意味 |
|---|---|---|---|
| 1 | `{ clean_error: 0 }` | `false` | 正常 |
| ★2★ | ★`{ clean_error: 1 }`★ | ★**`true`**★ | ★異常(清掃未実施) = 出発失敗★ |
| 3 | `{ clean_error: 2 }` | `false` | ★セキュリティ有効。未実施とは違う扱い★ |
| 4 | `{ clean_error: 3 }` | `false` | ★D-S制御盤異常。同上★ |
| 5 | `{ clean_error: 4 }` | `false` | ★清掃ロボット異常。同上★ |
| ★6★ | ★`{ clean_error: 5 }`（★コード表に無い値★）★ | `false` | ★未知の値は「未実施ではない」側に落ちる（落とし穴3）★ |
| ★★7★★ | ★★`undefined`（★履歴行なし★）★★ | ★★**`true`**★★ | ★★過去なのに結果行が無い＝データ収集処理の障害。**出発失敗として残す**（設計3-C。fail-loud）★★ |

**★`completionRateFromHistory(history)`★** — ★★同じく7ケース（`clean_error` 0 / 1 / 2 / 3 / 4 / 未知(5) / 履歴行なし）を必ず含めること★★

| # | 入力 | 期待 | 意味 |
|---|---|---|---|
| 1 | `{ clean_error: 0, clean_ratio: 92.4 }` | ★`92.4`★ | ★丸めない生値（`CLAUDE.md` §4.3）★ |
| 1' | ★`{ clean_error: 0, clean_ratio: 0 }`★ | ★**`0`**★ | ★★「0%で実施した」と「未実施」は別物。`null` にしないこと★★ |
| ★2★ | ★`{ clean_error: 1, clean_ratio: 0 }`★ | ★**`null`**★ | ★未実施（落とし穴1 の本体）★ |
| ★2'★ | ★`{ clean_error: 1, clean_ratio: 55 }`★ | ★**`null`**★ | ★矛盾したデータでも `clean_error` を正とする★ |
| 3 | `{ clean_error: 2, clean_ratio: 30.5 }` | `30.5` | ★未実施ではないので率を返す★ |
| 4 | `{ clean_error: 3, clean_ratio: 0 }` | `0` | 同上 |
| 5 | `{ clean_error: 4, clean_ratio: 61.2 }` | `61.2` | 同上 |
| ★6★ | ★`{ clean_error: 5, clean_ratio: 44.4 }`（★未知の値★）★ | ★`44.4`★ | ★未実施ではない側に落ちるので率を返す★ |
| ★★7★★ | ★★`undefined`（★履歴行なし★）★★ | ★★`null`★★ | ★結果が記録されていない★。★★これで不変条件 `departed === false` ⟺ `completion_rate === null` が
履歴行なしの場合にも保たれる★★（`notExecutedFromHistory(undefined) === true` なので `departed` も `false`） |

**★`hasCleanResult(history)`★**

| 入力 | 期待 |
|---|---|
| 履歴の行 | `true` |
| `undefined` | `false` |
| ★`map.get("存在しないキー")`★ | ★`false`★（★`Map.get` の戻りをそのまま渡す使い方を固定する★） |

**★★上の2表を `cleanLogs()` の出力に翻訳した早見表（★これも1本テストにしておくと事故に気づける★）★★**

| `clean_error` | `departed` | `completion_rate` | 不変条件 |
|---|---|---|---|
| 0 | `true` | `clean_ratio` | ✅ |
| ★1★ | ★**`false`**★ | ★**`null`**★ | ✅ |
| 2 / 3 / 4 | `true` | `clean_ratio` | ✅ |
| 未知(5) | `true` | `clean_ratio` | ✅ |
| ★★履歴行なし★★ | ★★**`false`**★★ | ★★**`null`**★★ | ✅ ★★（★設計3-C。行を消さずに出発失敗として出す★）★★ |

**★`latestHistoryByTimetableId(histories)`★**

| 入力 | 期待 |
|---|---|
| 1件 | ★キーは `clean_timetable_id.toString()`★（例 `map.get("1001")` で引ける） |
| ★同一 timetable 2件（`ended_at` が異なる）★ | ★**`ended_at` が新しい方**★ |
| ★同一 timetable 2件（`ended_at` 同着）★ | ★**入力順の先頭**★（★現行の `<` 比較の挙動を維持する★） |
| ★同一 timetable 2件で ★`clean_error` が 0 と 1★ で `ended_at` が違う★ | ★★新しい方の `clean_error` が採られる★★（★どの行を見るかで答えが変わることを固定する★） |
| 別 timetable 2件 | 2エントリ |
| 空配列 | 空の Map |

### 2-2. ★実装★

- ★`lib/db/clean-log-mapping.ts` に**追加**する★（★設計2 の #2 / #3 / #4★）。
  ★#1（`departedFromHistory` → `hasCleanResult` の改称）は**段階3 で行う**★
  （★呼び出し側と同時に変えないとビルドが通らないため★）
  - ★★この段階では #2 / #3 は**既存の `departedFromHistory()` を呼ぶ**★★（★中身は `history !== undefined` で
    同じもの★）。★段階3 の改称でそのまま `hasCleanResult()` に置き換わる★。
    ★★`history === undefined` をインラインで書かないこと★★（★改称のときに直し漏れる★）
- ★`CleanResultLike` インターフェースを定義し、docstring に出所（`prisma/schema.prisma:479`）を書く★
- ★★`0/1` を直書きせず `CLEAN_ERROR` を使う★★（禁止事項5）
- ★docstring に必ず書くこと★: ★なぜ「行の有無」ではなく `clean_error` を読むのか（D-091 項1 の解決）★ /
  ★★`undefined`（履歴行なし）を `true`（出発失敗）にする理由★★（★設計3-C。★過去なのに結果行が無いのは
  データ収集処理の障害であり、黙って消すより出発失敗として見せる（fail-loud）★）/
  ★未実施のとき `clean_ratio` を返さない理由（落とし穴1）★ /
  ★履歴を1件に絞る規則を1箇所にした理由（落とし穴2 / D-103）★

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

- `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス（★テスト数が増えていること★）
- ★★`lib/db/index.ts` に差分が無いこと★★（★まだ呼び出していない★）
- ★★挙動が1つも変わっていないこと★★ — ★`cleanLogs()` 253件 / `prechecks()` 27件は不変★
- ★**ここで1コミット**★

---

## 段階3 — ★★シードと読み出しを**同時に**切り替える（1コミット）★★

★★この段階だけは、シード（書き込み側）と `lib/db/`（読み出し側）を**同じコミット**にすること★★。
★片方だけコミットすると「変えてはいけない数字」が壊れた状態が履歴に残る★。

### 3-1. ★シード（`scripts/seed-local-db.ts`）★

| # | やること |
|---|---|
| ★1★ | ★★**未実施行（fixtures の `completion_rate === null`）にも `clean_status_histories` を作る**★★。★`:779` の `continue` を消し、履歴を作る側に回す★ |
| ★2★ | ★未実施行の `clean_error` は ★`CLEAN_ERROR.notExecuted`★（★直書きしない★）★ |
| ★3★ | ★未実施行の `clean_status` は ★`0`（清掃前）★★。★新しい定数（例 `CLEAN_STATUS_BEFORE = 0`）を `:209-210` の隣に定義し、★「★定義書の履歴側の備考には `2` / `-1` しか書かれていない（原文）ので、これは当方の暫定値★」とコメントすること★（→ 申し送り / TeamWorks 確認事項） |
| ★4★ | ★未実施行の `task_area` / `clean_area` / `clean_ratio` は ★すべて `0`★★。★理由をコメントに書く★ — ★「取得元（PUDU Cloud の clean task log）が存在しないため既定値のまま」★。★★併せて「面積から率を再計算する処理は `0 / 0` になるので、先に `clean_error` を見ること」を書く★★（落とし穴5） |
| ★5★ | ★未実施行の `started_at` / `ended_at` は ★予定の時刻をそのまま★★（NOT NULL。落とし穴4）。★定義書の原文「清掃スケジュールの清掃開始日時を格納する」を根拠としてコメントに引く★ |
| ★6★ | ★★既存の完了行に `clean_error` = 2 / 3 / 4 を散らす★★。★★`clean_ratio` / `clean_area` / `task_area` / `clean_status` は**変えないこと**★★（★変えると 253行の grade・連続日数・ダッシュボードが動く★）。★規則は剰余など**決定的なもの**にする（ランダム禁止・冪等）★。★2 / 3 / 4 が**それぞれ3件以上**になるようにする★。★13行の未実施行には当てないこと★ |
| ★7★ | ★`clean_statuses`（当日分。`:812-826`）にも**同じ `clean_error`** を入れる★。★履歴と `clean_statuses` で値が食い違わないようにする★。★当日の未実施行にも行ができるので件数が1件増える（`clean_statuses` は本作業の読み出し対象ではないので画面は変わらない）★ |
| ★8★ | ★`console.log` の完了出力に ★`clean_error` の分布★ を足す★（例 `clean_status_histories=253 (0=..., 1=13, 2=..., 3=..., 4=...)`）。★段階5 の検証が楽になる★ |
| ★9★ | ★★設計判断1（`:157-165`）のコメントを書き換える★★ — ★「行を作らないことで未実施を表す」→「`clean_error = 1` で未実施を表す」★。★★出所（定義書 `:213`）とユーザー回答（2026-09-10）を書く★★。★古い記述を残さないこと（次に読む人が両方を信じてしまう）★ |
| ★10★ | ★冪等であること（`npm run seed:local` を2回実行して結果が同じ）★ |

### 3-2. ★読み出し（`lib/db/index.ts` / `lib/db/clean-log-mapping.ts`）★

| # | やること |
|---|---|
| ★1★ | ★`departedFromHistory()` を ★`hasCleanResult()`★ に改称し、★docstring を全面的に書き換える★★ — ★★「★行が**ある**から出発した★」という**推測の記述を消す**★★（★これが本指示書で消したい推測★）。★新しい意味（結果が記録されているか）と、★D-103 の「2箇所に書かない」意図は**残す**★。★★なお「★行が**無い**なら出発失敗★」という側は**推測ではなく決定**として残る★★（設計3-C。★docstring では `notExecutedFromHistory()` を参照させる★） |
| ★2★ | ★`cleanLogs()`: 履歴 Map の生成を ★`latestHistoryByTimetableId(histories)`★ に置き換える★（★`:221-229` のインライン実装を削除。★`ended_at` 最新を採る規則とコメントは関数側へ移す★） |
| ★3★ | ★★`cleanLogs()`: `mainRows` のフィルタは ★`isFutureSchedule()` の条件のみ★。**1文字も変えない**★★ — ★★履歴の有無でフィルタを足さないこと★★（設計3-C。★履歴の行が無い過去の本清掃も**返す**。返さないと出発失敗が消える★）。★`isFutureSchedule()` が必須である理由（未来の予定は履歴が無いので、外さないと全部出発失敗になる）をコメントに書く★（設計4） |
| ★4★ | ★`cleanLogs()`: `departed: !notExecutedFromHistory(history)`★ |
| ★5★ | ★`cleanLogs()`: `completion_rate: completionRateFromHistory(history)`★（★落とし穴1★） |
| ★6★ | ★`prechecks()`: 履歴 Map の生成を ★同じ `latestHistoryByTimetableId()`★ に置き換える★（★`new Map(histories.map(...))` をやめる。落とし穴2★） |
| ★7★ | ★`prechecks()` の `launch_failed` ループの条件を ★★①未来ならスキップ（`isFutureSchedule`。★残す。必須★）→ ②`notExecutedFromHistory()` が真なら出す★★ の**2段だけ**にする★。★★「履歴が無ければスキップ」は**入れないこと**★★（設計3-C。★履歴が無い行は `notExecutedFromHistory()` が `true` を返すので、そのまま `launch_failed` になる★） |
| ★8★ | ★★「本清掃行を指すエラー行を `launch_failed` に読み替えない」という現行の扱いを変えないこと★★（D-104。`lib/db/index.ts:402-406`） |
| ★9★ | ★既存テスト `lib/db/__tests__/clean-log-mapping.test.ts:86-105`（`departedFromHistory` の3件）と `:111` / `:162`（`isFutureSchedule` のテストが同関数を使っている）を追従させる★。★★改称（`hasCleanResult`）に合わせるだけで、**期待値は変わらない**★★（`undefined` → `false` のまま。★意味が「未出発か」から「結果が記録されているか」に変わる点だけ、テスト名と説明文を直す★）。★★「履歴の行が無い＝出発失敗」という**結果**も変わらない★★ — ★ただしその判定を担う関数が `notExecutedFromHistory()`（`undefined` → `true`）に移ることに注意★。★行番号は自分で取り直すこと★ |

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

- `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス
- ★`npm run seed:local` を2回実行して結果が同じ（★出力の件数が一致★）★
- ★★DB の実測★★:

```
"/c/Program Files/MySQL/MySQL Server 8.4/bin/mysql.exe" -u root --default-character-set=utf8mb4 dwatch_local -e "
SELECT clean_error, COUNT(*) FROM clean_status_histories GROUP BY clean_error ORDER BY clean_error;
SELECT COUNT(*) AS histories FROM clean_status_histories;"
```

  ★期待★: ★`histories` = **253**★（240 → 253）/ ★`clean_error = 1` が **13件**★ /
  ★`2` / `3` / `4` がそれぞれ **3件以上**★ / ★合計が 253★
- ★`grep -n "clean_error" scripts/seed-local-db.ts lib/db/*.ts` で、★数値の直書きが無いこと★を確認した
- ★**ここで1コミット**★（★シードと読み出しを同じコミットに入れたこと★）

---

## 段階4 — ★`clean_error` を API まで運ぶ（画面には出さない）★

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

### 4-1. `d-watch-api.yaml`（★`CleanLogItem` は `:1118-1153`★。★行番号は自分で取り直すこと★）

- ★`clean_error` を追加★:
  - ★★`{ type: integer, nullable: true, example: 0 }`★★ — ★★`nullable` を忘れないこと★★
    （★設計3-C により、★履歴（`clean_status_histories`）の行が無い過去の本清掃も**返す**★ため。
    ★その行には読むべき `clean_error` が存在しない★）
  - ★`description` に**定義書の原文のコード表**を書く★（`0:正常 / 1:異常(清掃未実施) / 2:異常(セキュリティ有効) / 3:異常(D-S制御盤異常) / 4:異常(清掃ロボット異常)`）
  - ★「★値は変換しない生値★」「★★`1` **または** `null`（履歴の行が無い）のとき `departed=false` / `completion_rate=null` になる★★」を書く
  - ★`null` の意味も書く★: ★「★履歴の行が無い（＝清掃結果が記録されていない）★。★出発失敗として扱う★」★
- ★★`facility_id` も追加する★★ — ★`lib/types/index.ts:59` にはあるのに YAML に無い（実測）★。
  ★同じスキーマブロックを触るので、契約と実装の乖離をここで閉じる★（`CLAUDE.md` §6-1。
  ★指示書S が `PrecheckItem` で同じ抜けを直したのと同じ★）
- ★`completion_rate` の `description`（`:1138`「未出発時はnull」）は ★★「未実施(`clean_error=1`)、または履歴の行が無い(`clean_error=null`)ときnull」に直す★★
- ★`departed` の `description`（`:1132`）にも ★根拠が `clean_error` であること★ を書く

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

- ★★`CleanLogItem` に `clean_error: number | null;` を追加★★（★★**nullable にする**★★ —
  ★設計3-C により、★履歴の行が無い過去の本清掃も返す★ため。★その行の `clean_error` は `null`★。
  ★★`null` の行は `departed: false` / `completion_rate: null`（＝出発失敗）である★★ことをコメントに書く）
- ★`completion_rate` / `departed` のコメントも `clean_error` 基準に直す★
  （★「`clean_error === 1` **または** `clean_error === null` のとき `departed === false`」★）

### 4-3. `lib/fixtures/clean-logs.ts`

> ★★**最初に読むこと — fixtures の既存の値は1行も変更しません**★★
> ★この段階でやるのは ★`row()` に導出フィールドを1つ足す**型追従だけ**★ です★。
> ★★既存の行の `departed` / `completion_rate` / `status_1h` / `status_30m` / 施設名 / 件数を
> **1つも書き換えないこと**★★（禁止事項12）。

- ★`row()`（`:44`）で ★`clean_error` を `departed` から導出する★★:
  ★`departed ? CLEAN_ERROR.normal : CLEAN_ERROR.notExecuted`★（★数値を直書きしない★）
- ★コメントに書くこと★: ★★fixtures では `departed` が入力で `clean_error` が導出、
  DB 経由では `clean_error` が正で `departed` が導出（★向きが逆★）★★
- ★★fixtures に 2/3/4 の行は足さない★★ — ★画面への出し方が未決なので、
  検証すべき対象がまだ無い★。★2/3/4 はシード（段階3）だけで検証する★。
  ★★帰結: `USE_FIXTURES` の true / false で `clean_error` の分布が違う（**想定内**。段階6 で記録する）★★
- ★fixtures は `departed` が入力なので、★`clean_error` が `null` になる行は fixtures には存在しない★
  （`null` は DB 経由でだけ出る）。★型は `number | null` だが fixtures 側は常に `number`★

#### ★★注意 — 「`status_1h: "セキュリティ有効"` + `departed: false` は新仕様と矛盾する」は**誤り**★★

★★`lib/fixtures/clean-logs.ts:139` と `:157` を「矛盾しているから直す」と考えないこと★★。
★★矛盾していません。直す必要はありません★★。

| カラム | 何を表すか | 出所 |
|---|---|---|
| ★`status_1h` / `status_30m`★ | ★★**事前チェック時点（60分前 / 30分前）のロボットの状態**★★ | ★`robot_error_reports.error_type` 由来★ |
| ★`clean_error`★ | ★★**その清掃の結果**★★ | ★`clean_status_histories` 由来★ |

★★**別のカラム・別の時点の話**なので、
「60分前にセキュリティ有効だった → 結果その清掃は実施されなかった（`clean_error = 1`）」は
新仕様の下でも**完全に成立する組み合わせ**★★です。
★むしろ因果としては自然な並び★（★セキュリティが有効なままだったので出発できなかった★）。

★★この誤解は起草時に実際に発生しました★★ — ★同じ勘違いで fixtures を書き換えないよう、
この注意書きを消さないこと★。

### 4-4. `prismaSource.cleanLogs()`

- ★★`clean_error: history?.clean_error ?? null`★★（★生値をそのまま。★履歴の行が無ければ `null`★）★を draft に足す
- ★★`??` を `||` にしないこと★★ — ★`clean_error` は `0`（正常）が正当な値なので、`||` だと **0 が null に化ける**★
  （★`tsc` も `lint` も捕まえない。段階5 の「`clean_error` の分布が DB と完全一致」で初めて分かる★）

### 4-5. ★型追従が必要な箇所（★自分で grep し直すこと★）★

| # | 箇所 | 内容 |
|---|---|---|
| 1 | ★`lib/clean-logs/__tests__/query.test.ts:6`★ | `row(overrides: Partial<CleanLogItem>): CleanLogItem` の土台オブジェクト |
| 2 | ★`lib/dashboard/__tests__/aggregate.test.ts:24`★ | `CleanLogItem` を組み立てるヘルパー |
| 3 | ★`lib/api/__tests__/client.test.ts:35`★ | `ListResponse<CleanLogItem>` の本文 |
| ★4★ | ★`app/api/clean-logs/export/route.ts`（`toCsv()`。`:25`）★ | ★★**列を足さない**★★（禁止事項14）。★差分が無いことを確認するだけ★ |
| ★5★ | ★`components/features/detail-log/CleanLogTable.tsx`★ | ★★**差分なし**★★（禁止事項14） |

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

- `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス
- ★`d-watch-api.yaml` と `lib/types/index.ts` の内容が一致していること★（★型 / nullable / description★）
- ★★`components/` と `app/api/clean-logs/export/route.ts` に差分が無いこと★★
- ★`cleanLogs()` 253件 / `prechecks()` 27件が不変（★段階5 で実測する★）★
- ★**ここで1コミット**★

---

## 段階5 — ★実測での検証（★コミットなし★）★

### 5-1. ★DB 側★

```
"/c/Program Files/MySQL/MySQL Server 8.4/bin/mysql.exe" -u root --default-character-set=utf8mb4 dwatch_local -e "
SELECT clean_error, COUNT(*) FROM clean_status_histories GROUP BY clean_error ORDER BY clean_error;
SELECT COUNT(*) AS main_rows FROM clean_timetables WHERE precheck = 0;
SELECT COUNT(*) AS histories FROM clean_status_histories;
SELECT clean_error, COUNT(*) FROM clean_statuses GROUP BY clean_error ORDER BY clean_error;"
```

★確認する点★:

| # | 項目 |
|---|---|
| 1 | ★`main_rows`（253）と `histories`（253）が ★**一致**★ すること★ — ★★これは「段階3 のシードが未実施13行にも履歴を作れたか」の確認★★。★★一致しない場合、差分の行は `cleanLogs()` から**落ちるのではなく**、★「出発失敗」として画面に出る★★★（設計3-C。★だから `departed === false` の件数が 13 を超える★）。★★件数が減っていないことも併せて見ること★★ |
| 2 | ★`clean_error = 1` が **13件**★（★「変えてはいけない数字」#2 と同じ★） |
| 3 | ★`2` / `3` / `4` がそれぞれ3件以上あること★ |
| 4 | ★`clean_statuses` 側の `clean_error` が履歴と食い違っていないこと★ |

### 5-2. ★API 側（`USE_FIXTURES=false`）★

★★`.env.local` を書き換えず、コマンドラインで env を渡すこと★★（★戻し忘れを防ぐ★）:

```
USE_FIXTURES=false DATABASE_URL="mysql://root@localhost:3306/dwatch_local" npx tsx -e "
import { db } from './lib/db';
const logs = await db.cleanLogs();
const pre = await db.prechecks();
const count = (rows, f) => rows.filter(f).length;
console.log('cleanLogs', logs.length);
console.log('departed=false', count(logs, r => !r.departed));
console.log('rate=null', count(logs, r => r.completion_rate === null));
console.log('不一致(departed=false と rate=null がズレた行)', count(logs, r => !r.departed !== (r.completion_rate === null)));
console.log('clean_error 分布', JSON.stringify(logs.reduce((a, r) => (a[String(r.clean_error)] = (a[String(r.clean_error)] ?? 0) + 1, a), {})));
console.log('clean_error=1 の departed', [...new Set(logs.filter(r => r.clean_error === 1).map(r => r.departed))]);
console.log('clean_error=null の件数', count(logs, r => r.clean_error === null));
console.log('clean_error=null の departed', [...new Set(logs.filter(r => r.clean_error === null).map(r => r.departed))]);
console.log('clean_error=2/3/4 の rate が非null', count(logs, r => [2,3,4].includes(r.clean_error) && r.completion_rate !== null));
console.log('prechecks', pre.length, 'launch_failed', count(pre, p => p.severity === 'launch_failed'));
"
```

★★期待値（★1つでも外れたら実装が違う★）★★:

| # | 項目 | 期待 |
|---|---|---|
| ★1★ | `cleanLogs()` の件数 | ★**253**★ |
| ★2★ | `departed === false` | ★**13**★ |
| ★3★ | `completion_rate === null` | ★**13**★ |
| ★4★ | ★`departed === false` と `completion_rate === null` のズレ★ | ★**0**★（★落とし穴1 を踏んでいないことの証明★） |
| ★5★ | `clean_error` の分布 | ★DB の分布と**完全一致**（変換していないことの証明）★。★★`0` のバケツが `null` に化けていないこと（`??` を `||` にすると化ける）★★ |
| ★6★ | `clean_error === 1` の `departed` | ★**`[false]` だけ**★ |
| ★★7★★ | ★★`clean_error === null` の件数★★ | ★★**0件**★★（★シードが253行すべてに履歴を作っているため★）。★★もし 0 でなければ、それは「履歴の行が無い過去の本清掃」であり、★出発失敗として出ている★（設計3-C。★消えていないことが正しい★）。その場合 `departed` は ★**`[false]` だけ**★ であることを確認する★★ |
| ★8★ | `clean_error` が 2/3/4 の行の `completion_rate` | ★**全件 非null**★（★未実施扱いになっていないこと★） |
| ★9★ | `prechecks()` | ★**27件 / `launch_failed` 13件**★ |

★`npx tsx` でエイリアス（`@/`）が解決できない場合★は ★dev サーバ＋curl に切り替えてよい★
（`/api/clean-logs?per_page=300` / `/api/prechecks`）。★★その場合も上の9項目を実測すること★★。

### 5-3. ★画面（回帰していないこと）★

| 画面 | 期待 |
|---|---|
| ★`/detail-log`★ | ★200 / **253件**★。★✗（未出発）が **13件**★ |
| ★`/dashboard`★ | ★200 / **9 / 9**★（D-099） |
| ★`/alerts`★ | ★200 / **27件**（`launch_failed` 13）★。★「出発失敗」が出ていること★ |
| `/facilities` / `/schedules` / `/ds-logs` / `/ds-alerts` | ★200。回帰していないこと★ |
| `/reports` | ★500（第5歩・指示書T の範囲）★ |

- ★エラーメッセージが `[db] xxx のPrisma実装はまだありません。…` であること★（`lib/db/index.ts:164`）
- ★★**404 が出たら Edge Runtime バンドル破壊の回帰を疑うこと**★★（D-086）。
  ★500 = 想定どおりの未実装 / 404 = ビルドが壊れている★
- ★Browser pane（`preview_start`）が使えなければ curl で代替し、★目視できなかった項目を必ず記録する★★

### 5-4. ★`USE_FIXTURES=true` での確認★

- ★`.env.local` を触っていないなら、そのまま `USE_FIXTURES=true` で dev サーバを起動するだけ★
- ★`/detail-log` / `/dashboard` / `/alerts` が★従来どおり★動くこと★
- ★★fixtures 側の件数・判定が1つも変わっていないこと★★（禁止事項12）

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

- ★5-1 の4項目・5-2 の9項目をすべて実測した★
- ★5-3 の回帰確認をした（できなかった項目は理由を控えた）★
- ★★`.env.local` が `USE_FIXTURES=true` のままであることを確認した★★

---

## 段階6 — 記録と push

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

★**着手直前に `grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md | tail -3` で最終番号を実測すること**★
（★`^### D-0` だけでは3桁台を拾えない★。★作成時点の最終は `D-105`。改訂時点で別担当が `D-106`
（本指示書の判定方針を確定した決定）を追記中なので、★★`D-107` の見込み★★★）。

★記録すること★:

| # | 内容 |
|---|---|
| ★1★ | ★★`clean_error` のコード表を根拠に判定を切り替えたこと★★ — ★出所（定義書 `:213`）とユーザー回答（2026-09-10）の原文★。★★D-091 項1 / D-099 §1 の「実データでどのカラムが未実施を表すかは要確認」が**定義書ベースで解決した**（実データでの確認は未了）★★ |
| ★2★ | ★コード表を `lib/config/` の1箇所に置いたこと★と、★ラベルを原文ママにした理由★ |
| ★3★ | ★★履歴の行が無い過去の本清掃を、これまでどおり「出発失敗」として残した判断（設計3 の **C案**）★★と、★不採用にした A案（行ごと除外）/ B案（`departed: true`）とその理由★。★★**fail-silent より fail-loud を採った**こと★★（★行ごと消すと運用担当が「何かが足りない」ことにすら気づけない★）。★★併せて「不変条件 `departed === false` ⟺ `completion_rate === null` は A でも C でも守られるので、A案の根拠にはならない」ことも書く★★（★起草時にこれを A案の根拠にしていた。同じ誤りを繰り返させないため★） |
| ★4★ | ★★`isFutureSchedule()` が **C案の下では必須**であること★★（★未来の予定には履歴の行が無いので、外すと未来の清掃が全部「出発失敗」になる★）。★加えて、逆向き（未来に `clean_error=0` の履歴が先に作られる）の事故も止める二重の役割★ |
| ★5★ | ★★履歴を1件に絞る規則を `latestHistoryByTimetableId()` に統一したこと★★ — ★`cleanLogs()` と `prechecks()` で規則が違っていた（実測）事実も書く★ |
| ★6★ | ★★`completion_rate` が `null` → `0` に化ける落とし穴（落とし穴1）と、それをどう防いだか★★ |
| ★7★ | ★シードに足したもの★（未実施13行の履歴 / 2/3/4 の分布 / `clean_status = 0` / 面積を0にした理由） |
| ★8★ | ★★シードの `clean_status = 0` が当方の暫定値であること★★（★定義書の履歴側の備考は `2` / `-1` のみ★） |
| ★9★ | ★API に `clean_error` を足したこと★と、★画面・CSV には出していないこと（未決）★。★★`nullable` にした理由（設計3-C により履歴の行が無い過去の本清掃も返すため）★★ |
| ★10★ | ★`USE_FIXTURES` の true / false で `clean_error` の分布が違うこと（想定内）★。★fixtures には `clean_error = null` の行が存在しない（`departed` が入力なので）ことも書く★ |
| ★★10'★★ | ★★`lib/fixtures/` の**既存の値は1行も変更していない**こと★★と、★★その理由★★ — ★`status_1h` / `status_30m`（事前チェック時点のロボットの状態。`robot_error_reports.error_type` 由来）と `clean_error`（清掃の結果。`clean_status_histories` 由来）は**別のカラム**なので、★`status_1h: "セキュリティ有効"` + `departed: false`（`lib/fixtures/clean-logs.ts:139` / `:157`）は新仕様と**矛盾しない**★。★起草時に「矛盾する」と誤認した経緯ごと記録し、次に読む人が fixtures を書き換えないようにする★ |
| ★11★ | ★★TeamWorks への確認事項（下記「申し送り」の表をそのまま）★★。★第8回の確認書の候補として記録する★ |
| ★12★ | ★実測値★（`npm test` のファイル数・テスト数 / `tsc` / `lint` / `cleanLogs()` / `prechecks()` の件数。★着手前の基準値との差★） |
| ★13★ | ★段階5 で確認できなかった項目があれば、その理由と限界★ |

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

- ★最新は 追記30（`docs/handoff.md:2434`）★。次は ★追記31★ の見込み。★着手直前に自分で確認すること★
- ★★冒頭（`docs/handoff.md:6-7`）の「最新の追記」が **追記29 のまま**（実測）★★ →
  ★★自分の追記に合わせて直すこと★★
- ★「次に触る人が最初に読むべき点」に必ず書くこと★:
  1. ★★出発失敗の判定は ★`clean_error = 1`★ **または** ★履歴の行が無い★ になった★★
     （★「行があれば出発した」という推測は消えた。★「行が無ければ出発失敗」は**決定として残した**★★）
  2. ★★履歴の行が無い過去の清掃は「出発失敗」として**画面に出る**★★ —
     ★★消さない★★（★データ収集処理の障害を運用担当に見せるため。fail-loud）★。
     ★帰結: 結果がまだ書かれていないだけの行も出発失敗として出る（★過剰検知は承知のうえ★）★
  3. ★★`isFutureSchedule()` は**必須**。消さないこと★★（★消すと未来の清掃が全部「出発失敗」になる★）
  4. ★★`lib/fixtures/` の `status_1h: "セキュリティ有効"` + `departed: false` は新仕様と**矛盾しない**★★
     （★別カラム・別時点。fixtures は書き換えていない★）
- ★「次にやること」の ★「`departed` を実データでどう判定するかの確認」（追記30 の4番）★ を
  ★★本作業の内容に更新する★★（★カラムは決まった。残るのは「未実施でも行が作られるか」の実データ確認★）

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

- ★`git add` は必ずファイルパスを明示。`git add -A` は禁止★（禁止事項1）
- ★★`docs/handoff/管理画面_設計検討_20260910.md` をコミットに含めないこと★★（禁止事項2）
- ★`.env.local` をコミットしないこと★（禁止事項16）
- `git push origin develop`（★`main` へは push しない★）
- ★完了条件★: `docs/decisions.md` に決定番号を追記した（★上記13項目すべて★）/
  `docs/handoff.md` に追記し★冒頭の最新番号も直した★ / ★`origin/develop` へ push 済み★ /
  ★文書末尾の「実行状態」表を更新した★

---

## ★★申し送り★★

### 1. ★未決（★本指示書では実装しない★）★

| # | 未決事項 | 状態 |
|---|---|---|
| ★1★ | ★★`clean_error` が **2 / 3 / 4** の行を**どこに・どう出すか**★★（`/detail-log` の列を増やす / `/alerts` に種別を足す / 別画面 など） | ★★未決★★。★本指示書は「値を API まで運ぶ」ところで止めている★。★★着手前確認にはしていない（実装を止めないため）★★。★出し方を決めるときは、`lib/config/` のラベル表（段階1）をそのまま使えば原文が保たれる★ |
| ★2★ | ★`2`（セキュリティ有効）は D-091 項3 の「セキュリティ無効はアラートにしない」という内部調整と関係しうる★ | ★★D-091 項3 の結果待ち★★。★先に決めないこと★ |
| ★3★ | ★出発失敗の行に対応状況（`status` / `comment` / `responded_at` / `resolved_at`）の置き場が無い★ | ★D-104 項3 のまま（★変わっていない★）。★`clean_error = 1` の行には `robot_error_reports` の行が無い★ |

### 2. ★★TeamWorks への確認事項（★第8回の確認書の候補★）★★

| # | 確認したいこと | なぜ重要か |
|---|---|---|
| ★1★ | ★★**清掃が実施されなかった場合も `clean_status_histories` の行は作成されますか**★★（`clean_error = 1` の行） | ★★**本指示書の設計の土台**★★。★★前提が外れても出発失敗が消えることはない（設計3-C）★★が、★代わりに「★結果がまだ書かれていないだけの行★」まで出発失敗として出る（★**過剰検知**★）★。★fail-loud に倒す判断をしているので、実データでどちらの挙動になるかを確認したい★。★★最優先★★ |
| ★2★ | ★未実施の行の `clean_status` には何が入りますか★ | ★定義書の `clean_status_histories` 側の備考は ★`2` / `-1` のみ★（`:212`。原文）。★`0`（清掃前）を使ってよいのか不明★。★当方のシードは暫定で `0` にしている★ |
| ★3★ | ★★未実施の行の `task_area` / `clean_area` は 0 ですか。それとも「予定していた清掃対象面積」が入りますか★★ | ★★`CLAUDE.md` §4.2 の清掃完了率（Σ清掃面積 ÷ Σ清掃対象面積）の**分母が変わる**★★。★サマリ集計は TeamWorks 実装なので、当方の再計算値と一致するかに直結する★ |
| ★4★ | ★`clean_error` が 2 / 3 / 4 のとき、`clean_ratio` / 面積には何が入りますか★（途中まで清掃した値か、0か） | ★`/detail-log` の完了率と判定（◎○△×）の意味が変わる★ |
| ★5★ | ★1回の清掃に複数の異常が同時に起きた場合、`clean_error` はどれになりますか★（優先順位はあるか） | ★1カラムに1値しか入らないため★ |
| ★6★ | ★`clean_error` に **5以上**の値が入る可能性はありますか★ | ★`unsigned tinyint`（0〜255）★。★当方は「1 以外は未実施ではない」として扱っている★ |
| ★7★ | ★`clean_statuses.clean_error`（論理名「エラー」）と `clean_status_histories.clean_error`（論理名「結果」）の使い分けは何ですか★ | ★同じコード表だが論理名が違う（原文）★。★当方は履歴側だけを読んでいる★ |
| ★8★ | ★1つの `clean_timetable_id` に対し `clean_status_histories` は必ず1行ですか★ | ★複数あると `ended_at` 最新の1件を採る規則（本指示書で統一）が効く。★その規則が妥当かの裏取りが要る★★ |

### 3. ★本指示書では触らないもの★

| 対象 | いつ |
|---|---|
| ★タイムゾーン（DBがJSTかUTCか未確定）★ | ★★別件★★（禁止事項8） |
| ★サマリ系4本（`reportKpis` / `facilityPerformance` / `errorBreakdown` / `continuousProblems`）★ | ★指示書T（第5歩）★ |
| ★点検記録2本（`inspections` / `inspectionDetail`）★ | ★最後（D-072 項7）★ |
| ★`dsAlerts` の `security_off` 側★ | ★D-091 項3 の内部調整の結果待ち★ |
| ★`PATCH /prechecks/{id}/status` の API ルート実装★ | ★別の指示書★ |
| ★`docs/decisions.md` の既存記述の整理（D-099 §1 の「要確認」の書き換え等）★ | ★★本指示書は**追記のみ**★★。★既存の決定文を書き換えないこと★ |

---

## 完了条件（全体）

1. ★★着手前確認なし。段階0 から通しで実行した★★
2. 段階0〜6 をすべて実施した（★途中で止めた場合は、どこで止めたかを実行状態表に書くこと★）
3. `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス（★813テストから増えていること★）
4. ★★`clean_error` のコード表とラベルが `lib/config/` の1箇所にあり、`0/1/2/3/4` が他のどこにも直書きされていないこと★★
5. ★★ラベルが定義書の原文と1文字も違わないこと（半角括弧を含む）／「その他」「不明」という語が無いこと★★
6. ★★出発失敗の判定が ★「履歴の行が無い」**または** `clean_error === CLEAN_ERROR.notExecuted`★ になり、
   `cleanLogs()` と `prechecks()` が**同じ純粋関数**（`notExecutedFromHistory()`）を共有していること★★
   （★2項の AND を呼び出し側に書いて規則を2箇所に散らしていないこと★）
6'. ★★`cleanLogs()` の `mainRows` フィルタに「履歴があること」の条件が**足されていない**こと★★
   （★履歴の行が無い過去の本清掃も返る＝出発失敗として出る。設計3-C★）
7. ★★履歴を1件に絞る規則が1箇所（`latestHistoryByTimetableId()`）に統一されていること★★
8. ★★`isFutureSchedule()` が残っていること★★（★呼び出し2箇所ともそのまま★）
9. ★★実測: `cleanLogs()` **253件** / `departed === false` **13件** / `completion_rate === null` **13件**（ズレ0）/ `prechecks()` **27件**（`launch_failed` 13）/ `/dashboard` **9 / 9**★★
10. ★★シードに `clean_error` = 1（13件）と 2 / 3 / 4（各3件以上）が入り、`clean_status_histories` が **253行**になっていること★★／★2回実行して同じ結果になること★
11. ★`d-watch-api.yaml` の `CleanLogItem` に `clean_error`（★`nullable: true`★）と `facility_id` が定義され、
    `lib/types/index.ts`（★`clean_error: number | null`★）と一致していること★
11'. ★★`lib/fixtures/` の**既存の値に差分が無い**こと★★（★`row()` への導出フィールド追加だけ★。
    ★`git diff lib/fixtures/` で `departed` / `completion_rate` / `status_1h` / `status_30m` / 施設名 / 行数が
    1つも変わっていないことを確認する★）
12. ★★`components/` / `app/api/clean-logs/export/route.ts` / `prisma/schema.prisma` / `lib/db/prisma.ts` / `local-guard.ts` / `use-fixtures.ts` / `ds-status.ts` / `precheck-mapping.ts` / `prismaSource` の `facilities` / `schedules` / `dsDevices` / `dsAlerts` / サマリ系4本 に差分が無いこと★★
13. ★`docs/handoff/管理画面_設計検討_20260910.md` が未追跡のまま残っていること（コミットに入っていない）★
14. `docs/decisions.md` / `docs/handoff.md`（★冒頭の最新番号も★）の更新 / ★**`origin/develop` へ push 済み**★ /
    ★文書末尾の「実行状態」表を更新した★

---

## 実行状態

| 段階 | 状態 | コミット |
|---|---|---|
| ★**着手前の確認**★ | ★★**なし**★★ — ★判定に使う値（`clean_error = 1`）・コード表・ラベルはすべて確定済み★。★未決1件（2/3/4 の出し方）は「値を API まで運ぶ」ところで止めることで回避★ | — |
| 段階0 前提の確認 | ★★**完了**★★ | コミットなし |
| 段階1 `lib/config/` のコード表 | ★★**完了**★★ | `d744285` |
| 段階2 期待値表 → テスト → 純粋関数 | ★★**完了**★★ | `703690b` |
| 段階3 ★シードと読み出しを同時に切り替え★ | ★★**完了**★★（★同一コミット★） | `3bbd504` |
| 段階4 `clean_error` を API まで運ぶ | ★★**完了**★★ | `6dfd0b8` |
| 段階5 実測での検証 | ★★**完了**★★（★5-1 の4項目 / 5-2 の9項目とも期待どおり★） | コミットなし |
| 段階6 記録と push | ★★**完了**★★ | 本記録のコミット |

**実行日**: ★★2026-09-10★★ / **取得した決定番号**: ★★`D-107`★★（実測で最終番号が `D-106` であることを確認して採番）
**実測値（着手前 / 完了後）**: ★43ファイル / 813テスト / `tsc` 0 / `lint` 0★ → ★★44ファイル / 862テスト / `tsc` 0 / `lint` 0★★ ／ ★`cleanLogs()` 253件（`departed === false` 13 / `completion_rate === null` 13 / ズレ0）／ `prechecks()` 27件（`launch_failed` 13）／ `/dashboard` 9 / 9 ／ `clean_status_histories` 253行（`clean_error` 0=225 / 1=13 / 2=5 / 3=5 / 4=5）★
**★決めたこと（実行前に確定済み）★**: ★★`departed = 履歴の行がある かつ その行の clean_error !== 1`★★ /
出発失敗（未実施）の判定 = ★`clean_error === 1`★ **または** ★履歴の行が無い★ /
履歴の行が無い場合 = ★★これまでどおり「出発失敗」として残す（★行ごと除外しない★。現行の13件を維持。fail-loud）★★ /
`isFutureSchedule()` = ★残す（★C 案の下では**必須**★）★ / コード表の置き場所 = ★`lib/config/index.ts` の1箇所★ /
ラベル = ★定義書の原文ママ（未知の値は `String(code)`。「その他」「不明」は使わない）★ /
`clean_error` = ★2/3/4 も含めて API まで運ぶ（★`nullable`★。画面・CSV には出さない）★ /
`lib/fixtures/` = ★★既存の値は1行も変更しない（型追従の導出フィールド追加のみ）★★
**★改訂で反転した点（起草版との差）★**: ★起草版は「履歴の行が無い＝除外」だったが、★★「出発失敗として残す」に反転★★★ /
★起草版が持っていた「fixtures が新仕様と矛盾する」という前提は★**誤り**★として撤回（fixtures は無変更）★ /
★決定番号を `D-106` → `D-107` にずらした★
**詳細**: `docs/decisions.md` の該当番号を参照
