# 指示書V — 連続問題日数を施設単位に揃え、当期KPIの分母から未来の予定を外す

| 項目 | 内容 |
|---|---|
| 実行環境 | ★**Cursor 内の Claude が実装します**★（`.claude/commands/d-watch.md` の役割分担表を参照） |
| 実行状態 | ★**完了（D-110。2026-09-11）**★ |
| 実行順 | ★**指示書T（D-108。`327682e`）完了後のレビューで出た不具合2件 + それに伴うユーザー確定5件の修正**★。★指示書M / P / R / S / T / U が作った `lib/db/clean-log-mapping.ts` / `lib/db/report-mapping.ts` / `lib/db/index.ts` の上に積む作業★です。★★`prisma/schema.prisma` と `lib/fixtures/` は**1文字も変えません**★★が、★★`d-watch-api.yaml` / `lib/types/index.ts` / 画面（`components/features/reports/`）/ `scripts/seed-local-db.ts` は**変えます**★★（★決定1 と決定5。起草版から変わった点★）。★触るコードが重なる未実行の指示書は着手直前に自分で確認すること★ |
| 対象 | ★**不具合2件 + ユーザー確定5件**★。★★中心は「既存の関数を呼ぶだけ」「分割キーを変えるだけ」ですが、★決定1 だけは API契約・型・画面まで波及します★★★ |
| 取得する決定番号 | ★**着手直前に `grep -nE "^### D-(0\|1)[0-9][0-9]" docs/decisions.md \| tail -3` で最終番号を実測すること**★。★★`^### D-0` だけでは3桁台（D-100 以降）を拾えない★★。★2026-09-07 に `D-068` の二重採番が実際に起きています★。★★本改訂の5決定は**別担当が `D-109` として追記済み**（`docs/decisions.md:6328`。★未コミット★）★★なので、本書の実行記録は **`D-110`** の見込み★。★それでも必ず自分で実測してから採番すること★ |
| 前提 | ★**D-026**（`docs/decisions.md:122`。★「連続」はカレンダー日ではなくスケジュール存在日の連続★）★ / ★**D-103**（`:5222`。`consecutive_problem_days` はA案 ＝ `computeConsecutiveProblemDays()` を流用する）★ / ★**D-106**（`:5830`。★`clean_timetables` には1週間分の予定がまとめて登録される＝**未来の本清掃行が常時存在する**★）★ / ★**D-107**（`:5993`。指示書U の実行記録。★`isFutureSchedule()` は必須★）★ / ★**D-108**（`:6218`。指示書T の実行記録。★項3 が本書で直す `String(facility_id)` 詰め替えの出所★・★項7 に**誤りがある**（★D-109 §6 で訂正済み。段階7-2 で確認して参照する★）★）★ / ★★**D-109**（`:6328`。★本書に織り込んだ**ユーザー確定5決定**（2026-09-11）そのもの★。★着手時に行番号と内容を実測し、本書と食い違っていたら**実装を止めて確認を求めること**★）★★ / **D-102**（`:5139`。確定期間はサマリを読む・当期は詳細から算出）/ **D-091 項1**（`:3442`。猶予30分を当方で計算しない）/ **D-042**（`:226`。書き込み許可4カラム）/ **D-050**（`:308`。段階ごとに検証してからコミット）/ **D-056**（`:353`。`schema.prisma` は生成物）/ **D-077**（`:1847`。エラー値を変換しない）/ **D-089 項3・D-104**（★「その他」「不明」という語を使わない★） |
| 段階数 | ★**8段階**★（段階0〜段階7）。★コミットは段階1 / 2 / 3 / 4 / 5 / 7 の**6つ**★ |
| ★着手前確認★ | ★★**なし**★★ — ★判定の分かれ目（同一日に複数台の予定があるとき、**1台でも問題があればその日は問題日**／週の初めに施設を消さない／70.0 の扱い／正常終了行にも L3 が出ること／検証データを足すこと）は ★★すべてユーザーが 2026-09-11 に確定させています★★（後述「★ユーザー確定の5決定★」）。★★ユーザーの回答を待たずに段階0 から通しで実行してよい★★ |
| 着手前の基準 | ★**着手直前に自分で `npm test` / `npx tsc --noEmit` / `npm run lint` を実行し、その実測値を基準にすること**★。★作成時点の実測（2026-09-11。★鵜呑みにしない★）: `npm test` = **45ファイル / 894テスト（全パス）** / `tsc` = **0** / `lint` = **0**★ |
| 作成 | 2026-09-11（★改訂 2026-09-11。ユーザー確定5決定を反映★。作成時点の決定事項最終番号 **D-108**（★D-109 は別担当が `:6328` に追記済み・未コミット★）/ `docs/handoff.md` の最新は **追記32**（`docs/handoff.md:2662`）。★着手時に実測すること★） |

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

> ★**この指示書を読んでいるあなたへ**★ — `.claude/commands/d-watch.md` の「実装は Cursor 側」は
> **2つの環境の役割分担**です。**Cursor の中で動いている Claude は実装担当**です。
> ★調査や改訂ではなく実装を行ってください。★
> ★★**うまくいかなければ段階の切れ目（コミット単位）で止めて構いません**★★ —
> ★段階1（新ステータス）→ 段階2（当期の分母）→ 段階3（施設単位の連続日数）→ 段階4（70.0）→
> 段階5（検証データ）の順に、★どこで止めても単体で意味が通る形★に並べてあります★。

---

## ★★冒頭に読むべきこと — 直すのは5つ★★

| # | 直すこと | 症状 / 理由 | 直し方 |
|---|---|---|---|
| ★1★ | ★★施設別パフォーマンスに「判定なし」ステータスを足す★★ | ★決定1 の手当て。★このあと#2 を入れると、週の初め・月の初めに `operation_rate` が `null` になる★。★現状 `judgeFacilityStatus(null)` は `"bad"` を返すので、★★全施設が赤い「要改善」バッジになる★★★ | ★`d-watch-api.yaml` の enum → 型 → `judgeFacilityStatus()` → 画面バッジの順に4つ目の値を足す★ |
| ★2★ | ★★当期のKPIの分母に「まだ来ていない予定」が入る★★ | ★稼働率・出発成功率が実際より**低く**出る★。★実データでは常時1週間先までの本清掃行が存在する（D-106）ので、★★毎日発生する★★★ | ★`aggregateCleanStatsByFacility()` に `now` を渡し、★★未来の行は**バケットは作るが加算しない**★★★ |
| ★3★ | ★★連続問題日数の集計単位が2画面で違う★★ | ★清掃詳細ログ（F-02）が ★**ロボット単位**★ で数えており、★`CLAUDE.md` §4.5 の R3/R4「**同一施設で**2日以上／3日以上連続」とずれている★★ | ★分割キーを `serial_number` → ★`facility_id`★ にし、★同一日は OR で1日に畳む★★ |
| ★4★ | ★★完了率ちょうど 70.0 が「問題なし」になっている★★ | ★`CLAUDE.md` §4.1 は「△ = 10%以上70%以下」、§4.5 の R3/R4 は「△以下」★。★★70.0 は △ なので**問題あり**が正しい★★ | ★`isProblemCompletion()` を `<= GRADE_THRESHOLDS.good` 相当に直す★（★`judgeGrade()` は変えない★） |
| ★5★ | ★★検証データが足りない★★ | ★「日替わりで別の台が問題 → 3日連続」を実データで再現できない★（★複数台の予定がある日が 2026-09-09 の1日しか無い★） | ★`scripts/seed-local-db.ts` に施設7 の複数日ぶんの複数台スケジュールと履歴を足す★ |

---

## ★★ユーザー確定の5決定（2026-09-11。★D-109（`docs/decisions.md:6328`）として別担当が台帳へ追記済み★）★★

★★この5件は確定済みです。★★ ★迷ったらこの節に戻ること★。★★本書の他の記述とこの節が食い違っていたら、この節が正★★。

### ★決定1 — 週の初め・月の初めに施設が一覧から消えるのを防ぐ★

★起草版のまま（「範囲判定の直後に `continue`」）実装すると、★★実測で「日曜 00:30 に施設別パフォーマンスが
10施設 → 0施設」「1日 00:30 は今週も今月も 0施設」★★ になります★。
★理由: `facilityPerformance()` の当期行は ★★バケットのキーからしか作られない★★（`lib/db/index.ts:1066` / `:1089`）ため、
その施設の当期予定が全部未来だと ★バケットごと生まれない★ → ★行が消える★。

★★ユーザー回答（原文）★★:

> ★Server・DB側では一度1週間分の予定を作成したら基本的に常にそれを保持してリクエストすると返してくる★

★★確定した方針★★:

- ★`aggregateCleanStatsByFacility()` は、★★期間内の行であれば**未来でもバケットを作る**★★★
- ★未来の行は `planned` / `executed` / `taskArea` / `cleanArea` / `errorCount` の ★**加算だけをスキップ**★ する★
- ★★起草版の「範囲判定の直後に `continue`」は**誤り**★★ — ★`continue` すると入れ物（バケット）が作られない★
- ★結果、週の初めは ★★`planned: 0` のバケットが並び、施設は消えない★★★。
  ★稼働率・出発成功率は `null`（＝画面では「—」）になる★（`lib/judge/kpi.ts:19-23` が分母0で `null` を返す）

★★併せて必須の手当て（＝段階1）★★:

- ★`judgeFacilityStatus()`（`lib/judge/kpi.ts:75-82`）が `operationRateValue === null` で `"bad"` を返す★。
  ★このままだと ★★週の初めに全施設が「要改善」の赤バッジになる★★★
- ★★`null` のときは「判定なし」を返すように変える★★。★`FacilityStatus` に**4つ目の値**を足す★
  （★名前は実装者判断★。★★「その他」「不明」という語は使わない★★ — D-089 項3 / D-104）
- ★★API契約が先★★（`CLAUDE.md` §6 作業ルール1）: ★`d-watch-api.yaml` の enum を先に直してから★
  ★型（`lib/types/index.ts`）→ 判定（`lib/judge/kpi.ts`）→ クエリ（`lib/reports/query.ts`）→
  画面（`components/features/reports/`）★ の順に追う
- ★画面のバッジに4つ目の見た目（グレー等）を足す★。
  ★★モックに無い表示を足すことになります★★（`d-watch-mock.jsx` は3値しか持たない） —
  ★★これは意図した逸脱であり、段階7 の記録に「モックに無い表示を足した」と明記すること★★
- ★★`FACILITY_STATUS_THRESHOLDS`（`lib/config/index.ts:159-164`）の閾値は変えない★★

### ★決定2 — 同一ロボットが同じ日に2回清掃した場合も「1日」に畳む★

- ★現行は「2回とも失敗＝2日連続」★（`lib/db/__tests__/clean-log-mapping.test.ts:502-509` が固定している）
- ★★施設×日で畳むので「1日」に統一する★★。★根拠は D-026「連続はスケジュール存在日の連続」＝**日単位**★
- ★★このテストは期待値を 2 → 1 に直す★★。★★意図した仕様変更であることをテスト名とコメントに書くこと★★

### ★決定3 — 完了率ちょうど 70.0% は「問題あり」に直す★

- `isProblemCompletion()`（`lib/db/clean-log-mapping.ts:280-282`）が ★`< GRADE_THRESHOLDS.good`（＝ `< 70`）★ になっている
- ★★`CLAUDE.md` §4.1 は「△ = 10%以上70%以下」、§4.5 の R3/R4 は「△以下」★★ → ★70.0 は △ なので問題あり★
- ★★`<= 70` 相当に直す★★。★★閾値は `lib/config/` の `GRADE_THRESHOLDS` から読む。`70` を直書きしない★★
- ★★`judgeGrade()`（`lib/judge/`）は変えないこと★★ — ★あちらは「○ = 70%超」で既に正しい★。
  ★★今回直すのは「問題ありの日」の判定だけ★★
- ★実データへの影響（★実測済み 2026-09-11★）★: ★完了率ちょうど 70.0 の履歴は **1件**★
  （`clean_timetable_id=1005` / ★施設5★ / 2026-09-10）。
  ★★施設5 の期末連続日数が **0 → 2（L2）** に変わる★★（★前日 09-09 が未実施のため★）
- ★`lib/db/__tests__/clean-log-mapping.test.ts:422` の `isProblemCompletion(70.0) === false` は **true** に変わる★
  （★同ファイル `:404-414` の `it.each` 表の `[70.0, false]` も `[70.0, true]` に直す★）
- ★fixtures（`lib/fixtures/clean-logs.ts:96`）は手書きで `consecutive_problem_days: 1` を与えており「△以下」派★。
  ★★変更後は実装と整合する★★（★`lib/fixtures/clean-logs.ts:226` の生成式は `<` のままだが、
  生成される率に 70.0 ちょうどは現れないので矛盾しない★）

### ★決定4 — 正常に終わった行にも L3 が出るのを許容する★

- ★施設単位にすると、★★完了率95%で正常終了した行にも R3/R4 が出ます★★★
- ★逆に、別の台のせいで施設の連続が2以上だと、★★その台にとって初回の出発失敗でも R1 が出なくなります★★★
  （`lib/judge/advice.ts:61` の `consecutiveProblemDays <= 1` / `:71` の R3 / `:76` の R4）
- ★★ユーザー承認済み。R3/R4 は「現場に行くか」の判断なので施設単位が正しい★★
- ★★この挙動を「意図した結果」として扱い、テストで固定すること★★（段階3 の (c) 表）

### ★決定5 — 検証データを足す★

- ★実測: ★★複数ロボットの施設は施設7（3台）だけ。しかも複数台の予定がある日は 2026-09-09 の1日だけ★★★
- ★施設5 は robots が2台いるが、section 106 に `clean_timetables` が1行も無く**実質1台**★
- ★★`scripts/seed-local-db.ts` に、施設7 の**複数日ぶんの複数台スケジュールと履歴**を足す★★。
  ★ユーザー確定ケース（★1日目Aが問題 / 2日目Bが問題 / 3日目Aが問題 → 3日連続★）を
  ★★そのまま再現できる形にする★★
- ★★シードは冪等を保つこと★★（★乱数を使わない・id を明示採番する・既存行のローテーションをずらさない★）
- ★★`lib/fixtures/` は1行も変更しない★★

---

## ★不具合A — 当期のKPIの分母に「まだ来ていない予定」が入る（＝決定1・決定2の前半）★

### 現状（★2026-09-11 実測★）

`lib/db/report-mapping.ts` の `aggregateCleanStatsByFacility()`（★`lib/db/report-mapping.ts:170`★）は
★期間の範囲判定しかしていません★:

```ts
const jstDate = toJstIso(row.start_at).slice(0, 10);
if (jstDate < range.from || jstDate > range.to) continue;   // ← ここだけ
```

★`isFutureSchedule()`（`lib/db/clean-log-mapping.ts:263`）を掛けていない★ため、
★★「今週の予定だが、まだ時刻が来ていない清掃」も `planned`（分母）に入ります★★。
★実施していないので `executed`（分子）には入りません★ → ★稼働率・出発成功率が下がる★。

### ★なぜ「毎日発生する」のか★

- ★`clean_timetables` には ★**1週間分の清掃予定がまとめて登録される**★（★ユーザー回答 2026-09-10。D-106★）
- ★つまり実データでは ★**最大7日先までの本清掃行が常時DBに存在する**★
- ★今週の分母には「今日より後の今週ぶん」が丸ごと入る★ → ★週の頭ほど大きく下振れする★

### ★非対称になっていることが問題の本体★

★同じ `facilityPerformance()` の中で、★★連続日数の側だけは既に除外しています★★★（`lib/db/index.ts:934`）:

```ts
const consecutiveEntries = mainRows
  .filter((row) => !isFutureSchedule(row.start_at, now))   // ← 除外している
```

★`cleanLogs()`（`lib/db/index.ts:292`）と `prechecks()`（`:500`）も除外済み★です。
★★除外していないのは `aggregateCleanStatsByFacility()` だけ★★ —
★同じ1つのメソッドの中で、連続日数は未来を外し、KPIは外していない★という状態です。

### ★★ただし「行ごと捨てる」のは誤り（決定1）★★

★`cleanLogs()` / `prechecks()` は「行を返さない」で正しい★（★一覧に未来の予定を出さないため★）。
★★しかし `aggregateCleanStatsByFacility()` の戻り値は「施設 → バケット」の `Map` であり、
このキーが `facilityPerformance()` の**当期行そのもの**になります★★（`lib/db/index.ts:1066` / `:1089`）。

★★したがって「未来の行しか無い施設」のバケットを作らないと、その施設が一覧から消えます★★ —
★週の初め（日曜）・月の初め（1日）に**全施設が消える**★。★実測済み★。

★★正しい実装★★:

```ts
// 範囲外だけ捨てる（バケットも作らない）
if (jstDate < range.from || jstDate > range.to) continue;

const facilityId = Number(row.facility_id);
const bucket = result.get(facilityId) ?? emptyCleanStatsBucket();

// ★未来の予定は「期間内にある」ので★バケットは作る★が、★数えない★（決定1）
if (isFutureSchedule(row.start_at, now)) {
  result.set(facilityId, bucket);
  continue;
}
// …以降は現状どおり加算する
```

★★上の形（`result.set()` してから `continue`）でなくてもよい★★ —
★「未来の行でもバケットが必ず存在し、かつ1つも加算されない」ことが満たせれば書き方は実装者判断★。

### ★実測での影響★

★★作成時点のローカルDB（`dwatch_local`）には未来の本清掃行が **0件** です★★
（`SELECT COUNT(*) FROM clean_timetables WHERE precheck=0 AND start_at > NOW();` → **0**。
`MAX(start_at)` = **2026-09-10 18:00**）。
★★つまり**今このままでは症状が出ません**★★ — ★段階6 で未来行を一時的に投入して実測すること★。

★ユーザーの実測（2026-09-10）★: ★翌日の予定を12施設ぶん足すと稼働率・出発成功率が
★**78.57% → 55.00%**★ に落ちた★。
★作成時点の当週（2026-09-06〜09-12）の素の値は `planned` = **26** / `executed` = **20**（= 76.92%）★
（★実測。着手時に自分で取り直すこと★）。

### ★確定期間（サマリ由来）には影響しない★

★`kpiFromSummaryRatios()` は TeamWorks のサマリ値をそのまま使うだけ（D-102）★なので無関係です。
★★確定期間の値を当方の計算で上書きしないこと★★（禁止事項9）。

---

## ★不具合B — 連続問題日数の集計単位が2画面で違う（＝決定2・決定4）★

### ★仕様（`CLAUDE.md` §4.5）は施設単位★

| # | 条件 |
|---|---|
| R3 | ★**同一施設で**2日以上連続（出発失敗 or △以下）★ |
| R4 | ★**同一施設で**3日以上連続（出発失敗 or △以下）★ |

★★「同一施設で」と明記されています。★★

### 現状（★2026-09-11 実測★）

| 画面 | 実装 | 分割キー | 仕様との一致 |
|---|---|---|---|
| ★レポート（F-04）★ | `lib/db/index.ts:934-946` | ★`String(row.facility_id)` を `serial_number` の欄に詰めている★ | ★**施設単位＝仕様どおり**★ |
| ★清掃詳細ログ（F-02）★ | `lib/db/index.ts:348` → `computeConsecutiveProblemDays()`（`lib/db/clean-log-mapping.ts:317-325`） | ★`row.serial_number` ＝ ★**ロボット単位**★★ | ★★**仕様とずれている。こちらを直す**★★ |

`lib/db/clean-log-mapping.ts:320-325`（★実測。原文★）:

```ts
const bySection = new Map<string, ConsecutiveInput[]>();
for (const row of rows) {
  const list = bySection.get(row.serial_number);   // ← ロボット単位
  if (list) list.push(row);
  else bySection.set(row.serial_number, [row]);
}
```

★「P1 は 1清掃エリア = 1ロボットなので `serial_number` で代用できる」（`ConsecutiveInput` の docstring）
という前提で書かれていますが、★★**1施設に複数の清掃エリア（＝複数台）がある**★★ので代用になっていません★。

### ★★ユーザーが確定させた判定（2026-09-11）— A案★★

★★ユーザーの回答（原文）★★:

> 2台のうち 1日目Aが問題 / 2日目Bが問題 / 3日目Aが問題 でも ★3日連続で問題発生と記録する★

★★→ 同じ日に複数台の予定があるとき、**1台でも問題があればその日は「問題があった日」**とする（A案）★★

| 案 | 内容 | 採否 |
|---|---|---|
| ★**A** ✅★ | ★同一日の複数行を **OR** で畳み、1台でも問題ならその日は問題日★ | ★★**採用（ユーザー確定）**★★ |
| B | ★全台が問題のときだけ問題日とする（AND）★ | ★**不採用**★ — ★★3台ある施設がほぼ永久にエスカレーションしなくなる★★ |

★★同じ畳み込みが「同一ロボットが同じ日に2回清掃した」ケースにも効きます★★（★決定2★）—
★現行テスト（`lib/db/__tests__/clean-log-mapping.test.ts:502-509`）が「2回とも失敗＝2日連続」を固定しているが、
★★D-026（連続はスケジュール**存在日**の連続＝日単位）に照らして誤り★★なので ★期待値を 2 → 1 に直す★。

### ★修正方針（これをそのまま実装すること）★

1. ★`ConsecutiveInput` の分割キーを ★**`facility_id`**★ にする★
   （★`serial_number: string` を `facility_id: number` に置き換える★。
   ★`cleanLogs()` の `drafts`（`lib/db/index.ts:329` 付近）は既に `facility_id: Number(row.facility_id)` を
   持っているので、呼び出し側は何も足さなくてよい★）。
2. ★★同一日に複数行があるとき、`isProblem` を **OR** で畳んでから
   `countConsecutiveProblemDays()` に渡す★★。
   ★同関数（`lib/judge/escalation.ts:32`）の契約は「★スケジュール存在日1日分の結果★を日付昇順で渡す」＝
   ★**1日1エントリ**★です。★★今は1行1エントリで渡しているので、同じ日付の要素が3つあると
   「3日連続」と数えてしまいます★★（落とし穴1）。
3. ★★畳み込みも純粋関数として切り出し、テストで固定すること★★（例: `foldProblemDaysByDate()`）。
   ★`lib/judge/` ではなく `lib/db/clean-log-mapping.ts` に置くこと★ —
   ★「どの日がスケジュール存在日か」の判断はデータ取得側の責任、という
   `lib/judge/escalation.ts` の契約（`:11-16`）どおり★。
   ★★畳みは `computeConsecutiveProblemDays()` の**内部だけ**で行うこと★★ —
   ★呼び出し側でやると規則が2箇所に散る（D-103）★。
4. ★★戻り値は現状どおり「行id → 連続日数」の `Map<number, number>`★★。
   ★同じ日の全行に同じ値が入る★（★これが「行ごとに違う値が入る」現状との違い★）。
5. ★レポート側（`lib/db/index.ts` の `facilityPerformance()`）の**集計単位は変えない**★
   （★既に施設単位で正しい★）。ただし ★`serial_number: String(row.facility_id)`（`:940`）という
   詰め替えは紛らわしいので消し、★分割キーを明示的なフィールド名で渡す形に整える★★。
   ★★これは動作を変えない整理です★★（同じ値で同じ分割になる）。
6. ★★`countConsecutiveProblemDays()`（`lib/judge/escalation.ts`）は**1文字も変えない**★★。

### ★★注意: レポート側の**値**も変わります（意図した変化）★★

★上の5は「集計単位を変えない」という話であり、★★2 の日単位畳み込みは
レポート側の**値**にも効きます★★★。★同じバグ（同一日の複数行を複数日と数える）の修正なので、
★★意図した変化として受け入れ、段階7 で記録すること★★。

★実測（2026-09-11。ローカルDB。★段階3 まで適用した時点の期待値★）★:

| 施設 | 日 | 行 | 完了率 | 現状の値 | ★段階3 後の期待値★ |
|---|---|---|---|---|---|
| ★施設7★ | 2026-09-09 | `id=1026`（section 107） | 62.0（★問題★） | 1 | ★1★ |
| ★施設7★ | 2026-09-09 | `id=1027`（section 108） | 94.0 | ★0★ | ★★1★★ |
| ★施設7★ | 2026-09-09 | `id=1028`（section 109） | 88.0 | ★0★ | ★★1★★ |

- ★施設7 の前のスケジュール存在日は 2026-09-04（完了率 94.7 ＝ 問題なし）★なので ★連続は 1 日★
- ★レポート側★: `consecutiveProblemDaysAtPeriodEnd()`（`lib/db/report-mapping.ts:262`）は
  「期間内の最終ログ」＝ `id=1028`（17:00）を選ぶため、
  ★★施設7 の当週（2026-09-06〜09-12）・当月の `consecutive_problem_days` が **0 → 1** になります★★
- ★★段階4（70.0）を入れると、さらに**施設5** の当週・当月が **0 → 2** に変わります★★（決定3）
- ★★段階5（検証データ）を入れると、さらに**施設7 が 1 → 4** に変わります★★
  （★新しく足す3日ぶんが 09-09 の直前に連なるため。詳細は段階5★）
- ★これ以外の施設・期間の値は変わりません★（★同一日に複数行があるのは施設7 の 09-09 だけ★）

---

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

### 1. ★★段階5 の前後で変わる数字 / 変わらない数字（★2026-09-11 実測★）★★

| # | 項目 | 値 | 本書での扱い |
|---|---|---|---|
| ★1★ | `npm test` | ★**45ファイル / 894テスト（全パス）**★ | ★★増える★★（★減らないこと★） |
| ★2★ | `npx tsc --noEmit` | ★**0**★ | ★変わらない★ |
| ★3★ | `npm run lint` | ★**0**★ | ★変わらない★ |
| ★4★ | `db.cleanLogs()` の件数 | ★**253**★ | ★★段階5 で**増える**（本清掃行を足すため）★★。★段階4 までは 253 のまま★ |
| ★5★ | `db.prechecks()` の件数 | ★**27**★（★`precheck` **14** / `launch_failed` **13**★） | ★★**変わらない**★★（★段階5 で足す行はすべて実施済み・事前チェック行を作らないため★） |
| ★6★ | `clean_status_histories.clean_error` の分布 | ★`0`=225 / `1`=13 / `2`=5 / `3`=5 / `4`=5★ | ★★段階5 で `0` だけ増える★★（★1〜4 は増やさない★） |
| ★7★ | `/reports` の `facility-performance` | ★確定期間 **11件** / 当期 **10件**（`limit` を上げたとき。既定 `limit=10` で切れる。D-108 項6）★ | ★★当期の件数は本書では**変わらない**★★（★決定1 でバケットを作るので、未来行があっても件数は減らない★） |
| ★8★ | `continuous-problems` | ★**500（未実装）**★（第6歩の範囲。★本書では直さない★） | ★変わらない★ |

★★4 が段階4 までで1件でも変わっていたら実装が違います★★（★連続日数は「値」であって「件数」ではない★）。
★着手直前に自分で実測して基準にすること★。

### 2. ★ローカルDBの現状（★2026-09-11 実測★）★

| 項目 | 値 |
|---|---|
| `clean_timetables` の本清掃（`precheck=0`） | ★253行★ |
| ★未来の本清掃（`start_at > NOW()`）★ | ★★**0行**★★（★だから不具合A は今このままでは再現しない★） |
| `MIN(start_at)` / `MAX(start_at)`（本清掃） | ★2026-08-11 16:30 / 2026-09-10 18:00★ |
| ★複数台の施設★ | ★**施設5 = 2台**（section 105 / 106）★ / ★**施設7 = 3台**（section 107 / 108 / 109）★ / 施設12 = 0台 |
| ★施設5 の `clean_timetables`★ | ★section 105 のみ★（★2台あるが予定は1台ぶんしかない → **実質1台**★） |
| ★施設7 の `clean_timetables`★ | ★2026-08-11〜09-04 は section 107 のみ / ★**2026-09-09 だけ 107 / 108 / 109 の3行**★★ |
| ★完了率ちょうど 70.0 の履歴★ | ★★**1件**（`clean_timetable_id=1005` / 施設5 / 2026-09-10）★★（決定3） |
| ★2画面の食い違い★ | ★★現状は**偶然0件**★★（★施設7 の 09-09 でレポート側が最終ログ `id=1028`（連続0）を選ぶため、詳細ログの `id=1028`（0）と一致してしまう★）。★★ログが増えれば食い違う★★ |

### 3. ★落とし穴（★踏むとテストが通ったまま壊れます★）★

| # | 落とし穴 |
|---|---|
| ★1★ | ★★`countConsecutiveProblemDays()` は「配列の要素」を後ろから数えます★★（`lib/judge/escalation.ts:32-41`）。★**日付を見ていません**★。★同じ日付の要素を3つ渡すと「3日連続」と返ります★。★★だから畳み込みが必須★★ — ★分割キーを `facility_id` に変えるだけで畳み込みを忘れると、★★施設7 の 09-09 が「3日連続」に化けます★★★ |
| ★2★ | ★★`isFutureSchedule()` を消さないこと★★（D-107 設計3-C。★消すと未来の清掃が全部「出発失敗」になる★）。★本書は `isFutureSchedule()` を**足す**方向の作業であり、既存の3箇所（`:292` / `:500` / `:934`）は1文字も変えない★ |
| ★3★ | ★★`now` を1回だけ作って使い回すこと★★。★行ごとに `new Date()` を作ると境界の行の扱いがぶれ、★テストが実行時刻で揺れます★★ |
| ★4★ | ★★`isFutureSchedule()` の境界は `>`（`start_at === now` は「来ていない」に入れない）★★。★変えないこと★（`lib/db/clean-log-mapping.ts:263`） |
| ★5★ | ★★猶予30分（`LAUNCH_GRACE_MINUTES`）を足さないこと★★（D-091 項1）。★足すと設定値を変えたときに過去のログの判定まで変わる★ |
| ★6★ | ★★`aggregateCleanStatsByFacility()` で未来の行を `continue` で捨てないこと★★（★決定1★）。★★捨てるとバケットが作られず、週の初め・月の初めに施設が一覧から消えます★★。★捨ててよいのは「範囲外の行」だけ★ |
| ★7★ | ★★`judgeFacilityStatus(null)` を `"bad"` のままにしないこと★★（★決定1★）。★決定1 の分母修正だけ入れて `judgeFacilityStatus()` を放置すると、★★週の初めに全施設が赤い「要改善」になります★★★。★だから段階1（ステータス）を段階2（分母）より**先**にやる★ |
| ★8★ | ★★`??` を `||` に書き換えないこと★★（`0` が別の値に化けます） |
| ★9★ | ★★判定は丸める前の生値で行うこと★★（`CLAUDE.md` §4.3。`completion_rate` に `Math.round()` を掛けない） |
| ★10★ | ★`consecutiveProblemDaysAtPeriodEnd()`（`lib/db/report-mapping.ts:262`）の「期間内の最終ログを選ぶ」規則は変えないこと★。★畳み込み後は同じ日の全行が同じ値になるので、どの行が選ばれても結果は同じになります（決定性が上がるだけ）★ |
| ★11★ | ★★段階4 で `judgeGrade()` を触らないこと★★（決定3）。★あちらの「○ = 70%超」は既に正しい★。★直すのは `isProblemCompletion()` だけ★ |
| ★12★ | ★★段階5 で追加する行を既存のループの中に混ぜないこと★★。★`scripts/seed-local-db.ts` の本ループは `historyIndex`（`:869` 付近）と `errorReportCount` の**通番の剰余**で `clean_error` と対応状況を決定的に散らしています★ — ★★途中に行を差し込むと既存行の値が全部ずれ、`clean_error` 分布（225/13/5/5/5）が変わります★★。★★追加行は本ループの**後ろ**に独立したブロックとして足すこと★★ |
| ★13★ | ★★段階5 で追加する行に事前チェック行・エラー行を作らないこと★★。★作ると `prechecks()` の 27 件が変わります★ |

---

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

| # | ★してはいけないこと★ | 理由 |
|---|---|---|
| ★1★ | ★★**`git add -A` を使うこと**★★ | `CLAUDE.md` §7。★過去に別作業の途中ファイルを巻き込む事故が起きている★。★★必ずファイルパスを明示すること★★ |
| ★2★ | ★★**`README.md` / `docs/code-map.md` / `docs/handoff/管理画面_設計検討_20260910.md` / `.claude/worktrees/**` に触ること**★★ | ★★**すべて別作業のもの**★★。`README.md` は未コミットの変更あり、`docs/code-map.md` と `管理画面_設計検討_20260910.md` は未追跡（`git status` に `M` / `??` で出る）、`.claude/worktrees/**` には★別作業のコード地図★が入っている。★**本指示書のコミットに含めない・編集しない・削除しない**★ |
| ★3★ | ★★**メインDBのスキーマ変更（`prisma migrate` / `db push` / `db pull` / `prisma/schema.prisma` の手書き）**★★ | `CLAUDE.md` §7 / D-056 / D-059。★★本書はスキーマ変更を1つも必要としません★★（★段階5 は**既存テーブルへの行追加**だけ★） |
| ★4★ | ★★**書き込み許可4カラム（`status` / `comment` / `responded_at` / `resolved_at`）以外への DB 書き込み**★★ | D-042。★アプリコードの読み出しは**読むだけ**★。★段階5 の追加行は `scripts/seed-local-db.ts`（＝ローカル検証用シード）からの投入であり、アプリの実行時書き込みではない★。★段階6 の検証用 INSERT は `dwatch_local` の**一時行**のみ（★必ず削除する★）。★メインDB / `dwatch_ddl` に接続しないこと★ |
| ★5★ | ★★**閾値（90/70/10、猶予30分、L2=2日/L3=3日）をハードコードすること**★★ | `CLAUDE.md` §7。★`lib/config/` から読む★。★★段階4 も `GRADE_THRESHOLDS.good` から読むこと。`70` を直書きしない★★ |
| ★6★ | ★★**表示用に丸めた値で判定すること**★★ | `CLAUDE.md` §4.3 / 落とし穴9 |
| ★7★ | ★★**`isFutureSchedule()` を消すこと・猶予を足すこと・境界（`>`）を変えること**★★ | D-107 設計4 / 落とし穴2・4・5。★★消すと未来の清掃が全部「出発失敗」になります★★ |
| ★8★ | ★★**レポート側の連続日数の「施設単位」を崩すこと**★★ | `CLAUDE.md` §4.5（R3/R4 は「同一施設で」）/ D-103。★`facilityPerformance()` の分割単位は施設のまま★。★整えるのは**渡し方（フィールド名）だけ**★ |
| ★9★ | ★★**確定期間（サマリ由来）の値を当方の計算で上書きすること**★★ | D-102 / `CLAUDE.md` §2。★`kpiFromSummaryRatios()` に手を付けない★ |
| ★10★ | ★**`prisma/schema.prisma` / `lib/fixtures/` / `lib/reports/query.ts` の絞り込みロジック / APIルート（`app/api/`）の処理 を変更すること**★ | ★★本書が触ってよいのは、データ層の内部＋「判定なし」ステータスの追加に必要な最小限だけ★★。★`lib/reports/query.ts` は ★**`FACILITY_STATUS_FILTERS` の配列に4つ目の値を足すだけ**★（`:29-34`）。★`filterFacilityPerformanceByStatus()`（`:176`）の実装は汎用なので触らない★ |
| ★11★ | ★**`prismaSource` の `facilities` / `schedules` / `dsDevices` / `dsAlerts` / `errorBreakdown` / `continuousProblems` / 点検記録2本に手を付けること**★ | ★範囲外★。★触るのは `cleanLogs` / `reportKpis` / `facilityPerformance` の3本の**内部**だけ★ |
| ★12★ | ★**`lib/db/prisma.ts` / `local-guard.ts` / `use-fixtures.ts` / `ds-status.ts` / `precheck-mapping.ts` を変更すること**★ | ★完成しており、そのまま効く★ |
| ★13★ | ★**テストを DB 依存にすること**★ | ★遅く・不安定になり、DB が空の環境で落ちる★。★テストは純粋関数と手書きの入力だけ★ |
| ★14★ | ★**`continuousProblems`（第6歩）を実装すること**★ | ★別の指示書の範囲★。★`USE_FIXTURES=false` で 500 のままが正しい★ |
| ★15★ | ★**タイムゾーン（DBがJSTかUTCか）の話に手を付けること**★ | ★★別件★★（D-106 項8）。★`toJstIso()` の既存の使い方を1文字も変えない★ |
| ★16★ | ★**`npm run dev` を Bash で起動すること**★ | ★Browser pane の `preview_start` を使う★。★無ければ curl で代替し、その旨を記録する★ |
| ★17★ | ★**`.env.local` をコミットすること / DB の認証情報を文書やコードに書くこと**★ | `CLAUDE.md` §9.3。★検証後は `USE_FIXTURES=true` に戻す★ |
| ★18★ | ★**エラー種別を7分類に変換すること・「その他」「不明」という語を使うこと**★ | D-077 / D-089 項3 / D-104 項2。★★段階1 で足す4つ目のステータス名にも効きます★★。★段階7 の記録訂正でも語彙を作らないこと★ |
| ★19★ | ★**`FACILITY_STATUS_THRESHOLDS` の閾値（good=90 / warning=75）を変えること**★ | ★決定1 の明示指示★。★足すのは「分母0のときの4つ目の値」だけで、閾値そのものは触らない★ |

---

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

★段階0（前提の確認。★読むだけ・コミットなし★）
→ 段階1（★決定1 の手当て★: 「判定なし」ステータスを足す。★API契約 → 型 → 判定 → 画面★）
→ 段階2（★不具合A★: 当期の分母から未来を外す。★バケットは作る★）
→ 段階3（★不具合B★: 連続日数を施設単位にし、同一日を OR で畳む）
→ 段階4（★決定3★: 完了率ちょうど 70.0 を「問題あり」にする）
→ 段階5（★決定5★: 検証データをシードに足して再シード）
→ 段階6（実測での検証。★コミットなし★）
→ 段階7（記録と push）★

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

> ★★**期待値表 → テスト → 実装 の順序を崩さないこと**★★（`CLAUDE.md` §6-2 / §9.4）。
> ★★特に段階3 は「テストを先に書き、**落ちることを確認してから**実装する」こと★★ —
> ★★分割キーを変えただけで畳み込みを忘れても、既存テスト（1台ぶんの入力しか無い）は
> 全部通ってしまいます★★（落とし穴1）。★先に落ちるテストを作らないと、壊れたまま緑になります★。

> ★★**なぜこの順番なのか**★★
> - ★段階1 が先頭なのは ★★`CLAUDE.md` §6 作業ルール1「API契約を変えたくなったら先に YAML を直す」★★ と、
>   ★段階2 だけ先に入れると週の初めに全施設が赤バッジになる★（落とし穴7）から
> - ★段階2 と段階3 を分けるのは、「引数を1つ足す」修正と「分割キー変更 + 畳み込み新設」で
>   ★★触るファイルも壊し方も別物★★だから。★1コミットにまとめると段階6 の実測でどちらが効いたか分からなくなる★
> - ★段階4（70.0）を段階3 の後に置くのは、★★段階3 の期待値表を 70.0 の変更で二度書き直さないため★★
> - ★段階5（シード）を最後の実装段階に置くのは、★★件数が変わるので、それまでの段階を
>   「件数は1件も変わらない」で検証できるようにするため★★

---

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

### 読むもの

| # | 対象 | 見る点 |
|---|---|---|
| 1 | ★`CLAUDE.md` 全文★ | ★特に ★**§4.1（△ = 10%以上70%以下）**★ / ★**§4.5（R3/R4 は「同一施設で」）**★ / §4.3（丸め）/ §6（API契約が先）/ §8（用語）/ §9.4（判定ロジックの作法）★ |
| 2 | ★`docs/decisions.md` の **D-026**（`:122`）★ | ★「連続」はスケジュール存在日の連続★。★★畳み込みで日を1つにまとめても、清掃予定のない日は依然として飛ばされる（連続が途切れない）ことを確認する★★ |
| 3 | ★**D-103**（`:5222`）/ **D-106**（`:5830`）/ **D-107**（`:5993`）/ **D-108**（`:6218`）/ ★**D-109**（`:6328`）★ | ★A案の出所 / 1週間分の予定 / `isFutureSchedule()` の必須性 / `String(facility_id)` 詰め替えの出所 / ★本改訂の5決定★★。★行番号は自分で取り直すこと★。★★D-109 が本書と食い違っていたら実装を止めて確認を求める★★ |
| 4 | ★`docs/handoff.md` の末尾（★追記32★。`:2662`）★ | ★指示書T の完了内容と申し送り★ |
| 5 | ★`docs/handoff/指示書T_ローカルDBで動かす第5歩_レポートサマリ.md` / `指示書U_清掃結果をclean_errorで判定する.md`★ | ★段階の切り方・期待値表の書き方・記録の作法★ |
| 6 | ★`lib/db/clean-log-mapping.ts` 全文（346行）★ | ★`isFutureSchedule()`（`:263`）/ `isProblemCompletion()`（`:280`）/ `ConsecutiveInput`（`:285`）/ `computeConsecutiveProblemDays()`（`:317`）★ |
| 7 | ★`lib/db/report-mapping.ts` 全文（369行）★ | ★`aggregateCleanStatsByFacility()`（`:170`）/ `kpiFromCleanStats()`（`:208`）/ `FacilityConsecutiveEntry`（`:241`）/ `consecutiveProblemDaysAtPeriodEnd()`（`:262`）★ |
| 8 | ★`lib/db/index.ts:270-360`（`cleanLogs()`）★ / ★`:780-830`（`reportKpis()` 当期）★ / ★`:910-1100`（`facilityPerformance()`）★ | ★`now` が既にどこで作られているか（`:289` / `:493` / `:932`）★ / ★`serial_number: String(row.facility_id)`（`:940`）★ / ★★当期行がバケットのキーから作られていること（`:1066` / `:1089`）★★ |
| 9 | ★`lib/judge/kpi.ts` 全文★ | ★`rate()` が分母0で `null` を返すこと（`:19-22`）★ / ★`judgeFacilityStatus()`（`:75-82`）が `null` を `"bad"` にしていること★ |
| 10 | ★`lib/judge/escalation.ts` 全文★ | ★★`countConsecutiveProblemDays()` が**日付を見ていない**こと（落とし穴1）を自分の目で確認する★★。★★この関数は変更しないこと★★ |
| 11 | ★`lib/judge/advice.ts:50-80`★ | ★決定4 の影響箇所（R1 の `consecutiveProblemDays <= 1` / R3 / R4）★。★★この関数も変更しない★★ |
| 12 | ★`lib/db/__tests__/clean-log-mapping.test.ts:392-512` / `lib/db/__tests__/report-mapping.test.ts:113-174` / `:238-281`★ | ★既存テストの書き方（ヘルパー `row()` 等）に揃えるため★ |
| 13 | ★`scripts/seed-local-db.ts:704-925`★ | ★清掃ログ系4テーブルの投入ループ★。★★`historyIndex` / `errorReportCount` の通番ローテーション（落とし穴12）を自分の目で確認する★★ |

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

- ★`.env.local` の `DATABASE_URL` が `dwatch_local` を指していることを確認する★。
  ★**値そのものを画面・ログ・文書に出さないこと**★（`CLAUDE.md` §9.3）
- ★`.env.local` に `ALLOW_DIRECT_ACCESS=true` があることを確認する★（D-101 項2。★無いと段階6 で詰まる★）
- ★★着手前の基準値を自分で実測して控えた★★（`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 COUNT(*) AS main_rows FROM clean_timetables WHERE precheck=0;
SELECT COUNT(*) AS future_main FROM clean_timetables WHERE precheck=0 AND start_at > NOW();
SELECT MIN(start_at), MAX(start_at) FROM clean_timetables WHERE precheck=0;
SELECT facility_id, DATE(start_at) d, COUNT(*) c FROM clean_timetables WHERE precheck=0
  GROUP BY facility_id, DATE(start_at) HAVING c > 1 ORDER BY facility_id, d;
SELECT clean_timetable_id, clean_ratio FROM clean_status_histories WHERE clean_ratio = 70.0;
SELECT t.facility_id, t.section_id, DATE(t.start_at) d FROM clean_timetables t
  WHERE t.precheck=0 AND t.facility_id=7 ORDER BY d, t.section_id;"
```

★作成時点の実測★: `main_rows` = ★253★ / `future_main` = ★**0**★ /
`MIN`〜`MAX` = ★2026-08-11 16:30 〜 2026-09-10 18:00★ /
★同一日に複数行がある施設・日 = ★**施設7 の 2026-09-09（3行）だけ**★★ /
★`clean_ratio = 70.0` = ★**1件（`clean_timetable_id=1005`）**★★ /
★施設7 は ★**09-04 まで section 107 のみ・09-09 だけ 107/108/109**★★

- ★★修正前の連続日数を全施設ぶん控えておくこと★★（★段階6 の突き合わせに使う★）:

```
USE_FIXTURES=false DATABASE_URL="mysql://root@localhost:3306/dwatch_local" npx tsx -e "
import { db } from './lib/db';
const fp = await db.facilityPerformance();
console.log(fp.map(r => [r.period, r.year, r.month, r.week, r.item.facility_id,
  r.item.consecutive_problem_days, r.item.status]));
"
```

- ★★決定番号を実測した★★（`grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md | tail -3`）。
  ★★`D-109`（`:6328`）が既に入っていることを確認したら、本書の記録は `D-110`★★。★入っていなければ空き番号を採る★

---

## 段階1 — ★決定1 の手当て: 「判定なし」ステータスを足す★（1コミット）

### 1-0. ★なぜ最初にやるのか★

★段階2（当期の分母から未来を外す）を入れると、★★週の初め・月の初めに `planned = 0` の施設が出ます★★★。
★`operationRate(0, 0)` は `null`（`lib/judge/kpi.ts:19-23`）★ →
★★`judgeFacilityStatus(null)` は現在 `"bad"` を返すので、全施設が赤い「要改善」バッジになります★★。
★★先にステータスを足しておけば、そのような中間状態を作らずに済みます★★。

★★`CLAUDE.md` §6 作業ルール1 のとおり、`d-watch-api.yaml` から直すこと★★。

### 1-1. ★★期待値表（★これをそのままテストにすること。実装より先に書く★）★★

対象: `judgeFacilityStatus(operationRateValue)`（`lib/judge/kpi.ts:75`）

★4つ目の値の名前は実装者判断★。★★「その他」「不明」という語は使わないこと★★（D-089 項3 / D-104）。
★以下の表では仮に `no_data`（画面表示「判定なし」）と書きます★ — ★別名にした場合は表の値を読み替えること★。

| # | 入力（稼働率） | ★期待★ | 備考 |
|---|---|---|---|
| ★1★ | 96.3 | `"good"` | ★既存テスト（`lib/judge/__tests__/kpi.test.ts:95`）★ |
| ★2★ | 90.0 | `"good"` | ★境界（以上）。既存★ |
| ★3★ | 89.9 | `"warning"` | ★既存★ |
| ★4★ | 75.0 | `"warning"` | ★境界（以上）。既存★ |
| ★5★ | 74.9 | `"bad"` | ★既存★ |
| ★6★ | 0 | ★`"bad"`★ | ★★0% は「判定なし」ではない★★（★実施予定があって1回も実施できていない＝要改善★） |
| ★7★ | ★★`null`★★ | ★★`no_data`（＝**`"bad"` ではない**）★★ | ★★`lib/judge/__tests__/kpi.test.ts:104` の期待値を差し替える★★。★意図した仕様変更であることをテスト名とコメントに書く★ |

★★#6 と #7 を両方入れること★★（★「分母0」と「分子0」を取り違えると静かに壊れます★）。

### 1-2. テストを書く

- ★`lib/judge/__tests__/kpi.test.ts:93` の `describe("judgeFacilityStatus（良好/注意/要改善）")` に足す★
  （★`describe` の名前も4値に合わせて直すこと★）
- ★★`:104` の `expect(judgeFacilityStatus(null)).toBe("bad")` は**期待値を差し替える**★★。
  ★★消さずに、テスト名とコメントに「決定1（2026-09-11 ユーザー確定）で `null` は判定なしに変えた」と書くこと★★
- ★この時点で #7 が**落ちる**ことを確認してから実装に進む★

### 1-3. 実装（★この順序で★）

1. ★★`d-watch-api.yaml`（**先に直す**）★★
   - `FacilityPerformance.status` の enum（★`d-watch-api.yaml:1294`★）に4つ目を足す
   - `GET /reports/facility-performance` の `status` クエリパラメータの enum（★`:524`★）にも足す
     （★`all` は残す★）
   - ★`description` に「稼働率が算出できない（その期間の清掃予定が0件、または予定が全部これから）ときの値」と書く★
2. `lib/types/index.ts`
   - ★`FacilityStatus`（`:238`）に4つ目を足す★
3. `lib/judge/kpi.ts`
   - ★`judgeFacilityStatus()`（`:75-82`）の `if (operationRateValue === null) return "bad";` を
     ★4つ目の値を返す★ に変える★
   - ★★docstring の「`null`（分母0＝その期間に清掃予定が1件もない）は「要改善」に寄せる」という説明を
     書き換えること★★: ★「分母0は**評価できない**ので判定なしにする（決定1。2026-09-11）」★ /
     ★「`clean_timetables` には1週間分の予定がまとめて登録される（D-106）ため、
     週の初め・月の初めは『予定はあるが実施0件』が正常な状態になる」★
   - ★★`FACILITY_STATUS_THRESHOLDS` は触らない★★（禁止事項19）
4. `lib/reports/query.ts`
   - ★`FACILITY_STATUS_FILTERS`（`:29-34`）に4つ目を足す★（★`FacilityStatus` の定義順 + `all`★）
   - ★★`filterFacilityPerformanceByStatus()`（`:176`）は汎用なので触らない★★
5. `components/features/reports/FacilityPerformanceTable.tsx`
   - ★`STATUS_LABEL`（`:116-120`）に4つ目のラベルを足す★（★「判定なし」。★「その他」「不明」は禁止★）
   - ★`statusColor()`（`:129-133`）に4つ目の色を足す★ —
     ★★グレー系（`THEME_TOKENS[theme]` の既存トークンから選ぶ。16進のカラーコードを渡すこと。
     `var(--…)` を渡すと `Pill` の `${color}22` が壊れる。同ファイル `:122-128` のコメント参照）★★
   - ★★「モックに無い表示を足している」ことをコメントに1行残すこと★★
     （★`d-watch-mock.jsx` は3値しか持たない。決定1 で承認済み★）
6. `components/features/reports/ReportsScreen.tsx`
   - ★`:259-262` の `<select>` に4つ目の `<option>` を足す★（★`StatusFilter` は `FacilityStatus | "all"` なので型は自動で追従する★）
7. ★`lib/db/index.ts` は**変更しない**★（★`judgeFacilityStatus()` を呼んでいるだけ（`:965`）なので自動で追従する★）

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

- ★期待値表7ケースがテストになっており、全部通る★
- ★★`lib/fixtures/__tests__/reports.test.ts:180-185`（fixtures の `status` が `judgeFacilityStatus()` と一致するテスト）が
  通ったままであること★★（★fixtures に `operation_rate.value === null` の行は無いので通るはず★。
  ★落ちたら fixtures ではなく**実装**を疑うこと。`lib/fixtures/` は変更禁止★）
- ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★
- ★`git diff --stat` が ★`d-watch-api.yaml` / `lib/types/index.ts` / `lib/judge/kpi.ts` / `lib/reports/query.ts` /
  `components/features/reports/FacilityPerformanceTable.tsx` / `components/features/reports/ReportsScreen.tsx` /
  `lib/judge/__tests__/kpi.test.ts` の7ファイルだけ★★
- ★★コミット（ファイルパスを明示。`git add -A` 禁止）★★
  - 例: `git commit -m "feat: 指示書V 段階1 施設別パフォーマンスに判定なしステータスを足す"`

---

## 段階2 — ★不具合A: 当期の分母から未来の予定を外す★（1コミット）

### 2-1. ★★期待値表（★これをそのままテストにすること。実装より先に書く★）★★

対象: `aggregateCleanStatsByFacility(mainRows, historyByTimetableId, errorCountByMain, range, now)`

★共通の前提★: `range = { from: "2026-09-06", to: "2026-09-12" }` / `now = 2026-09-09T12:00:00+09:00` /
履歴は `clean_error = 0` / `task_area = 100` / `clean_area = 90` / エラー件数は 0 とする（★特記が無い限り★）。

★★この表の要は「未来の行は**バケットを作るが加算しない**」（決定1）★★です。
★★「未来の行があるとバケットができない」形になっていたら実装が誤りです★★。

| # | ケース | 入力（`start_at`） | ★期待★ |
|---|---|---|---|
| ★1★ | 期間内・過去 | 2026-09-08T17:00+09:00 | ★`planned` = 1 / `executed` = 1 / `taskArea` = 100 / `cleanArea` = 90★ |
| ★★2★★ | ★★期間内・未来（★バケットはできる★）★★ | 2026-09-10T17:00+09:00 | ★★`result.has(施設) === true` かつ `planned`=0 / `executed`=0 / `taskArea`=0 / `cleanArea`=0 / `errorCount`=0★★（＝ `emptyCleanStatsBucket()` と等しい） |
| ★★3★★ | ★★未来の予定を足しても `planned` が増えない★★ | #1 の行 + #2 の行を同時に渡す | ★★`planned` = **1**（2 にならない）★★ / `executed` = 1 |
| ★4★ | ★同時刻ちょうど★ | `start_at === now`（2026-09-09T12:00+09:00） | ★`planned` = 1（★`isFutureSchedule()` は `>` なので「来ていない」に入れない★）★ |
| ★5★ | ★★期間の左端ちょうど★★ | 2026-09-06T00:00+09:00 | ★`planned` = 1★ |
| ★6★ | ★★期間の右端ちょうど★★ | 2026-09-12T23:59+09:00 ／ ★`now` = 2026-09-13T00:00+09:00★ | ★`planned` = 1★（★`now` を後ろにずらして「未来ではない」状態を作る★） |
| ★7★ | 期間の左端の前日 | 2026-09-05T17:00+09:00 | ★★計上しない。★バケットも作らない★（`result.has(施設) === false`）★★ |
| ★8★ | 期間の右端の翌日 | 2026-09-13T17:00+09:00 | ★★同上（`result.has(施設) === false`）★★ |
| ★9★ | ★★未来行に履歴が既にある場合★★ | 未来の行に `clean_error=0` / `task_area=100` / `clean_area=90` の履歴を付ける | ★★バケットはできるが `planned` / `executed` / `taskArea` / `cleanArea` の**すべてが 0 のまま**★★ |
| ★10★ | ★★未来行にエラーが紐づいている場合★★ | 未来の行に `errorCountByMain` で 3 件 | ★★バケットはできるが `errorCount` = **0**★★ |
| ★★11★★ | ★★未来行しかない施設（★決定1 の本体★）★★ | 施設2 の行が未来だけ | ★★`result.has(2) === true` かつ `emptyCleanStatsBucket()` と等しい★★（★★起草版の「バケット自体ができない」から**反転**した★★） |
| ★12★ | ★★`now` を注入できること★★ | #2 と同じ行で `now` を 2026-09-11T00:00+09:00 にする | ★`planned` = 1（★同じ入力でも `now` 次第で答えが変わる＝注入が効いている★）★ |
| ★13★ | 複数施設 | 施設1（過去1件）+ 施設3（過去2件・未来1件） | ★施設1 = 1 / 施設3 = 2 / ★`result.size === 2`★★ |
| ★★14★★ | ★★未来行だけの施設のKPI（★段階1 とのつなぎ★）★★ | #11 のバケットを `kpiFromCleanStats(bucket, undefined)` に通す | ★★`operation_rate.value === null` / `launch_rate.value === null` / `completion_rate.value === null` / `error_rate.value === null`★★ かつ ★★`judgeFacilityStatus(null)` が段階1 の4つ目の値を返す★★ |

★★#3 / #6 / #11 / #12 / #14 は必ず入れること★★
（★不具合A の本体・両端・**決定1**・注入可能性・画面までのつながり★）。

### 2-2. テストを書く

- ★`lib/db/__tests__/report-mapping.test.ts` の `describe("aggregateCleanStatsByFacility")`（`:113`）に足す★
- ★既存の2ケース（`:127` / `:164`）は**消さないこと**★。★`now` 引数の追加に追従させるだけ★
  （★★`now` は必須引数にするので、既存ケースにも明示的に `now` を渡すこと★★）
- ★#14 は `describe("kpiFromCleanStats")` 側に置いてもよい★（★置き場所は実装者判断★）
- ★★この時点で #2 / #3 / #9〜#11 が**落ちる**ことを確認してから実装に進むこと★★

### 2-3. 実装

1. `lib/db/report-mapping.ts`
   - `aggregateCleanStatsByFacility()`（`:170`）のシグネチャに ★`now: Date`★ を足す
     - ★★**必須引数にすること**★★（★`now: Date = new Date()` のような既定値を置かない★） —
       ★省略できると、テストが「今日の日付」で通ってしまい意味が無くなる★
   - ★★範囲外の行だけ `continue` で捨て、未来の行は**バケットを作ってから**加算をスキップする★★（決定1）
   - ★`isFutureSchedule` を `./clean-log-mapping` から import する★（★同ファイルは既に
     `notExecutedFromHistory` を同じ場所から import している（`:20`）ので行を足すだけ★）
   - ★docstring に理由を残すこと★:
     - ★「1週間分の予定がまとめて登録される（D-106）ので、まだ来ていない予定を分母に入れると
       稼働率・出発成功率が下振れする」★
     - ★「`cleanLogs()`（`lib/db/index.ts:292`）・`prechecks()`（`:500`）・連続日数（`:934`）と同じ扱いに揃えた」★
     - ★★「ただし**行ごと捨てない**。この Map のキーが `facilityPerformance()` の当期行そのものなので
       （`lib/db/index.ts:1066` / `:1089`）、捨てると週の初め・月の初めに施設が一覧から消える（決定1）」★★
2. `lib/db/index.ts`
   - `reportKpis()`: ★当期集計の前に `const now = new Date();` を1つ作り★、`:805` / `:813` の
     両方に渡す（★★2つ作らないこと★★）
   - `facilityPerformance()`: ★既存の `const now = new Date();`（`:932`）を `:1052` / `:1058` に渡す★
   - ★渡している箇所に短いコメントを付ける★（★「1回の集計で同じ `now` を使い回す」★）

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

- ★期待値表14ケースがテストになっており、全部通る★
- ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★
- ★`git diff --stat` が ★`lib/db/report-mapping.ts` / `lib/db/index.ts` / `lib/db/__tests__/report-mapping.test.ts` の3ファイルだけ★★
- ★★コミット（ファイルパスを明示）★★
  - 例: `git commit -m "fix: 指示書V 段階2 当期KPIの分母から未来の予定を外す"`

---

## 段階3 — ★不具合B: 連続問題日数を施設単位に揃え、同一日を畳む★（1コミット）

### 3-1. ★★期待値表（★これをそのままテストにすること。実装より先に書く★）★★

#### (a) ★畳み込み関数（例: `foldProblemDaysByDate()`）★ — ★1施設ぶんの行 → `ScheduleDayResult[]`★

| # | ケース | 入力 | ★期待★ |
|---|---|---|---|
| ★a1★ | 1日1行 | 09-01(50) | `[{date:"2026-09-01", isProblem:true}]` |
| ★a2★ | ★★同一日に問題行と正常行★★ | 09-01(50) + 09-01(95) | ★★`[{09-01, true}]`（**1要素**・OR）★★ |
| ★a3★ | ★同一日に正常行だけ★ | 09-01(95) + 09-01(99) | `[{09-01, false}]` |
| ★a4★ | ★★入力順が逆でも同じ★★ | a2 の2行を逆順で渡す | ★★a2 と同じ結果★★（★順序非依存★） |
| ★a5★ | 複数日 | 09-01(50) + 09-03(95) | `[{09-01,true},{09-03,false}]`（★日付昇順★） |
| ★a6★ | ★日付が降順で来ても昇順に並ぶ★ | 09-03 → 09-01 の順 | ★`[{09-01,...},{09-03,...}]`★ |
| ★a7★ | 未実施（`completion_rate === null`） | 09-01(null) | `[{09-01, true}]` |
| ★★a8★★ | ★★同一日・同一ロボットが2回（決定2）★★ | 09-01 02:00(20) + 09-01 22:00(20) | ★★`[{09-01, true}]`（**1要素**。★2要素にしない★）★★ |
| ★a9★ | 空 | `[]` | `[]` |

#### (b) ★`computeConsecutiveProblemDays()`★ — ★行id → 連続日数★

★★以下は**必ず全部入れること**★★（ユーザー指定）。

| # | ケース | 入力（施設 / 日 / 行id / 完了率） | ★期待（行id → 値）★ |
|---|---|---|---|
| ★★b1★★ | ★★2台・3日・毎日どちらか1台が問題★★ | F1: 09-01 `id1`(50 ★問題★) `id2`(95) ／ 09-02 `id3`(95) `id4`(40 ★問題★) ／ 09-03 `id5`(30 ★問題★) `id6`(99) | ★★`id1`=1 `id2`=1 / `id3`=2 `id4`=2 / `id5`=3 `id6`=3★★（★★3日連続★★。★同じ日の全行が同じ値★） |
| ★★b2★★ | ★★2台・同じ日に両方問題★★ | F1: 09-01 `id1`(50) `id2`(40) | ★★`id1`=1 `id2`=1★★（★★**2 にならないこと**★★。落とし穴1） |
| ★★b3★★ | ★★1台のみの施設（従来と同じ値）★★ | F1: 09-01 `id1`(50) / 09-02 `id2`(60) / 09-03 `id3`(95) | ★`id1`=1 / `id2`=2 / `id3`=**0**★（★既存テスト（`clean-log-mapping.test.ts:445` 付近）と同じ値になること★） |
| ★★b4★★ | ★★スケジュール非存在日を挟む（D-026）★★ | F1: 09-01 `id1`(50) / ★09-02 は行なし★ / 09-03 `id2`(50) | ★★`id1`=1 / `id2`=**2**★★（★途切れない★） |
| ★b5★ | ★施設が違えば混ざらない★ | F1: 09-01 `id1`(50) ／ F2: 09-01 `id2`(50) / 09-02 `id3`(50) | ★`id1`=1 / `id2`=1 / `id3`=2★ |
| ★b6★ | ★★同じ日に複数台・最新日が問題なし★★ | F1: 09-01 `id1`(50) / 09-02 `id2`(95) `id3`(99) | ★`id1`=1 / `id2`=**0** / `id3`=**0**★ |
| ★b7★ | ★★ロボットが違っても施設が同じなら連続する★★（★仕様の本体★） | F1: 09-01 `id1`(50, ロボットA) / 09-02 `id2`(50, ロボットB) | ★★`id1`=1 / `id2`=**2**★★（★★旧実装では両方 1 だった★★） |
| ★★b8★★ | ★★同一ロボットが同じ日に2回失敗（決定2）★★ | F1: 09-01 02:00 `id1`(20) / 09-01 22:00 `id2`(20) | ★★`id1`=1 / `id2`=**1**★★（★★既存テスト `clean-log-mapping.test.ts:502-509` の期待値 **2 → 1**★★） |
| ★b9★ | 未実施（`completion_rate === null`） | F1: 09-01 `id1`(null) | ★`id1`=1★ |
| ★b10★ | 空 | `[]` | ★`Map.size === 0`★ |
| ★b11★ | ★同じ日の行の並び順を変えても結果が同じ★ | b1 の入力を任意の順で渡す | ★b1 と同じ★ |

★★b7 と b8 が「旧実装で落ちる」テストです★★ — ★★実装前に書き、**落ちることを確認してから**直すこと★★。

★★完了率ちょうど 70.0 のケースはここには置かないこと★★ — ★段階4 の期待値表に置きます★
（★段階3 で書くと段階4 で書き直しになる★）。

#### (c) ★★決定4: 施設単位にした帰結（★意図した結果★。テストで固定する）★★

★施設単位にすると次の2つが起きます。★★どちらもユーザー承認済みで、直しません★★★ —
★★「気づいた人が元に戻さない」ように、テストで固定してコメントに理由を書くこと★★。

| # | 帰結 | 入力例 | ★期待★ |
|---|---|---|---|
| ★c1★ | ★★正常終了した行にも R3/R4 が出る★★ | ★施設の連続が3日（b1 の `id6`。完了率99で正常終了）★ → `matchAdviceRules({ departed: true, completionRate: 99, consecutiveProblemDays: 3 })` | ★`["R3","R4"]` を含む★（★★これが正しい。R3/R4 は「現場に行くか」の判断なので施設単位★★） |
| ★c2★ | ★★別の台のせいで、その台にとって初回の出発失敗でも R1 が出ない★★ | `matchAdviceRules({ departed: false, completionRate: null, consecutiveProblemDays: 2 })` | ★`"R1"` を**含まない**／`["R3"]` を含む★（★`lib/judge/advice.ts:61` の `consecutiveProblemDays <= 1`★） |

★★`lib/judge/advice.ts` は変更しないこと★★。★このテストは既存の関数の**挙動を記録するため**に書きます★。
★置き場所は `lib/judge/__tests__/advice.test.ts`（既存があればそこ）★。

### 3-2. テストを書く

- ★`lib/db/__tests__/clean-log-mapping.test.ts` の `describe("computeConsecutiveProblemDays")`（`:426`）に足す★
- ★畳み込み関数の `describe` を新設する★
- ★★既存テスト（`:426-512`）は**消さない**★★。★`ConsecutiveInput` のフィールド名変更に追従させるだけ★
  （★ヘルパー `row(id, key, date, rate)` の第2引数がロボットの識別子（`"S1"`）から施設id（`1`）に変わる★）
- ★★例外は `:502-509`（「同じ日に複数回の清掃がある場合はそれぞれ1回分として数える」）★★ —
  ★★期待値を **2 → 1** に直し、テスト名を「同じ日に複数回清掃しても1日として数える（決定2。日単位）」に変える★★。
  ★コメントに「D-026『連続はスケジュール存在日の連続』＝日単位。2026-09-11 ユーザー確定」と書くこと★
- ★`lib/db/__tests__/report-mapping.test.ts:238`（`consecutiveProblemDaysAtPeriodEnd`）も
  ★型変更に追従させる★。★★期待値は変えないこと★★（同関数の規則は不変）

### 3-3. 実装

1. `lib/db/clean-log-mapping.ts`
   - `ConsecutiveInput`（`:285`）の ★`serial_number: string` → `facility_id: number`★ に置き換える
     （★docstring も直すこと★。★「P1 は 1清掃エリア = 1ロボットなので serial_number で代用できる」という
     ★**現状の説明は誤り**★ — ★1施設に複数の清掃エリアが実在する（施設5 = 2台 / 施設7 = 3台）★）
   - ★畳み込み関数を新設する★（例）:
     ```ts
     /** 同一日の複数行を OR で1日に畳む（A案。ユーザー確定 2026-09-11 / D-109） */
     export function foldProblemDaysByDate(
       rows: readonly ConsecutiveInput[]
     ): ScheduleDayResult[]
     ```
     - ★日付は `scheduled_at.slice(0, 10)`（現状と同じ）★
     - ★日付昇順・重複なし★
     - ★`isProblem` は `isProblemCompletion()` の OR★
     - ★★docstring に「なぜ畳むのか」を残すこと★★:
       ★`countConsecutiveProblemDays()` の契約は1日1エントリであり、
       同じ日付の要素を複数渡すと日数が水増しされる★ / ★A案の根拠（ユーザー回答 2026-09-11 / D-109）★ /
       ★B案（全台問題のときだけ問題日）を採らない理由（★3台ある施設がほぼ永久に
       エスカレーションしなくなる★）★ / ★★同一ロボットが同じ日に2回清掃した場合も1日に畳む（決定2）★★
   - `computeConsecutiveProblemDays()`（`:317`）
     - ★分割キーを `row.facility_id` にする★（★`Map<number, ConsecutiveInput[]>` でよい★。
       ★変数名 `bySection` は実態に合わないので `byFacility` 等に改めること★）
     - ★施設ごとに `foldProblemDaysByDate()` で `days` を作る★
     - ★行 → その行の日付が `days` の何番目かを引き、`countConsecutiveProblemDays(days.slice(0, i + 1))` を入れる★
       （★★同じ日の全行に同じ値が入る★★）
     - ★★畳みはこの関数の内部だけで行うこと★★（★呼び出し側でやると規則が2箇所に散る。D-103★）
     - ★docstring を更新する★: ★「施設単位（`CLAUDE.md` §4.5 の R3/R4 は『同一施設で』）」★ /
       ★「同一日は OR で畳む（A案）」★ / ★D-026 は引き続き成立（行が無い日は自然に飛ぶ）★ /
       ★★決定4（正常終了した行にも L3 が出る／別の台のせいで R1 が出なくなる）を「意図した結果」として書く★★
2. `lib/db/index.ts`
   - `cleanLogs()`（`:348`）: ★`drafts` は既に `facility_id: Number(row.facility_id)` を持つので**変更不要**★
     （★型が通ることを確認する★）
   - `facilityPerformance()`（`:934-946`）: ★`serial_number: String(row.facility_id)` の行（`:940`）を**削除**する★。
     ★`facility_id: Number(row.facility_id)` だけで分割キーになる★。
     ★★動作は不変★★（★同じ施設で同じ分割になる★）。
     ★D-108 項3 が「`serial_number` の代わりに `String(facility_id)` を詰めている」と記録した状態を解消する★
   - `lib/db/report-mapping.ts` の `FacilityConsecutiveEntry`（`:241`）:
     ★docstring から「`ConsecutiveInput.serial_number` は分割キーとして使われるだけなので
     `String(facility_id)` を入れてよい」という説明を消す★（★もう詰め替えないため★）。
     ★型を `ConsecutiveInput` と統合するかは実装者判断★ —
     ★★ただし `consecutiveProblemDaysAtPeriodEnd()` の**動作は変えないこと**★★

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

- ★期待値表 (a) 9ケース / (b) 11ケース / (c) 2ケースがテストになっており、全部通る★
- ★★`b7` / `b8` が実装前に落ち、実装後に通ることを確認した★★
- ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★
- ★`git diff --stat` が ★`lib/db/clean-log-mapping.ts` / `lib/db/report-mapping.ts` / `lib/db/index.ts` /
  `lib/db/__tests__/clean-log-mapping.test.ts` / `lib/db/__tests__/report-mapping.test.ts` /
  `lib/judge/__tests__/advice.test.ts`（(c) の置き場所）の6ファイルだけ★★
- ★★コミット（ファイルパスを明示）★★
  - 例: `git commit -m "fix: 指示書V 段階3 連続問題日数を施設単位にし同一日をORで畳む"`

---

## 段階4 — ★決定3: 完了率ちょうど 70.0% を「問題あり」に直す★（1コミット）

### 4-1. ★★期待値表★★

対象: `isProblemCompletion(completionRate)`（`lib/db/clean-log-mapping.ts:280`）

| 完了率 | ★変更前★ | ★★変更後（期待）★★ | 判定（`judgeGrade`） |
|---|---|---|---|
| `null`（未実施） | true | ★true★ | × |
| 0 | true | ★true★ | × |
| 9.9 | true | ★true★ | × |
| 10.0 | true | ★true★ | △ |
| 69.9 | true | ★true★ | △ |
| ★★70.0★★ | ★false★ | ★★**true**★★ | ★★△（＝「△以下」なので問題あり）★★ |
| 70.1 | false | ★false★ | ○ |
| 90.0 | false | ★false★ | ○ |
| 90.1 | false | ★false★ | ◎ |

★★`judgeGrade()` の期待値は1つも変わりません★★（★あちらは「○ = 70%超」で既に正しい★。落とし穴11）。

★連続日数への波及（★これもテストにすること★）★:

| # | ケース | ★期待★ |
|---|---|---|
| ★d1★ | F1: 09-01 `id1`(70.0) | ★`id1` = **1**★（★変更前は 0★） |
| ★d2★ | F1: 09-01 `id1`(70.0) / 09-02 `id2`(70.1) | ★`id1` = 1 / `id2` = **0**★（★70.1 は ○ なので問題なし★） |

### 4-2. テストを書く

- ★`lib/db/__tests__/clean-log-mapping.test.ts` の `describe("isProblemCompletion")`（`:392`）★:
  - ★`:393-402` のコメント表を直す★（★`| 70.0 | true | △なので問題あり |`★）
  - ★`:410`（`it.each` の表。`:404` から始まる）の `[70.0, false]` を ★`[70.0, true]`★ に直す★
  - ★★`:418-423` の「★70.0 の扱いは未確定（fixtures の式に合わせている。決定台帳に記載）★」テストは
    **期待値を true に差し替え、テスト名とコメントを書き換える**★★:
    - ★テスト名の例: 「★完了率ちょうど 70.0 は △ なので「問題あり」（決定3。2026-09-11 ユーザー確定）★」★
    - ★コメントに「`CLAUDE.md` §4.1「△ = 10%以上70%以下」/ §4.5「△以下」。`judgeGrade()` の
      『○ = 70%超』とは別物なので `judgeGrade()` は変えない」と書くこと★
- ★`describe("computeConsecutiveProblemDays")` に d1 / d2 を足す★
- ★この時点で 70.0 のケースが**落ちる**ことを確認してから実装に進む★

### 4-3. 実装

- `lib/db/clean-log-mapping.ts` の `isProblemCompletion()`（`:280-282`）:
  ```ts
  return completionRate === null || completionRate <= GRADE_THRESHOLDS.good;
  ```
  ★★`70` を直書きしないこと★★（禁止事項5）。★★`<` を `<=` にするだけ★★。
- ★★docstring（`:268-279`）の「★未確定として残る点★」の段落を書き換えること★★:
  - ★「`CLAUDE.md` §4.1 の △ は 10%以上**70%以下**、§4.5 の R3/R4 は『△以下』なので、
    ちょうど 70.0 は問題あり。決定3（2026-09-11 ユーザー確定）で確定した」★
  - ★「`judgeGrade()` の『○ = 70%超』とは別の判定。あちらは変えていない」★
  - ★★「fixtures（`lib/fixtures/clean-logs.ts:96`）の手書き `consecutive_problem_days: 1` と整合が取れた」★★
  - ★★「`lib/fixtures/clean-logs.ts:226` の生成式は `<` のままだが、生成される完了率に 70.0 ちょうどは
    現れないので矛盾しない」★★（★`lib/fixtures/` は変更禁止★）
- ★★`lib/judge/` は1文字も変えないこと★★

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

- ★期待値表9行 + d1 / d2 がテストになっており、全部通る★
- ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★
- ★★`lib/fixtures/` に差分が無い★★
- ★`git diff --stat` が ★`lib/db/clean-log-mapping.ts` / `lib/db/__tests__/clean-log-mapping.test.ts` の2ファイルだけ★★
- ★★コミット（ファイルパスを明示）★★
  - 例: `git commit -m "fix: 指示書V 段階4 完了率ちょうど70.0を問題ありにする"`

---

## 段階5 — ★決定5: 検証データをシードに足して再シードする★（1コミット）

### 5-1. ★なぜ必要か★

★★現シードでは「日替わりで別の台が問題 → 3日連続」を実データで再現できません★★:

- ★複数ロボットの施設は施設7（3台）だけ★
- ★★複数台の予定がある日は 2026-09-09 の1日だけ★★ → ★「連続」を作れない★
- ★施設5 は robots が2台いるが、section 106 に `clean_timetables` が1行も無く実質1台★

### 5-2. ★足すもの（★これをそのまま実装すること★）★

★★施設7 の section 107（A）と section 108（B）に、3日ぶんの本清掃行と履歴を足す★★:

| 日 | section 107（A） | section 108（B） | ★期待する連続日数★ |
|---|---|---|---|
| ★1日目★ | ★完了率 50（**問題**）★ | 完了率 95 | ★★両方 1★★ |
| ★2日目★ | 完了率 95 | ★完了率 40（**問題**）★ | ★★両方 2★★ |
| ★3日目★ | ★完了率 30（**問題**）★ | 完了率 99 | ★★両方 3（L3）★★ |

★★これがユーザー確定ケース（1日目Aが問題 / 2日目Bが問題 / 3日目Aが問題 → 3日連続）そのものです★★。

★日付★: ★★施設7 に既存の予定が無い日を選ぶこと★★。
★作成時点の実測では、施設7 の予定は「★09-04 まで section 107 のみ / 09-09 に 107・108・109★」なので、
★★`dateOnly(-4)` / `dateOnly(-3)` / `dateOnly(-2)` の3日が空いています★★
（★シード実行日からの相対。`lib/fixtures/_helpers.ts` の `dateOnly()` / `at()` と同じ作法★）。
★★着手時に段階0 のクエリで空いている日を再確認すること★★（★埋まっていたらずらす★）。

★★連鎖の帰結（★必ず理解してから実装すること★）★★:
★既存の `dateOnly(-1)`（＝ 2026-09-09 相当）の3行（62 / 94 / 88）は OR で「問題あり」になる★ので、
★★施設7 の連続は 3日目の 3 に続いて **4** になります★★ →
★★施設7 の当週・当月の `consecutive_problem_days` は **4**★★（★段階3 時点の 1 から更に変わる★）。

### 5-3. ★実装（`scripts/seed-local-db.ts`）★

1. ★★追加行の定義を定数として置く★★（★ファイル上部の他の定数群（`:208-330` 付近）に揃える★）:
   - ★serial_number で指定する★（★`robotBySerial` / `scheduleIdBySerial` で施設id・清掃エリアid・
     スケジュールidを引く★ — ★既存の作法と同じ★）
   - ★★見つからなければ `throw` する（fail-loud）★★。★静かにスキップしないこと★
   - ★★docstring に「なぜこのデータが要るのか」を書くこと★★:
     ★「複数台の施設で『日替わりで別の台が問題 → 3日連続』を再現するための検証データ（決定5。D-109）」★ /
     ★「fixtures には無い（`lib/fixtures/` は変更禁止）」★
2. ★★投入は本ループ（`:735-925`）の**後ろ**に独立したブロックとして書くこと★★
   （★★落とし穴12。本ループの中に混ぜると `historyIndex` / `errorReportCount` の通番がずれ、
   既存行の `clean_error` 分布（225/13/5/5/5）と対応状況が全部変わります★★）
3. ★id は明示採番する★（★冪等性★）:
   - ★★fixtures の id 帯（1001〜1280）と、事前チェックのオフセット（10万 / 20万）を避けること★★
   - ★例: `1501` から連番（`1501`〜`1506`）★。★`clean_status_histories.id` は本清掃行と同じ id（1清掃=1履歴）★
4. ★行の中身★:
   - `clean_timetables`: ★`precheck = CLEAN_TIMETABLE_PRECHECK.main` / `parent_id = 0` /
     `end_at = start_at + MAIN_CLEAN_DURATION_MINUTES`★（★既存と同じ作法★）
   - `clean_status_histories`: ★`clean_error = CLEAN_ERROR.normal`（＝実施済み）★ /
     ★`task_area = TASK_AREA_SQM` / `clean_area = task_area × 完了率 ÷ 100` /
     `clean_ratio = 完了率`（★丸めない生値★）★ / ★`started_at` / `ended_at` は予定の時刻★
   - ★★事前チェック行（`precheck != 0`）と `robot_error_reports` は作らないこと★★（落とし穴13。
     ★作ると `prechecks()` の 27 件が変わる★）
   - ★★`clean_statuses` にも入れないこと★★（★当日分だけの「今の状態」テーブル。追加行は過去日★）
5. ★投入件数をログに出す★（★既存の `console.log` の作法に揃える★）
6. ★★乱数を使わないこと★★。★★2回実行して同じ結果になること★★

### 5-4. 再シードと実測

```
npm run seed:local     # ★package.json の実際のスクリプト名を確認すること★
```

★再シード後に実測して控える★:

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

★★期待★★:

| # | 項目 | ★期待★ |
|---|---|---|
| ★1★ | `main_rows` | ★★**253 + 6 = 259**★★（★追加行数と一致すること★） |
| ★2★ | `clean_error` 分布 | ★★`0` = 225 + 6 = **231** / `1`=13 / `2`=5 / `3`=5 / `4`=5★★（★★1〜4 が1件も動いていないこと★★。動いていたら落とし穴12） |
| ★3★ | 同一日に複数行がある施設・日 | ★★施設7 の**4日**（追加3日 + 既存の 09-09）★★ |
| ★4★ | ★2回シードしても同じ★ | ★もう一度 `npm run seed:local` を流し、★1〜3 が同じ値★になること★ |

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

- ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★（★テストは増減しない段階★）
- ★5-4 の4項目を実測した★
- ★`git diff --stat` が ★`scripts/seed-local-db.ts` の1ファイルだけ★★
  （★★`lib/fixtures/` に差分が無いこと★★）
- ★★コミット（ファイルパスを明示）★★
  - 例: `git commit -m "chore: 指示書V 段階5 複数台の連続日数を検証するシードデータを足す"`

---

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

### 6-1. ★連続日数の検証（★DBを変更せずにできる★）★

★★`.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();
console.log('cleanLogs', logs.length, 'prechecks', pre.length,
  'launch_failed', pre.filter(p => p.severity === 'launch_failed').length);
const f7 = logs.filter(r => r.facility_id === 7);
console.log('施設7', f7.map(r => [r.id, r.scheduled_at.slice(0,10), r.completion_rate,
  r.consecutive_problem_days, r.escalation_level]));
const f5 = logs.filter(r => r.facility_id === 5 && r.completion_rate === 70);
console.log('施設5 の70.0行', f5.map(r => [r.id, r.scheduled_at.slice(0,10),
  r.consecutive_problem_days, r.escalation_level]));
const fp = await db.facilityPerformance();
console.log('当期の施設別', fp.filter(r => r.year >= 2026)
  .map(r => [r.period, r.item.facility_id, r.item.consecutive_problem_days, r.item.status]));
"
```

★`npx tsx` でエイリアス（`@/`）が解決できない場合★は ★dev サーバ＋curl に切り替えてよい★
（`/api/clean-logs?per_page=300` / `/api/reports/facility-performance?limit=50`）。
★★その場合も下の期待値をすべて実測すること★★。

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

| # | 項目 | ★期待★ |
|---|---|---|
| ★1★ | `cleanLogs()` の件数 | ★★**259**（＝ 253 + 段階5 の追加6行）★★ |
| ★2★ | `prechecks()` の件数 / `launch_failed` | ★**27 / 13**（★段階5 でも変わっていないこと★）★ |
| ★3★ | ★★施設7 の追加3日★★ | ★★1日目の2行が**両方 1** / 2日目が**両方 2** / 3日目が**両方 3**★★（★★決定5 のケースがそのまま出ること★★） |
| ★4★ | ★★施設7 の 09-09 相当の3行★★ | ★★`id=1026` / `1027` / `1028` が**すべて 4**★★（★完了率は 62.0 / 94.0 / 88.0 のまま★） |
| ★5★ | ★★施設7 の当週・当月の `consecutive_problem_days`★★ | ★★**4**（★修正前は 0★）★★ |
| ★6★ | ★★施設5 の 70.0 の行★★ | ★★`consecutive_problem_days` = **2** / `escalation_level` = **L2**★★（★修正前は 0 / L1相当★。決定3） |
| ★7★ | ★★施設5 の当週・当月の `consecutive_problem_days`★★ | ★★**2**（★修正前は 0★）★★ |
| ★8★ | ★他施設の連続日数★ | ★★段階0 で控えた修正前の値と**同じ**★★（★動くのは施設7 と施設5 だけ★） |
| ★9★ | ★連続日数が「3」に化けていないこと★ | ★★施設7 の 09-09 相当の日が **3**（＝同一日の3行を3日と数えている）になっていたら畳み込みを忘れている★★（落とし穴1） |
| ★10★ | ★`status` に「判定なし」が出ていないこと★ | ★★この時点では未来行が0件なので、当期の全施設に稼働率がある＝「判定なし」は出ないはず★★（★出たら 6-2 の前に原因を調べる★） |

### 6-2. ★不具合A・決定1 の検証（★未来行を一時投入する。★必ず消すこと★）★

★★`dwatch_local` は複数セッションから共有されています（D-108 項8）★★ —
★★投入前に他セッションが使っていないか確認し、検証後は**必ず削除すること**★★。

★手順★:

1. ★投入前の値を控える★:
   ```
   USE_FIXTURES=false DATABASE_URL="mysql://root@localhost:3306/dwatch_local" npx tsx -e "
   import { db } from './lib/db';
   const k = await db.reportKpis();
   console.log(JSON.stringify(k.filter(r => r.year === 2026 && r.month === 9), null, 1));
   const fp = await db.facilityPerformance();
   console.log('当期の件数', fp.filter(r => r.period === 'weekly').length,
     fp.filter(r => r.period === 'monthly').length);
   "
   ```
2. ★未来行（明日ぶん）を投入する★ — ★★id を `990000 + facility_id` に固定して、消しやすくする★★:
   ```
   "/c/Program Files/MySQL/MySQL Server 8.4/bin/mysql.exe" -u root --default-character-set=utf8mb4 dwatch_local -e "
   INSERT INTO clean_timetables
     (id, facility_id, section_id, clean_schedule_id, parent_id, start_at, end_at, precheck, inactive)
   SELECT 990000 + t.facility_id, t.facility_id, t.section_id, t.clean_schedule_id, 0,
          TIMESTAMP(DATE_ADD(CURDATE(), INTERVAL 1 DAY), TIME(t.start_at)),
          TIMESTAMP(DATE_ADD(CURDATE(), INTERVAL 1 DAY), TIME(t.end_at)),
          0, 0
   FROM clean_timetables t
   JOIN (SELECT facility_id, MAX(start_at) ms FROM clean_timetables WHERE precheck=0 GROUP BY facility_id) m
     ON m.facility_id = t.facility_id AND m.ms = t.start_at
   WHERE t.precheck = 0;
   SELECT COUNT(*) AS inserted FROM clean_timetables WHERE id BETWEEN 990001 AND 990099;"
   ```
   ★★投入行数を控えること★★（★作成時点の見込みは **11行**（施設12 はロボット0台で予定が無い）。
   ★実測値を記録すること★）。
   ★★明日が当週の外（今日が土曜）だと当週の分母が増えません★★ — ★その場合は当月で確認するか、
   `INTERVAL 1 DAY` を当週内に収まる値に変えること★。
3. ★★もう一度 `reportKpis()` / `facilityPerformance()` を実測する★★:
   - ★★**稼働率・出発成功率が 1 でも変わっていたら不具合が残っています**★★
   - ★修正が効いていれば、★★未来行を入れる前と**まったく同じ値**★★ になります
   - ★★当期の施設別パフォーマンスの**件数も変わらない**こと★★（★決定1。未来行があってもバケットは作る★）
4. ★★決定1 の本体を確認する★★ — ★「未来行しか無い施設」を作って、行が消えないことを見る★:
   ```
   "/c/Program Files/MySQL/MySQL Server 8.4/bin/mysql.exe" -u root --default-character-set=utf8mb4 dwatch_local -e "
   SELECT id, facility_id, start_at FROM clean_timetables WHERE id BETWEEN 990001 AND 990099 ORDER BY facility_id;"
   ```
   - ★★当週の範囲内で、その施設の**過去の行が1件も無い**状態を作る★★のが確実な確認方法です。
     ★例: 週の初めでなければ、投入した未来行の施設のうち1つについて、当週の過去行を
     一時的に別テーブルへ退避する…といった操作は**しないこと**（壊す危険が大きい）★
   - ★★代わりに、`aggregateCleanStatsByFacility()` の**単体テスト #11**（段階2）で担保します★★。
     ★実測では「件数が減らないこと」だけ確認すれば十分★
   - ★★どうしても実データで見たい場合★★は、★★週の初め（日曜）／月の初め（1日）に再実測する★★と
     自然に再現します。★その場合は「施設が消えないこと」「`status` が『判定なし』になること」を記録する★
5. ★★**必ず削除する**★★:
   ```
   "/c/Program Files/MySQL/MySQL Server 8.4/bin/mysql.exe" -u root --default-character-set=utf8mb4 dwatch_local -e "
   DELETE FROM clean_timetables WHERE id BETWEEN 990001 AND 990099;
   SELECT COUNT(*) AS main_rows FROM clean_timetables WHERE precheck=0;
   SELECT COUNT(*) AS future_main FROM clean_timetables WHERE precheck=0 AND start_at > NOW();"
   ```
   ★★`main_rows` = **259**（段階5 の後の値）/ `future_main` = **0** に戻ったことを確認すること★★

★★参考（修正前の壊れ方。★確認は任意★）★★: ★修正前のコードで同じ未来行を入れると、
稼働率・出発成功率が下がります（ユーザー実測: 78.57% → 55.00%）★。
★確認したい場合は段階2 のコミットを一時的に `git stash` して測り、★★必ず戻すこと★★★。

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

| 画面 | 期待 |
|---|---|
| ★`/detail-log`★ | ★200 / ★**259件**（段階5 で6件増えた後の値）★。★✗（未出発）が **13件**★ |
| ★`/dashboard`★ | ★200 / **9 / 9**★（D-099） |
| ★`/alerts`★ | ★200 / **27件**（`launch_failed` 13）★ |
| ★`/reports`★ | ★200（KPI・施設別・エラー内訳）★。★★「連続問題施設」セクションは **500（未実装。第6歩）**★★ |
| `/facilities` / `/schedules` / `/ds-logs` / `/ds-alerts` | ★200。回帰していないこと★ |

- ★★施設別パフォーマンス表のステータス列に「判定なし」バッジの見た目を確認できるなら見ておくこと★★
  （★未来行が無い状態では出ないので、★出なくてよい★。★★単体テストで担保済み★★）
- ★★**404 が出たら Edge Runtime バンドル破壊の回帰を疑うこと**★★（D-086）。
  ★500 = 想定どおりの未実装 / 404 = ビルドが壊れている★
- ★Browser pane（`preview_start`）が使えなければ curl で代替し、★目視できなかった項目を必ず記録する★★
- ★★`next dev` は `.env.local` を共有する別セッションに影響します（D-108 項8）★★ —
  ★`next build && next start -p <別ポート>` で自分専用のサーバーを起てるほうが安全★

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

- ★`.env.local` を触っていないなら、そのまま `USE_FIXTURES=true` で起動するだけ★
- ★★fixtures 側の件数・判定が1つも変わっていないこと★★（禁止事項10）
  - ★`lib/fixtures/clean-logs.ts` の `consecutive_problem_days` は**手書きの値**であり、
    ★★段階3・段階4 の変更の影響を受けません★★（★fixtures は計算していない★）
  - ★★だからこそ、段階3・4・5 の効果は `USE_FIXTURES=false` でしか見えません★★ —
    ★★動作確認は必ず `USE_FIXTURES=false` で行うこと★★
  - ★施設別パフォーマンスの `status` も fixtures では手書きなので「判定なし」は出ません★（★正常★）

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

- ★6-1 の10項目・6-2 の手順1〜5・6-3 の回帰確認・6-4 をすべて実測した★
- ★★投入した一時行を削除し、`main_rows` = 259 / `future_main` = 0 に戻したことを確認した★★
- ★★`.env.local` が `USE_FIXTURES=true` のままであることを確認した★★
- ★`git status` に `lib/` `scripts/` `docs/` `components/` `d-watch-api.yaml` 以外の差分が無いことを確認した★
  （★`README.md` / `docs/code-map.md` / `docs/handoff/管理画面_設計検討_20260910.md` は
  ★**着手前と同じ状態のまま**★であること。禁止事項2）

---

## 段階7 — 記録と push（1コミット）

### 7-1. `docs/decisions.md` に追記（★`D-110` の見込み。★着手直前に実測した番号を使うこと★）

★★`D-109` は別担当が「本書の5決定」として追記済みです（`docs/decisions.md:6328`。★作成時点では未コミット★）★★ — ★★その番号を奪わないこと★★。
★見出しの形式は既存に揃える★: `### D-110 \`[確定]\` 指示書V（連続日数の集計単位と当期の分母）を実行した`

★最低限入れる項目★:

| # | 項目 |
|---|---|
| 1 | ★★決定1 の実装: 施設別パフォーマンスに4つ目のステータス（「判定なし」）を足した★★。★`d-watch-api.yaml` → 型 → `judgeFacilityStatus()` → 画面の順（`CLAUDE.md` §6 作業ルール1）★。★★モックに無い表示を足した★★こと。★`FACILITY_STATUS_THRESHOLDS` は変えていない★ |
| 2 | ★不具合A: 当期KPIの分母に未来の予定が入っていた。`aggregateCleanStatsByFacility()` に `now`（★必須引数★）を渡し `isFutureSchedule()` を適用した。呼び出し4箇所（`lib/db/index.ts:805` / `:813` / `:1052` / `:1058`）★ |
| 3 | ★★決定1 の肝: 未来の行でも**バケットは作る**（加算だけスキップ）★★。★行ごと捨てると週の初め・月の初めに施設が一覧から消える（実測: 10施設 → 0施設）★ |
| 4 | ★不具合B: 連続問題日数の分割キーを `serial_number`（ロボット単位）から `facility_id`（施設単位）に変えた。★`CLAUDE.md` §4.5 の R3/R4「同一施設で」に合わせた★★ |
| 5 | ★★A案の確定（ユーザー回答 2026-09-11。原文を引用すること）★★: ★同一日に複数台の予定があるとき、1台でも問題があればその日は問題日★。★B案（全台が問題のときだけ）を採らない理由: 3台ある施設がほぼ永久にエスカレーションしなくなる★ |
| 6 | ★★決定2: 同一ロボットが同じ日に2回清掃した場合も「1日」に畳む★★（★既存テストの期待値を 2 → 1 に変えた。根拠は D-026＝日単位★） |
| 7 | ★★`countConsecutiveProblemDays()` は日付を見ないので、畳み込みを入れないと同一日の複数行が複数日として数えられる★★（★今回の修正で初めて表面化した構造★） |
| 8 | ★★決定3: 完了率ちょうど 70.0 を「問題あり」に直した★★（★`isProblemCompletion()` を `<=` に。`GRADE_THRESHOLDS` から読む。`judgeGrade()` は変えていない★）。★実データへの影響: 施設5 の期末連続が 0 → 2（L2）★ |
| 9 | ★★決定4: 正常終了した行にも R3/R4 が出る／別の台のせいで初回の出発失敗に R1 が出ない — **意図した結果として受け入れた**★★（★テストで固定した★） |
| 10 | ★★決定5: 施設7 に複数日×複数台の検証データをシードへ足した★★（★追加行数・id 帯・本ループの後ろに置いた理由（通番ローテーションを崩さない）★） |
| 11 | ★★レポート側の値が変わったこと★★: ★施設7 の当週・当月が 0 → 4 / 施設5 が 0 → 2★。★集計単位は変えていない（元から施設単位）が、日単位の畳み込みと 70.0 の修正は両画面に効く★ |
| 12 | ★段階6 の実測値（`cleanLogs` 259 / `prechecks` 27 / `launch_failed` 13 / `clean_error` 分布 / テスト数 / 未来行投入時に率も件数も変わらないこと）★ |
| 13 | ★★D-108 項7 の訂正★★（★★D-109 §6 で訂正済みのはず。二重に書かず参照する★★。下記 7-2） |
| 14 | ★申し送り（下記「申し送り」節をそのまま移す）★ |

### 7-2. ★★D-108 項7 の訂正（★★既に D-109 §6 で訂正済みのはず。まず確認すること★★）★★

★★最初に `docs/decisions.md` の `D-109` を読むこと★★ —
★作成時点（2026-09-11）で、★★D-109 の「#### 6. D-108 の記述の訂正」に本節と同じ訂正が既に入っています★★★。

- ★★入っていれば、D-110 では**繰り返さず、1行で参照するだけ**にすること★★
  （例: 「★D-108 項7 の訂正は D-109 §6 のとおり。本作業でも同じ実測値を再確認した★」）。
  ★★台帳に同じ訂正を二重に書かないこと★★
- ★入っていなければ★、下の内容を D-110 に書くこと

★以下は訂正の根拠（★実測値。段階6 で再確認してもよい★）★。
★D-108 項7（`docs/decisions.md:6218` 以降。★行番号は実測すること★）は次のように書いていますが、
★★因果の部分が事実と違います★★:

> 清掃結果エラー用に本作業がシードした英語識別子（例:「EmergencyStop」）が同じ`robot_error_reports`
> テーブルに混在している

★★実測（2026-09-11）★★:

| テーブル | `error_type` の実際の値 |
|---|---|
| ★`robot_error_reports`★ | ★オフライン(6) / セキュリティ有効(4) / ★`Handrail Lifted`(2)★ / 充電中(1) / 緊急停止(1)★ |
| ★`facility_error_summaries`★ | ★`EmergencyStop` / `LostLocalization` / `RobotHardwareErrorToAdmin` / `LowBatteryLevel` / `CleanWaterEmpty` / `TrashFull` / `CanNotReach` / `UnknownErrorFromCloud` / ★空文字★（各2件）★ |

★★訂正内容★★:

- ★★指示書T がシードした英語識別子（`EmergencyStop` 等）は `facility_error_summaries`（＝**確定期間側**）にしかなく、
  `robot_error_reports`（＝**当期側**）には入っていない★★
- ★ただし `robot_error_reports` には指示書K 由来の ★`Handrail Lifted`★ が2件あり、
  ★★「日本語と英語識別子が混在する」こと自体は事実★★（★D-078 項2 / D-088。`lib/config/robot-status-match.ts:35` の
  `B2_HANDLE_UP_STATUSES`（暫定値）と対応する値★）
- ★★したがって「日英が混在する」という結論は変わらない。★誤りは「指示書T のシードが `robot_error_reports` に
  英語識別子を入れた」という**因果の部分だけ**★★★
- ★★方針（D-077。生値のまま表示する / 推測でマッピングしない）は変わらない★★

★訂正の書き方★: ★★D-108 の本文を書き換えないこと★★（★決定台帳は追記型。過去の記録を消さない★）。
★D-108 項7 の末尾に「★訂正あり → D-109★」（★D-109 に入っていなければ `→ D-110`★）の
1行だけ足すのは可★。

### 7-3. `docs/handoff.md` に追記（★追記33★）

- ★見出しは `## 2026-09-1X（追記33）`★（★番号は実測すること★。★★別担当が先に追記33 を使っていたら34★★）
- ★冒頭の「最新の追記」ブロック（`docs/handoff.md:6` 付近）も更新すること★
- ★入れる内容★:
  - ★★「連続問題日数は**施設単位**。同一日は1台でも問題ならその日は問題日。同一ロボットの2回清掃も1日」★★
  - ★「当期のKPIは未来の予定を分母に入れない。★★ただしバケットは作る（作らないと週の初めに施設が消える）★★」★
  - ★★「稼働率が算出できない期間のステータスは『判定なし』（4つ目）」★★
  - ★「完了率ちょうど 70.0 は問題あり（△以下）」★
  - ★次に触る人が踏みやすい落とし穴（★`countConsecutiveProblemDays()` は日付を見ない★ /
    ★シードの通番ローテーションに行を差し込まない★）★
  - ★申し送り★

### 7-4. push とこの段階の完了条件

- ★★コミット（ファイルパスを明示。`docs/decisions.md` と `docs/handoff.md` のみ）★★
  - 例: `git commit -m "docs: 指示書V完了(D-110)を記録する"`
  - ★★`docs/decisions.md` は別担当の `D-109`（作成時点で**未コミット**）が同じ作業ツリーに入っています★★ —
    ★★`git add docs/decisions.md` は D-109 ごと巻き込みます★★。
    ★着手時に D-109 が既にコミット済みか確認し、★★未コミットなら、その担当が先にコミットするのを待つか、
    巻き込むことを承知のうえでコミットメッセージに明記すること★★★
  - ★★コミット前に `git pull --rebase` の要否を確認し、★競合したら自分の追記だけを残して番号を取り直すこと★★★
- ★`git push origin develop`★
- ★★本書の冒頭表と末尾「実行状態」表を更新すること★★
- ★★`README.md` / `docs/code-map.md` / `docs/handoff/管理画面_設計検討_20260910.md` /
  `.claude/worktrees/**` に差分が無いことを最後に確認すること★★

---

## ★★申し送り — レビューで挙がった未修正の論点★★

★★以下は本指示書では直しません。★★ ★D-110 に列挙して残すこと★。

| # | 論点 | 状態 |
|---|---|---|
| ★①★ | ★★当期は稼働率と出発成功率が**必ず同値**になる★★ — `kpiFromCleanStats()`（`lib/db/report-mapping.ts:208`）が `operationRate` と `launchRate` に ★同じ分子（`bucket.executed`）・同じ分母（`bucket.planned`）★ を渡している。★「定刻より遅れて出発した」を区別する情報がDBに無いため★（同ファイル `:69-79` の docstring に既に明記あり） | ★★**ユーザーが定義を検討中。本指示書では直さない**★★ |
| ★②★ | ★★エラー率の分子が「事前チェック行に紐づくエラー」限定★★ — `errorCountByMainId()`（`:147`）は `robot_error_reports.clean_timetable_id` が★事前チェック行★を指す前提で本清掃行に付け直している。★清掃中に発生したエラーが本清掃行に直接紐づく場合、そのエラーは**数えられず**当期のエラー率が 0% になる★。★★fail-silent（例外もテスト失敗も出ない）★★ | ★実データで `clean_timetable_id` がどちらを指すかの確認が要る★ |
| ★③★ | ★D-108 項7 の記述の誤り★ | ★★D-109 §6 で訂正済み。段階7-2 で確認し、未記載なら D-110 に書く★★ |
| ★④★ | ★サマリ検算の 0.5 ポイント差★（D-108 項4）— シードが「率を先に丸めてから件数を作る」ためのダミーデータ側の誤差 | ★実装の問題ではない。★実データ接続後も当方の計算値で上書きしないこと★（D-102）★ |
| ★⑤★ | ★`periodRange()` が解けないと一覧APIが 500★ — `reportKpis()`（`lib/db/index.ts:801`）/ `facilityPerformance()`（`:992` / `:1031`）が `throw new Error(...)` する。★当期の期間キーは `weekOfMonth()` 自身が返した値なので通常は起きない★が、★★サマリ行の期間キーが不正だと一覧全体が落ちる★★ | ★「その行だけ落とす」に変えるか要検討★ |
| ★⑥★ | ★論理削除の扱いが3メソッドで不揃い★ — `cleanLogs()` / `prechecks()` は `deleted_at` で絞らない（該当テーブルに列が無いため。`lib/db/index.ts:214` / `:393`）が、`facilityPerformance()`（`:881`）は `facilities` / `robots` を `deleted_at: null` で絞る。★`clean_timetables.inactive` はどこでも絞っていない★ | ★方針として妥当かの確認が要る★ |
| ★⑦★ | ★★「判定なし」ステータスの算出基準そのもの★★ — `judgeFacilityStatus()` は ★暫定で稼働率ベース★（TeamWorks 回答待ち）。★本書は「分母0のときの扱い」だけを確定させた★ | ★算出基準が確定したら4値の割り当てごと見直す★ |

★他に本書の範囲外として残るもの★:

- ★`continuousProblems`（第6歩）★ — ★`USE_FIXTURES=false` で 500 のまま★（D-102 項1 / D-090 項13）
- ★タイムゾーン（DBがJSTかUTCか）★（D-106 項8）
- ★★`ConsecutiveInput` の全期間走査（性能）★★ — ★年間13万行では重い（D-092 項3-3）★。
  ★本書は走査回数を増やしも減らしもしない★（★畳み込みは施設ごとの1パスを足すだけ★）

---

## 完了条件（全体）

| # | 条件 |
|---|---|
| ★1★ | ★`FacilityStatus` に4つ目（「判定なし」）が足され、`d-watch-api.yaml` の2箇所の enum・型・判定・クエリ・画面がすべて追従している★（★「その他」「不明」という語を使っていない★） |
| ★2★ | ★`judgeFacilityStatus(null)` が `"bad"` ではなく4つ目の値を返す★。★`FACILITY_STATUS_THRESHOLDS` は変えていない★ |
| ★3★ | ★`aggregateCleanStatsByFacility()` が `now`（★必須引数★）を受け取り、未来の予定を分母に入れない★ |
| ★4★ | ★★未来の予定しか無い施設でも**バケットが作られる**（＝施設が一覧から消えない）★★ |
| ★5★ | ★呼び出し4箇所（`lib/db/index.ts:805` / `:813` / `:1052` / `:1058`）が `now` を渡し、★1回の集計で同じ `now` を使い回している★★ |
| ★6★ | ★`computeConsecutiveProblemDays()` の分割キーが `facility_id`★ |
| ★7★ | ★★同一日の複数行が OR で1日に畳まれ、同じ日の全行に同じ連続日数が入る★★（★同一ロボットの同日2回も1日★） |
| ★8★ | ★畳み込みが純粋関数として切り出され、`computeConsecutiveProblemDays()` の内部だけで使われ、テストで固定されている★ |
| ★9★ | ★`countConsecutiveProblemDays()` / `judgeGrade()` / `lib/judge/advice.ts` を1文字も変えていない★ |
| ★10★ | ★`facilityPerformance()` が `serial_number` への詰め替えをやめ、施設単位のまま動いている★ |
| ★11★ | ★`isProblemCompletion(70.0) === true`★。★閾値は `GRADE_THRESHOLDS` から読んでいる（`70` の直書きが無い）★ |
| ★12★ | ★決定4（正常終了行にも R3/R4 が出る／R1 が出なくなる）がテストで固定されている★ |
| ★13★ | ★期待値表（段階1 の7 / 段階2 の14 / 段階3 の (a)9+(b)11+(c)2 / 段階4 の9+2 ケース）がすべてテストになっている★ |
| ★14★ | ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス（★テスト数は着手前より増えている★）★ |
| ★15★ | ★段階5 の後で `db.cleanLogs()` = **259件** / `db.prechecks()` = **27件**（`launch_failed` 13）/ `clean_error` の `1`〜`4` の分布が変わっていない★ |
| ★16★ | ★施設7 の追加3日が 1 / 2 / 3、既存の日が 4 になり、当週・当月が **4**★。★施設5 の 70.0 の行が **2（L2）**、当週・当月が **2**★ |
| ★17★ | ★未来行を投入しても当期の稼働率・出発成功率も**施設別の件数も**変わらないことを実測し、★投入した行を削除した★★ |
| ★18★ | ★`prisma/` / `lib/fixtures/` に差分が無い★ |
| ★19★ | ★`README.md` / `docs/code-map.md` / `docs/handoff/管理画面_設計検討_20260910.md` / `.claude/worktrees/**` に触っていない★ |
| ★20★ | ★`docs/decisions.md`（★D-110。D-109 を奪っていない★。★D-108 項7 の訂正は D-109 §6 と重複させていない★）と `docs/handoff.md`（追記33）に記録した★ |
| ★21★ | ★6コミット（段階1 / 2 / 3 / 4 / 5 / 7）で push した★ |
| ★22★ | ★`.env.local` が `USE_FIXTURES=true` に戻っている★ |

---

## 実行状態

| 項目 | 内容 |
|---|---|
| 状態 | ★**完了**★ |
| 実行日 | 2026-09-11 |
| 決定番号 | D-109（前提。別担当が作成）→ 本作業で **D-110** を追加 |
| コミット | `6a44295`（段階1）/ `63db289`（段階2）/ `50c4404`（段階3）/ `9f79ea7`（段階4）/ `2a19852`（段階5）/ 本コミット（段階7） |
| 実測値 | `tsc`0 / `lint`0 / `npm test` 45ファイル928テスト / `cleanLogs()` 259件 / 施設7の連続日数1→2→3→4 / 施設5の70.0行が2（L2）/ 他施設は不変。詳細は `docs/decisions.md` の **D-110** / `docs/handoff.md` **追記33** を参照 |
