# 指示書AB — 残りの実装を通しで行う（群1 エリア別完了率 / 群2 ページ送り / 群3 エラー率の分子 / 群4 積み残し3件）

| 項目 | 内容 |
|---|---|
| 実行環境 | ★**Cursor 内の Claude が実装します**★（`.claude/commands/d-watch.md` の役割分担表を参照） |
| 実行状態 | ★★**完了（2026-09-11）**★★ — ★4群すべて実行済み（群1=D-119 / 群2=D-120 / 群3=D-118 / 群4=D-117）。詳細は文末の「実行状態」表★ |
| 位置づけ | ★★**統合指示書**★★ — ★ユーザー指示（2026-09-11）「ZからACまでを一気にできる指示書を作ってください」★に対応する1本です。★★4つの群を頭から順に実行します★★ |
| 実行順 | ★★**群1 → 群2 → 群3 → 群4**（★崩さないこと★。理由は後述「実行順とその理由」）★★ |
| 対象 | ★群1★ = `docs/handoff/指示書Z_エリア別完了率を実装する.md`（★**その指示書を頭から実行する**★。本書に内容は写しません）／ ★群2★ = `docs/handoff/指示書AA_一覧にページ送りを付ける.md`（★同上★）／ ★群3★ = ★本書に全部書いてあります★（エラー率の分子を本清掃側にする）／ ★群4★ = ★本書に全部書いてあります★（小さな積み残し3件） |
| ★着手前確認★ | ★★**なし**★★ — ★群1・群2 はそれぞれの指示書が「着手前確認なし」で確定済み★。★群3・群4 は本書の「群3 で決めたこと」「群4 の3件」がすべて確定済み（ユーザー指示 2026-09-11 + 監督判断）★。★★ユーザーの回答を待たずに群1 から通しで実行してよい★★ |
| 取得する決定番号 | ★★**着手直前に `grep -nE "^### D-(0\|1)[0-9][0-9]" docs/decisions.md \| tail -3` で最終番号を実測すること**★★。★★`^### D-0` だけでは3桁台（D-100 以降）を拾えない★★。★2026-09-07 に `D-068` の二重採番が実際に起きています★。★★本書には見込み番号を書きません★★ — ★必ず自分で実測してから採番すること★。★★群ごとに別の番号を取るか、4群まとめて1つにするかは実行者が決めてよい★★（★群1・群2 の指示書はそれぞれ自前の記録段階を持っているので、その指示書を実行したなら**その指示書の指示に従って1つ取る**のが自然です★） |
| 段階数 | ★群1★ = 指示書Z の段階0〜8（9段階）／ ★群2★ = 指示書AA の段階0〜7（8段階）／ ★群3★ = ★本書の段階3-0〜3-5（6段階。コミットは4つ）★／ ★群4★ = ★本書の段階4-0〜4-2（3段階。コミットは1つ）★ |
| 着手前の基準 | ★★**着手直前に自分で `npm test` / `npx tsc --noEmit` / `npm run lint` を実行し、その実測値を基準にすること**★★。★作成時点の実測（2026-09-11。★★鵜呑みにしない★★）: `npm test` = **45ファイル / 1023テスト（全パス）** / `tsc` = **0** / `lint` = **0** / `db.cleanLogs()` = **259件** / `db.prechecks()` = **27件**（precheck 14 / launch_failed 13）★ |
| 作成 | 2026-09-11（作成時点の決定事項最終番号 **D-116**（`docs/decisions.md:7291`）/ `docs/handoff.md` の最新は **追記36**（`docs/handoff.md:3050`）/ HEAD = `d1b7fec`。★着手時に実測すること★） |

> 実行後はこの表と、文書末尾の「実行状態」表を更新してください。

> ★**この指示書を読んでいるあなたへ**★ — `.claude/commands/d-watch.md` の「実装は Cursor 側」は
> **2つの環境の役割分担**です。**Cursor の中で動いている Claude は実装担当**です。
> ★調査や改訂ではなく実装を行ってください。★
> ★★**途中で止めてよい指示書です**★★ —
> ★群1 だけ終わって群2 に入れなくても、それまでの成果は完結しています★。
> ★各群の終わりに「★ここで止めてもよい★」と明記してあります★。

---

## ★★冒頭に読むべきこと — 本書は何で、何ではないか★★

本書は ★★4つの独立した作業を1本の順序に並べた統合指示書★★です。

| 群 | 内容 | 詳細の所在 | 触る場所 |
|---|---|---|---|
| ★群1★ | ★エリア別完了率★（501 仮実装をやめる） | ★★`docs/handoff/指示書Z_エリア別完了率を実装する.md`（1063行）★★ | `d-watch-api.yaml` / `lib/types/` / `lib/db/` / `lib/fixtures/` / ★`components/features/detail-log/`★ / `app/api/clean-logs/[id]/areas/` |
| ★群2★ | ★一覧6画面のページ送り★（201件目以降が見えない） | ★★`docs/handoff/指示書AA_一覧にページ送りを付ける.md`（993行）★★ | ★`lib/i18n.ts`★ / `components/ui/` / ★`components/features/detail-log/`★ ほか5画面 |
| ★群3★ | ★エラー率の分子を本清掃側にする★ | ★★本書（後述）★★ | `lib/db/report-mapping.ts` / `lib/db/index.ts` / `scripts/seed-local-db.ts` |
| ★群4★ | ★小さな積み残し3件★ | ★★本書（後述）★★ | `lib/judge/kpi.ts`（コメント）/ `lib/fixtures/clean-logs.ts`（比較演算子1つ）/ `lib/db/__tests__/clean-log-mapping.test.ts`（1ケース追加） |

### ★★群1・群2 は本書に内容を写していません★★

★★既存の指示書が詳細（期待値表・段階割り・禁止事項・実測手順）を全部持っています★★。
★本書に二重に書くと、片方だけ直したときに食い違いが生まれます★（★D-103 が禁じた「規則を2箇所に書く」状態★）。

★★群1・群2 でやることは1つだけです — **その指示書を頭から実行してください**★★。
★本書の「実行順とその理由」「禁止事項」「群ごとのコミット規律」だけが上乗せされます★。

### ★★群1・群2 の指示書は、指示書W 完了より**前**に書かれています★★

★★これが本書で一番踏みやすい落とし穴です★★。

- ★指示書Z / AA は、指示書W（D-116。2026-09-11 完了）より前に起草されました★
- ★★その後 `lib/i18n.ts` / `d-watch-api.yaml` / `lib/types/index.ts` / `lib/db/report-mapping.ts` /
  `components/features/reports/ReportsScreen.tsx` が変わっています★★
  （★指示書W 段階1・段階7 が `ContinuousProblem` の型とキーを足し、i18n に3キーを追加しました★）
- ★★したがって指示書Z / AA に書かれている**行番号・件数・「現状こうなっている」という記述は
  ずれている可能性があります**★★

★★対処（必ずやること）★★:

> ★★各群の**段階0** で、その指示書が「実測して控えること」と書いている項目を**自分で測り直す**★★。
> ★★指示書に書かれた行番号をそのまま信じて編集しないこと★★ —
> ★必ず `grep` / `Read` で現物を見てから編集する★。
> ★★数字が指示書と食い違っていても、それだけでは「壊れている」ことにはなりません★★
> （★指示書W がその間に入っているため★）。★食い違いの内容を作業メモに控えてから進むこと★。

### ★本書で新しく決めたことはありません（群1・群2 について）★

★群1・群2 の仕様上の分かれ目は、それぞれの指示書の「採る方針」「監督が決めたこと」節で確定済みです★。
★★本書がそれを上書きすることはありません★★。★食い違ったら、その群の指示書が正★。

---

## ★★実行順とその理由（★崩さないこと★）★★

```
群1（エリア別完了率）  →  群2（ページ送り）  →  群3（エラー率の分子）  →  群4（積み残し3件）
```

### ★なぜ 群1 → 群2 なのか — ★同じディレクトリを触るから★★

| 群 | `components/features/detail-log/` | `lib/i18n.ts` |
|---|---|---|
| ★群1（Z）★ | ★★触る★★（`AreaModal` の配線。段階6） | 触る可能性あり |
| ★群2（AA）★ | ★★触る★★（F-02 清掃詳細ログにページ送りを入れる。段階3） | ★★触る★★（段階2 で共通部品の文言を足す） |

- ★★2つが同じファイル（同じ画面コンポーネント）を編集します★★
- ★★順番を守らないと衝突します★★ — ★特に `components/features/detail-log/` は、
  群1 がモーダルの中身を作り替え、群2 が一覧の下にページ送りを足す★。
  ★片方を後から当てると、マージではなく「読み直して当て直す」作業になります★
- ★★`lib/i18n.ts` は指示書W が直近で3キー足したばかり★★（`docs/decisions.md` の D-116）。
  ★群2 の段階2 はここに追記します★ — ★行番号を実測してから触ること★
- ★★指示書Z と 指示書AA 自身が、この順序を自分の「実行順」節に書いています★★
  （★Z:338行「指示書Y と 指示書W の後」／ AA:303行「指示書Y・W・Z の後」★）。
  ★本書の順序はそれと一致しています★ — ★★勝手に入れ替えないこと★★

### ★なぜ 群3・群4 が後なのか — ★重ならないから★★

- ★群3 は `lib/db/report-mapping.ts` / `lib/db/index.ts` / `scripts/seed-local-db.ts` が中心★
- ★群4 は `lib/judge/kpi.ts`（コメントのみ）/ `lib/fixtures/clean-logs.ts`（演算子1つ）/
  `lib/db/__tests__/clean-log-mapping.test.ts`（テスト追加）★
- ★★どちらも群1・群2 が触る画面コード（`components/`）とは重なりません★★
- ★したがって ★★群3・群4 は群1・群2 より先に実行しても技術的には成立します★★ —
  ★★ただし本書は「群1 → 群2 → 群3 → 群4」で書いてあるので、順序を変えるなら
  作業メモにその判断と理由を残すこと★★（★次に読む人が本書の順序と実際の履歴の食い違いで迷うため★）

### ★群3 と 群4 の間について★

★群3 の段階3-1（シード）と群4 の②（`lib/fixtures/clean-logs.ts` の比較演算子）は、
どちらも「検証データ」まわりですが ★★別のファイル・別の目的★★です★ — ★混ぜないこと★。
★★群3 が触るのは `scripts/seed-local-db.ts`（ローカルDB）、群4 が触るのは
`lib/fixtures/clean-logs.ts`（`USE_FIXTURES` 用）★★。

---

## ★★コミットと検証の規律（★全群に効きます★）★★

1. ★★群ごとに区切ってコミットすること★★ — ★群をまたいだ1コミットを作らない★
2. ★★群の切れ目で `npx tsc --noEmit` / `npm run lint` / `npm test` の3つが通ること★★
   （★群の中の各段階でも通すこと。段階割りはそうなるように組んであります★）
3. ★★`git add -A` 禁止★★（`CLAUDE.md` §7。★過去に別作業の途中ファイルを巻き込む事故あり★）—
   ★★必ずファイルパスを明示して `git add` すること★★
4. ★★`docs/decisions.md` をコミットするときは `git diff docs/decisions.md` を先に見ること★★ —
   ★未コミットの追記が同居していると巻き込みます★（★指示書W で実際に起きた★）
5. ★★途中で止めてよい★★ — ★各群の終わりに「ここで止めてもよい」と書いてあります★。
   ★止めるときは、どの群のどの段階まで終わったかを作業メモに残すこと★

---

## ★禁止事項（★本書の全群・全段階に効きます★）★

1. ❌ ★★`git add -A` の使用★★ — ★ファイルパスを明示する★（`CLAUDE.md` §7）
2. ❌ ★★メインDBのスキーマ変更・マイグレーション作成★★（TeamWorks の領域。D-056。
   `prisma/schema.prisma` は `db pull` の生成物）
3. ❌ ★★`robot_error_reports` / `control_error_histories` の
   `status` / `comment` / `responded_at` / `resolved_at` ★**以外**★への DB 書き込み★★（D-042）。
   ★★本書のどの群も、アプリコードから DB に書き込みません★★
   （★`scripts/seed-local-db.ts` はローカル検証用の**シード**であり、アプリの書き込みではない★）
4. ❌ ★★閾値・コード値のハードコード★★ — ★`lib/config/` から読む★（`CLAUDE.md` §7）。
   ★群3 で `CLEAN_TIMETABLE_PRECHECK.main` を `0` と直書きしないこと★
5. ❌ ★★丸めた値で判定すること★★（`CLAUDE.md` §4.3）
6. ❌ ★★`lib/fixtures/` の既存の**値**を変更すること★★ —
   ★群4 ② で変えるのは**生成式の比較演算子1つだけ**で、出力される値は1件も変わりません★
   （★変わったら手が滑っています。段階4-1 の実測で確認します★）
7. ❌ ★★確定期間（サマリ由来）のコードを触ること★★ —
   ★`kpiFromSummaryRatios()` / `errorBreakdownRowsFromSummary()` / `facility_*_summaries` を読む経路★。
   ★群3 は**当期（詳細テーブル由来）だけ**を直します★
8. ❌ ★★アラート画面（`prechecks()`）の分子を変えること★★ — ★群3 の「変えないもの」を参照★
9. ❌ ★★エラー種別を7分類に変換すること／日本語に変換すること★★（D-077 / D-088。`CLAUDE.md` §4.8）
10. ❌ ★★次のファイルに触ること★★:
    `README.md` / `docs/code-map.md` / `docs/handoff/管理画面_設計検討_20260910.md` / ★`.claude/worktrees/**`★
11. ❌ ★★一時スクリプトをリポジトリに残すこと★★ —
    ★過去に `scripts/tmp-measure.ts` が残った★。★一時ファイルは作業用ディレクトリに置き、
    終わったら消すこと★。★★成功時にも必ず `process.exit(0)` を書くこと★★
    （★Prisma の接続が残ってハングした事故あり★）
12. ❌ ★★仕様が不明な箇所を推測で実装すること★★（`CLAUDE.md` §7）— ★止めて確認を求める★

---

# ★★群1 — エリア別完了率★★

## やること

> ★★`docs/handoff/指示書Z_エリア別完了率を実装する.md` を頭から実行してください。★★

★本書はその内容を写しません★（重複させると片方だけ直したときに食い違うため）。

### 群1 の段階（★その指示書が正★。ここは目次です）

| 段階 | 内容 | コミット |
|---|---|---|
| 段階0 | 前提の確認と実測 | なし |
| 段階1 | `d-watch-api.yaml` を直す | あり |
| 段階2 | 型と純粋関数と期待値表テスト | あり |
| 段階3 | fixtures を追加する | あり |
| 段階4 | データ層（`DataSource.areaCompletions()`） | あり |
| 段階5 | APIルート（501 を外す） | あり |
| 段階6 | 画面（`AreaModal` の配線） | あり |
| 段階7 | 実測での検証 | なし |
| 段階8 | 記録と push | あり |

### ★群1 の段階0 で必ず測り直すこと★

★★指示書Z は指示書W より前に書かれています★★（前述）。★次を自分で実測してから段階1 に進むこと★:

```bash
# ★決定番号の最終（見込みを書かない）★
grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md | tail -3

# ★指示書Z が挙げている前提決定の行番号（ずれている）★
grep -nE "^### D-(102|103|106|107|109|110|111|112|113|114|115|116) " docs/decisions.md

# ★触る予定のファイルの現状（行番号を信じない）★
grep -n "areaCompletions" lib/db/index.ts lib/types/index.ts d-watch-api.yaml
ls components/features/detail-log/
```

- ★★`d-watch-api.yaml` は指示書W 段階1 が `ContinuousProblem` を触っています★★ — ★行番号がずれます★
- ★★`lib/types/index.ts` も同様★★（`ContinuousProblem` に `launch_failed_days` が増えています）
- ★★`npm test` の件数は指示書Z の記述より増えています★★（★W がテストを足したため★）—
  ★自分で測った値を基準にすること★

### ★群1 の終わり★

- ★★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★★
- ★★`/detail-log` のエリアモーダルが 501 を出さなくなっていること★★
- ★★ここで止めてもよい★★ — ★群2 に入らなくても、エリア別完了率は単体で完結しています★

---

# ★★群2 — 一覧6画面のページ送り★★

## やること

> ★★`docs/handoff/指示書AA_一覧にページ送りを付ける.md` を頭から実行してください。★★

★本書はその内容を写しません★。

### 群2 の段階（★その指示書が正★。ここは目次です）

| 段階 | 内容 | コミット |
|---|---|---|
| 段階0 | 前提の確認と実測 | なし |
| 段階1 | ページ番号の並びを組み立てる純粋関数 | あり |
| 段階2 | 共通部品 `Pagination` と i18n | あり |
| 段階3 | 清掃詳細ログ F-02 に入れる（作法を確定させる） | あり |
| 段階4 | summary 系3画面（F-03 / F-06 / F-07） | あり |
| 段階5 | `ListResponse` 系2画面（F-08 / F-09） | あり |
| 段階6 | 6画面の実測検証 | なし |
| 段階7 | 記録と push | あり |

### ★群2 の段階0 で必ず測り直すこと★

```bash
# ★i18n は指示書W が直近で3キー足した（D-116）★ — 行番号も件数もずれている
grep -c "" lib/i18n.ts
grep -n "continuous" lib/i18n.ts

# ★群1（指示書Z）が detail-log を触った直後★ — 現物を見てから足すこと
ls components/features/detail-log/
grep -rn "per_page\|meta" app/api --include=route.ts | head -30
```

- ★★群1 を先に実行したので、`components/features/detail-log/` は指示書AA が書かれた時点と
  違う状態になっています★★ — ★★AA の段階3 に書かれている行番号・コード断片は必ず現物で確かめること★★
- ★★`lib/i18n.ts` は指示書W と、場合によっては群1 が触っています★★ — ★末尾に追記する前に構造を見る★

### ★群2 の終わり★

- ★★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★★
- ★★6画面すべてで 201件目以降に到達できること★★（指示書AA 段階6 の実測手順に従う）
- ★★ここで止めてもよい★★

---

# ★★群3 — エラー率の分子を本清掃側にする★★

★★ここからは本書に全部書いてあります。参照先の指示書はありません。★★

## ★★直す不具合 — 何が起きているか★★

### 1. ★TeamWorks 第8回 A7 の回答（★2026-09-11 受領。原文★）★

```
Q7. robot_error_reports.clean_timetable_id は事前チェックの行と本清掃の行のどちらを指すか
A7. どちらも指しています. ただし事前チェックは本清掃を円滑に実施できるためのものなので,
    サマリには含めない, とするのが正しいでしょう.
```

★★つまり `clean_timetable_id` は**事前チェック行も本清掃行も指しうる**。そして
エラー率の分子に入れるべきなのは**本清掃行を指すもの**です。★★

### 2. ★★現状は逆になっています★★（★★着手時に自分で実測して確かめること★★）

| 箇所 | 当期の分子 | 何が起きるか |
|---|---|---|
| ★`reportKpis()` / `facilityPerformance()` の `error_rate`★ | ★★事前チェック行に紐づくエラー**だけ**★★ | ★★本清掃行を指すエラーを**黙って捨てている**★★ |
| ★`errorBreakdown()`★ | ★★全件★★（`clean_timetable_id` を一切見ていない） | ★★事前チェック行のエラーも数えている★★ |

★機序（★実測して裏を取ること★）★:

```ts
// lib/db/report-mapping.ts — mainIdByPrecheckId() は 事前チェック行id → 本清掃id の対応表
// errorCountByMainId() は その対応表を引く
const mainId = mainIdByPrecheck.get(report.clean_timetable_id.toString());
if (mainId === undefined) continue;   // ★★本清掃行を指すエラーはここで落ちる★★
```

★★`clean_timetable_id` が本清掃行の id だと、事前チェック行の表には無いので
`undefined` になり `continue` します★★ — ★例外もログも出ません★。

★`errorBreakdown()` の側（`lib/db/index.ts` の当期ブランチ）は
`countErrorTypesInRange()` を呼んでいますが、この関数は `clean_timetable_id` を
一切参照しません★ — ★★全件数えます★★。

### 3. ★★なぜ目視で気づけないか★★

- ★現在のローカルDBのエラー行（14件）は ★★全部が事前チェック行を指しています★★★
  （`scripts/seed-local-db.ts` が事前チェック行にだけエラーを作るため）
- ★したがって「事前チェックだけ数える」も「全件数える」も ★★同じ数字になります★★★
- ★★同じレポート画面に**定義の違う2つの数字**が並んでいるのに、目視では一致して見える★★

### 4. ★★これは実装ミスではありません（★記録に残すこと★）★★

★質問した時点（第8回 Q7 を出す前）では「`clean_timetable_id` は事前チェック行を指す」と
**仮定して**書いたものです★。★その仮定は `lib/db/report-mapping.ts` の
`CleanStatsBucket.errorCount` の docstring に**明記されています**★:

```
/** エラー発生回数（事前チェック行に紐づく robot_error_reports の件数） */
errorCount: number;
```

★★仮定を明記してあったから、回答が来たときに直す場所が特定できました★★。
★★同じやり方を続けること★★ — ★ただし直した後は古い仮定を残さないこと（後述 決定5）★。

---

## ★★群3 で決めたこと（★理由ごと理解してから実装すること★）★★

### ★決定1 — 分子の定義★

> ★★「本清掃行（`precheck` が `CLEAN_TIMETABLE_PRECHECK.main`）を指す `robot_error_reports` の件数」★★

- ★★事前チェック行を指すものは**除外**★★（第8回 A7「事前チェックは…サマリには含めない」）
- ★★実装は「本清掃行の id の Set を作り、`clean_timetable_id` がその Set にあるものだけ数える」★★
- ★★理由★★: ★「事前チェック行の id ではない＝本清掃行」と推測してはいけません★ —
  ★`clean_timetable_id` には dangling（参照先が消えている）値もありうる★
  （★`prisma/schema.prisma` の `clean_timetable_id` には ★FK も KEY も無い★。
  `lib/db/index.ts` の落とし穴1★）。★★「本清掃行の集合に**入っている**」という肯定条件で数えること★★

### ★決定2 — `errorBreakdown()` の当期も同じ定義に揃える★

- ★同じレポート画面（F-06）に、定義の違う2つの数字を並べない★
- ★★`errorBreakdown()` の**確定期間**（サマリ由来）は触りません★★（決定6）

### ★決定3 — `clean_timetable_id` が `null` のエラー行は分子に入れない★

★現在、3箇所で扱いが違います★:

| 箇所 | 現状の `null` の扱い |
|---|---|
| `errorCountByMainId()`（率） | ★捨てる★（`if (report.clean_timetable_id === null) continue;`） |
| `prechecks()`（アラート） | ★捨てる★（`scheduled_at` を決められないため行が作れない） |
| `countErrorTypesInRange()`（内訳） | ★★数える★★（`clean_timetable_id` を見ていないので素通り） |

★★3箇所とも「捨てる」に揃えます★★。

★理由★: ★「本清掃行の id の Set に入っているか」で数えると、`null` は自動的に落ちます★ —
★★特別扱いのコードを足すのではなく、肯定条件の自然な帰結として落ちる形にすること★★。

★★申し送りに残すこと★★: ★`facility_id` と `errored_at` を持っているので、
「施設・期間には割り当てられる」— ★★入れる選択肢は技術的にあります★★★。
★今回は「清掃1回に紐づかないエラーは清掃のエラー率の分子ではない」という理由で捨てます★。

### ★決定4 — ★★シードに本清掃行を指すエラー行を足す★★★

★★これが無いと、直した後のローカル画面のエラー率が**一律 0%** になり、
「直ったのか壊したのか」が区別できません★★。

- ★★既存のエラー14件は1つも変えない★★（★値も id も★）— ★**追加**するだけ★
- ★★冪等を保つ★★ — ★乱数を使わない / id を明示採番する / 2回流しても同じ結果になる★
- ★詳しい作り方は段階3-1★

### ★決定5 — `CleanStatsBucket.errorCount` の docstring を書き換える★

★古い仮定（「事前チェック行に紐づく」）を残さないこと★ — ★★次に読む人が両方を信じてしまいます★★
（★指示書Y / 指示書R が同じ理由で「古い記述を残さない」と書いています★）。

### ★決定6 — 確定期間（サマリ由来）は触らない★

- ★`facility_error_summaries` / `kpiFromSummaryRatios()` / `errorBreakdownRowsFromSummary()` は**そのまま**★
- ★理由★: ★★TeamWorks のサマリ集計が事前チェックを除外しているかは**未確認**★★。
  ★A7 は「除外するのが正しいでしょう」という**見解**であって、
  「現在の集計バッチがそうなっている」という**事実の確認ではありません**★
- ★★第9回の確認事項に載せること（申し送り①）★★

### ★決定7 — エラーの期間帰属は「本清掃の `start_at` の JST 日付」に揃える★

- ★現状 `countErrorTypesInRange()` は `detectedAtFromError()`（`errored_at ?? created_at`）で期間を決めています★
- ★★これを「そのエラーが指す本清掃行の `start_at` の JST 日付」に変えます★★
- ★★理由★★: ★エラー率の**分母**は「清掃実施回数」です（`CLAUDE.md` §4.2）★。
  ★分母が清掃を基準にしているのに分子が検知時刻基準だと、深夜清掃が日をまたいだときに
  分子と分母が別の期間に入ります★ — ★★分子も清掃を基準にするのが整合的★★
- ★★`reportKpis()` / `facilityPerformance()` は**すでに**この帰属になっています★★
  （`aggregateCleanStatsByFacility()` が本清掃行を期間で絞ってからエラー件数を足すため）—
  ★★`errorBreakdown()` をそちらに揃える、という向きです★★
- ★`detectedAtFromError()` 自体は削除しないこと★ — ★`prechecks()`（アラート）が使っています★

### ★★決定8 — 段階の順序★★

> ★★シードを先に直すこと★★（本書は段階3-1 をシードにしてあります）。

★読み出しだけ先に変えると、ローカルのエラー率が一律 0% になり「壊れた」ように見えます★。
★★シードを先に入れておけば、読み出しを直した瞬間に「0% だった当期のエラー率に数字が入る」という
**直ったことの証拠**が得られます★★。

---

## ★変えないもの（★触ったら手が滑っています★）★

| 対象 | 理由 |
|---|---|
| ★`prechecks()`（アラート画面）★ | ★アラートは「事前チェックで検知した異常」を出す画面★。★★分子の話とは別★★（D-104。本清掃行を指すエラーを `launch_failed` に読み替えない、も維持） |
| ★`lib/judge/kpi.ts` の `errorRate()`★ | ★純粋関数。`rate(errors, executed)` のまま★。★★分子の**作り方**が変わるだけで、率の**計算**は変わりません★★ |
| ★`d-watch-api.yaml`★ | ★★レスポンスの形は変わりません★★。★★変えたくなったら、それは解釈が違うサインです — 止めて確認を求めること★★ |
| ★`lib/fixtures/` の既存値★ | ★`USE_FIXTURES=true` の画面は変わりません★ |
| ★確定期間（サマリ由来）のコード★ | ★決定6★ |
| ★`detectedAtFromError()`★ | ★`prechecks()` が使っている★ |

---

## 群3 の段階割り

| 段階 | 内容 | コミット |
|---|---|---|
| 段階3-0 | 前提の確認と実測 | ★なし★ |
| 段階3-1 | ★シードに本清掃エラーを足して再シードする★ | ★あり★ |
| 段階3-2 | ★期待値表 → 純粋関数（新規追加のみ）とテスト★ | ★あり★ |
| 段階3-3 | ★配線（3箇所）＋ docstring ＋ 旧関数の削除★ | ★あり★ |
| 段階3-4 | ★実測での検証★ | ★なし★ |
| 段階3-5 | ★記録★ | ★あり★ |

---

## 段階3-0 — 前提の確認と実測（★読むだけ・コミットなし★）

### 0-1. 読むもの

```bash
# ★A7 の原文（本書の出所）★
grep -n "Q7\|A7" docs/handoff/TeamWorks確認事項_第8回_20260911.md

# ★現状の実装（★行番号は必ず実測すること★）★
grep -n "mainIdByPrecheckId\|errorCountByMainId\|countErrorTypesInRange\|CleanStatsBucket" \
  lib/db/report-mapping.ts lib/db/index.ts

# ★決定事項★
grep -nE "^### D-(042|077|088|102|107|114|116) " docs/decisions.md
```

### 0-2. ★★実測して控えること（★段階3-4 の突き合わせに使います★）★★

★★一時スクリプトは作業用ディレクトリに置き、★成功時にも `process.exit(0)` を書くこと★★★
（★Prisma の接続が残ってハングした事故あり★）。★終わったら消すこと★。

```ts
// 例（★DB は SELECT のみ★。書き込まない）
import { db } from "@/lib/db";

async function main() {
  const kpis = await db.reportKpis();
  const perf = await db.facilityPerformance();
  const breakdown = await db.errorBreakdown();
  console.log("reportKpis rows:", kpis.length);
  console.log("当期のerror_rate:", kpis.filter(/* 当期の期間キー */).map((r) => r.kpi.error_rate));
  console.log("errorBreakdown 当期 items:", breakdown.filter(/* 当期 */).length);
  process.exit(0);           // ★★成功時にも必ず書く★★
}
main().catch((e) => { console.error(e); process.exit(1); });
```

★控える数字★:

| # | 何を | 控える理由 |
|---|---|---|
| ★1★ | ★`robot_error_reports` の件数（★14 のはず★）と、そのうち `clean_timetable_id` が 本清掃id / 事前チェックid / null / dangling の内訳★ | ★段階3-1 で「既存14件が変わっていないこと」を示す★ |
| ★2★ | ★当期（今週・今月）の `error_rate` の値★ | ★★直った後に**数字が増える**ことが合否です★★ |
| ★3★ | ★`errorBreakdown()` の当期の `items` と件数★ | ★同上★ |
| ★4★ | ★`db.cleanLogs()` の件数（★259 のはず★）と `db.prechecks()` の件数（★27 のはず★）★ | ★★回帰していないことの確認★★（群3 はどちらも変えません） |
| ★5★ | ★`npm test` のファイル数・件数 / `tsc` / `lint`★ | ★基準★ |

### この段階の完了条件

- ★上の1〜5 を控えた★
- ★★DB に1行も書いていない★★
- ★一時スクリプトを消した★

---

## 段階3-1 — ★シードに本清掃行を指すエラー行を足す★（1コミット）

### 1-1. ★★やること★★

`scripts/seed-local-db.ts` に、★★本清掃行（`clean_timetables.precheck === CLEAN_TIMETABLE_PRECHECK.main`）
を指す `robot_error_reports` の行★★を追加します。

★★加えて、決定3（`null` を捨てる）を実データで確認するための行を2件足します★★:

| 種類 | 件数 | 目的 |
|---|---|---|
| ★A: 本清掃行を指すエラー★ | ★複数（下記の規則）★ | ★★直した後にエラー率に数字が入ること★★ |
| ★B: `clean_timetable_id` が `null` のエラー★ | ★1件★ | ★★3箇所すべてで捨てられること★★（決定3） |
| ★C: dangling（存在しない `clean_timetable_id`）★ | ★1件★ | ★★例外を出さずに捨てられること★★（FK が無い列。落とし穴1） |

### 1-2. ★★既存14件を1つも変えないための注意（★ここが一番踏みやすい★）★★

★★`errorReportCount` を本清掃エラーで増やしてはいけません★★。

```ts
// 既存コード（事前チェック行のエラー）
const slot = errorReportCount % ERROR_RESOLUTION_ROTATION.length;
// ...
errorReportCount += 1;
```

- ★`errorReportCount` は ★★`status` / `comment` / `responded_at` / `resolved_at` の
  ローテーションのスロット★★を決めています★
- ★★ここに本清掃エラーを混ぜて通番を進めると、**後続の事前チェックエラー14件の値がずれます**★★
  （★「既存14件を変えない」という決定4 に反する★）
- ★★対処: 本清掃エラー用に**別のカウンタ**（例 `mainErrorReportCount`）を用意すること★★。
  ★最後の集計ログは両方を足して出す（ログの数字が増えるのは構いません）★

### 1-3. ★★挿入位置の注意★★

```ts
await prisma.cleanStatusHistory.create({ ... });
historyCount += 1;

// ★★ここに本清掃エラーの作成を置く★★

// ★`clean_statuses` は当日分だけ（D-093 項1）★
if (log.scheduled_at.slice(0, 10) !== todayJstDate) continue;
```

- ★★`if (... !== todayJstDate) continue;` より**前**に置くこと★★ —
  ★後ろに置くと、当日以外の行がすべて飛ばされて1〜2件しか作られません★
- ★`cleanError` が決まった後に置くこと★（★選定規則で使う場合★）

### 1-4. ★★A: 本清掃行を指すエラーの作り方★★

| 項目 | 値 |
|---|---|
| ★どの行に作るか★ | ★★実施済み（`log.completion_rate !== null`）の本清掃行のうち、`Number(log.id) % MAIN_CLEAN_ERROR_EVERY === 0` の行★★（★モジュール定数にすること。直書き禁止★） |
| ★`id` / `error_id`★ | ★`RER-${log.id}-main`★（★既存の `RER-<id>-<分数>` と衝突しない★。★明示採番＝冪等★） |
| ★`clean_timetable_id`★ | ★★`BigInt(log.id)`（＝本清掃行の id）★★ |
| ★`facility_id` / `section_id` / `robot_id` / `serial_number`★ | ★既存の事前チェックエラーと同じ作り方★ |
| ★`error_type`★ | ★★`MAIN_CLEAN_ERROR_TYPE_ROTATION` から決定的に選ぶ★★（下記） |
| ★`error_level` / `error_detail`★ | ★既存のプレースホルダ定数を使い回す★ |
| ★`status` / `comment` / `responded_at` / `resolved_at`★ | ★`mainErrorReportCount` を使って既存と同じローテーションで散らす★ |
| ★`errored_at`★ | ★`addMinutes(startAt, MAIN_CLEAN_ERROR_AFTER_MINUTES)`★（★清掃中に起きた体にする。定数化★） |

★★`error_type` に入れる値★★:

- ★★新しい値を発明しないこと★★（D-077 / `CLAUDE.md` §4.8）
- ★★`lib/fixtures/reports.ts` の `errorBreakdown` items が持っている生値を使うこと★★ —
  ★`EmergencyStop` / `LostLocalization` / `RobotHardwareErrorToAdmin` / `LowBatteryLevel` 等★。
  ★★これらは英語識別子であり、D-077 の「生値をそのまま表示する」に沿っています★★
- ★シードの既存コード（`facility_error_summaries` の投入部）が同じ値を使っているので、
  ★★当期の内訳と確定期間の内訳が同じ語彙になり、画面の比較がしやすくなります★★

★★件数の受け入れ条件（★これを満たすまで `MAIN_CLEAN_ERROR_EVERY` を調整すること★）★★:

| # | 条件 |
|---|---|
| ★1★ | ★★当週（今週）の本清掃行に**最低1件**★★ |
| ★2★ | ★★当月（今月）の本清掃行に**最低3件**★★ |
| ★3★ | ★2種類以上の `error_type` が当期に現れる★（★内訳が1行だけだと `rate` が常に100%になり検証にならない★） |
| ★4★ | ★★2回流しても同じ行・同じ値になる★★（冪等） |

★★`MAIN_CLEAN_ERROR_EVERY` は「調整してよいモジュール定数」であることを docstring に書くこと★★。

### 1-5. ★★B / C: `null` と dangling の1件ずつ★★

- ★ループの外（★既存の「施設7の連続日数検証データ」と同じ場所★）で作ること★ —
  ★★ループの中に混ぜると通番がずれます★★（既存コメントが同じ警告を出しています）
- ★`id` / `error_id`★: ★`RER-NULL-1` / `RER-DANGLING-1` のような明示採番★
- ★B の `clean_timetable_id`★: ★`null`★
- ★C の `clean_timetable_id`★: ★★どの `clean_timetables.id` とも一致しない値★★
  （★例: 本清掃id の最大値 + 大きめの定数。★定数化し、衝突しないことをコードで確かめること★）
- ★★docstring に「決定3 の検証用。3箇所すべてで捨てられることを確認するために置いてある」と書くこと★★
  — ★★次に読む人が「ゴミデータだ」と消さないように★★

### 1-6. 再シードと実測

```bash
npm run seed:local
```

★★実測して確認すること★★:

| # | 確認 | 期待 |
|---|---|---|
| ★1★ | ★`robot_error_reports` の総件数★ | ★14 + A の件数 + 2★ |
| ★2★ | ★★既存14件（`RER-<id>-<分数>`）の `status` / `comment` / `responded_at` / `resolved_at` が段階3-0 で控えた値と1つも変わっていないこと★★ | ★★完全一致★★ |
| ★3★ | ★`db.prechecks()` の件数★ | ★★27 のまま★★（★A/B/C は本清掃・null・dangling なので `prechecks()` が全部落とす★） |
| ★4★ | ★`db.cleanLogs()` の件数★ | ★★259 のまま★★ |
| ★5★ | ★当期の `error_rate`★ | ★★まだ変わらない★★（読み出しは未修正。★★これが正常★★） |
| ★6★ | ★`errorBreakdown()` の当期の件数★ | ★★増える★★（まだ全件数えているため。★★これも正常★★） |
| ★7★ | ★もう1回 `npm run seed:local` を流して、DB の中身が同じになること★ | ★冪等★ |

### この段階の完了条件

- ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★
- ★上の1〜7 を実測した★
- ★★コミット（`git add scripts/seed-local-db.ts` — ★パスを明示★）★★

---

## 段階3-2 — ★期待値表 → 純粋関数とテスト★（1コミット）

> ★★この段階では既存の関数を消しません／既存の呼び出し側も変えません★★ —
> ★新しい関数を**追加するだけ**にして、`tsc` を緑のまま保ちます★。
> ★★配線は段階3-3 です★★。

### 2-1. ★★期待値表（★これをそのままテストにすること。実装より先に書く★）★★

#### (a) ★`mainTimetableStartAtById()`★ — ★本清掃行 → id(文字列) → `start_at`★

| # | 入力 | ★期待★ |
|---|---|---|
| ★a1★ | 本清掃行2件（id=1001, 1002） | ★`Map { "1001" => Date, "1002" => Date }`（`size === 2`）★ |
| ★a2★ | 空配列 | ★`size === 0`★ |
| ★a3★ | ★`BigInt` の id が**文字列キー**になっていること★ | ★`map.has("1001") === true` / `map.has(1001 as never) === false`★ |

> ★★`BigInt` は `===` でキー比較できないので文字列化する★★（既存コードの作法と同じ）。

#### (b) ★`errorCountByMainTimetable()`★ — ★エラー行 + 本清掃idマップ → 本清掃id → 件数★

★★これが分子の本体です。★★

| # | ケース | 入力（本清掃id = 1001, 1002。事前チェックid = 101001） | ★期待★ |
|---|---|---|---|
| ★b1★ | ★★本清掃行を指すエラー★★ | `clean_timetable_id = 1001` | ★★`Map { "1001" => 1 }`★★ |
| ★b2★ | ★★事前チェック行を指すエラー★★ | `clean_timetable_id = 101001` | ★★`size === 0`（**数えない**）★★ |
| ★b3★ | ★★`clean_timetable_id` が `null`★★ | `null` | ★★`size === 0`（**数えない**。決定3）★★ |
| ★b4★ | ★★dangling（存在しない id）★★ | `clean_timetable_id = 999999` | ★★`size === 0`（**例外を投げない**）★★ |
| ★b5★ | ★同じ本清掃に2件★ | `1001` が2行 | ★`Map { "1001" => 2 }`★ |
| ★b6★ | ★本清掃と事前チェックが混在★ | `1001` / `101001` / `1002` | ★★`Map { "1001" => 1, "1002" => 1 }`（`size === 2`）★★ |
| ★b7★ | ★0件★ | `[]` | ★`size === 0`★ |
| ★b8★ | ★本清掃マップが空★ | エラー3件・マップ空 | ★`size === 0`★ |

#### (c) ★`countMainErrorTypesInRange()`★ — ★当期の内訳（`error_type` → 件数）★

★★期間帰属は「そのエラーが指す本清掃行の `start_at` の JST 日付」★★（決定7）。
★範囲は `[from, to]` の**両端を含む**★。

| # | ケース | ★期待★ |
|---|---|---|
| ★c1★ | ★本清掃行を指し、その `start_at` が範囲内★ | ★数える★ |
| ★c2★ | ★★事前チェック行を指す★★ | ★★数えない★★ |
| ★c3★ | ★★`clean_timetable_id` が `null`★★ | ★★数えない★★ |
| ★c4★ | ★★dangling★★ | ★★数えない・例外を投げない★★ |
| ★c5★ | ★本清掃の `start_at` が ★`range.from` ちょうど★★ | ★★数える★★ |
| ★c6★ | ★本清掃の `start_at` が ★`range.to` ちょうど★★ | ★★数える★★ |
| ★c7★ | ★本清掃の `start_at` が `range.from` の前日★ | ★数えない★ |
| ★c8★ | ★本清掃の `start_at` が `range.to` の翌日★ | ★数えない★ |
| ★★c9★★ | ★★`errored_at` は範囲外だが、本清掃の `start_at` は範囲内★★ | ★★数える★★（★★決定7 が効いていることの証拠。`errored_at` 基準なら落ちる★★） |
| ★c10★ | ★同じ `error_type` が2件★ | ★`Map { type => 2 }`★ |
| ★c11★ | ★0件★ | ★`size === 0`★ |

> ★★c9 は**この段階の肝**です★★ — ★これが無いと決定7 を実装したか確かめられません★。
> ★深夜清掃（`start_at` が 02:00）のエラーが翌日の `errored_at` を持つ、という現実的な形で書くこと★。

#### (d) ★`kpiFromCleanStats()` 側（★分母0 と 0件★。既存の挙動の確認）★

| # | ケース | ★期待★ |
|---|---|---|
| ★d1★ | ★`executed = 0` / `errorCount = 0`★ | ★★`error_rate.value === null`★★（★分母0 は率を出さない。0% にしない★） |
| ★d2★ | ★`executed = 10` / `errorCount = 0`★ | ★`error_rate.value === 0`★ |
| ★d3★ | ★`executed = 10` / `errorCount = 3`★ | ★`error_rate.value === 30`（★丸めない生値★）★ |

> ★d1〜d3 は既存の `errorRate()` の挙動です★ — ★★変えません★★。
> ★★分子の作り方を変えても分母0の扱いが変わっていないこと★★を固定するために書きます。

### 2-2. テストを書く

- ★置き場所★: ★`lib/db/__tests__/report-mapping.test.ts`（既存ファイルに追記）★
- ★★実装より先に書き、**落ちることを確認してから**実装すること★★
- ★`describe` は新しい関数名で切ること★

### 2-3. 実装（★`lib/db/report-mapping.ts` に**追加**する★）

```ts
/**
 * 本清掃行（`precheck === CLEAN_TIMETABLE_PRECHECK.main`）の id（文字列） → `start_at`。
 *
 * ★2つの用途を1本で賄う★ —
 *   1. ★キーの集合★が「本清掃行の id の Set」になる（エラーの分子の判定に使う）
 *   2. ★値★が期間帰属の基準になる（決定7。エラーの期間は本清掃の `start_at` の JST 日付）
 *
 * ★BigInt は `===` でキー比較できないので文字列化する★（`mainIdByPrecheckId()` と同じ作法）。
 */
export function mainTimetableStartAtById(
  mainRows: readonly MainTimetableLike[]
): Map<string, Date> { /* ... */ }

/**
 * `robot_error_reports` を ★本清掃id単位★ の件数に集計する。
 *
 * ★★分子の定義（TeamWorks 第8回 A7。2026-09-11）★★:
 *   ★本清掃行を指すエラーだけを数える★。★事前チェック行を指すものは含めない★
 *   （「事前チェックは本清掃を円滑に実施できるためのもの」なのでサマリに含めない）。
 *
 * ★★「本清掃行の集合に**入っている**」という肯定条件で数えること★★ —
 *   `clean_timetable_id` には FK も KEY も無く（`prisma/schema.prisma`。落とし穴1）、
 *   ★`null` も dangling もありうる★ので、「事前チェックでない＝本清掃」と推測してはいけない。
 *   ★`null` と dangling はこの条件の自然な帰結として落ちる★（決定3。特別扱いのコードを足さない）。
 */
export function errorCountByMainTimetable(
  errorReports: readonly { clean_timetable_id: bigint | null }[],
  mainStartAtById: ReadonlyMap<string, Date>
): Map<string, number> { /* ... */ }

/**
 * 当期（詳細テーブル）のエラー内訳を `error_type` ごとに数える。
 *
 * ★★期間帰属は「そのエラーが指す本清掃行の `start_at` の JST 日付」★★（決定7）—
 *   エラー率の分母が「清掃実施回数」（`CLAUDE.md` §4.2）なので、分子も清掃を基準にする。
 *   ★`errored_at` 基準ではない★（深夜清掃が日をまたぐと分子と分母が別の期間に入るため）。
 * ★`jstIsoFromDb()` で日本時間の壁時計値に補正してから日付の桁を取る★
 *   （TeamWorks 第8回 A2。指示書Y）。
 * ★`type` / `label` は生値のまま。7分類・日本語への変換はしない（D-077 / D-088）★
 */
export function countMainErrorTypesInRange(
  errorReports: readonly (ErrorReportLike & { clean_timetable_id: bigint | null })[],
  mainStartAtById: ReadonlyMap<string, Date>,
  range: { from: string; to: string }
): Map<string, number> { /* ... */ }
```

★★注意★★:

- ★`YYYY-MM-DD` は辞書順＝日付順★（既存コードと同じ比較の仕方）
- ★★`CLEAN_TIMETABLE_PRECHECK.main` を `0` と直書きしないこと★★ — ★`lib/config` から読む★
- ★★この段階では旧関数（`mainIdByPrecheckId` / `errorCountByMainId` / `countErrorTypesInRange`）を
  消さないこと★★ — ★消すのは段階3-3★

### この段階の完了条件

- ★期待値表を先に書き、テストが落ちることを確認してから実装した★
- ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★
- ★★コミット（`git add lib/db/report-mapping.ts lib/db/__tests__/report-mapping.test.ts`）★★

---

## 段階3-3 — ★配線 ＋ docstring ＋ 旧関数の削除★（1コミット）

### 3-1. ★`reportKpis()` / `facilityPerformance()`（`lib/db/index.ts`）★

★2箇所とも同じ形です★:

```ts
// 変更前
const mainIdByPrecheck = mainIdByPrecheckId(precheckRows);
const errCountByMainId = errorCountByMainId(errorReports, mainIdByPrecheck);

// 変更後
const mainStartAtById = mainTimetableStartAtById(mainRows);
const errCountByMainId = errorCountByMainTimetable(errorReports, mainStartAtById);
```

- ★★`precheckRows` がこの2メソッドで他に使われていないなら、変数ごと消すこと★★
  （★★使われているか必ず実測すること★★。★消してよいかは `grep` で確かめる★）
- ★`aggregateCleanStatsByFacility()` の引数名・型は変わりません★

### 3-2. ★`errorBreakdown()`（`lib/db/index.ts`）★

★★当期ブランチだけを変えます★★（★確定期間ブランチは触らない。決定6★）。

```ts
// 変更前
const errorReports = await prisma.robotErrorReport.findMany();

// 変更後（★本清掃行が要るので clean_timetables も読む★）
const [errorReports, timetables] = await Promise.all([
  prisma.robotErrorReport.findMany(),
  prisma.cleanTimetable.findMany(),
]);
const mainRows = timetables.filter(
  (row) => row.precheck === CLEAN_TIMETABLE_PRECHECK.main
);
const mainStartAtById = mainTimetableStartAtById(mainRows);
// ...
const currentWeekItems = errorBreakdownItemsFromCounts(
  countMainErrorTypesInRange(errorReports, mainStartAtById, currentWeekRange)
);
```

- ★★`CLEAN_TIMETABLE_PRECHECK` は `lib/config` からのインポート。`0` と直書きしない★★
- ★★当期の `rate` の分母は「その Map 内の全件数の合計」のまま★★
  （`errorBreakdownItemsFromCounts()` は変えません）— ★分子の集合が変わるので rate の値は変わります★
- ★★`errorBreakdown()` の docstring も直すこと★★ —
  ★「当期（今週・今月）: `robot_error_reports` を期間で絞って `error_type` ごとに数える
  （期間判定は `detectedAtFromError()`）」という記述が**古くなります**★

### 3-3. ★`CleanStatsBucket.errorCount` の docstring を書き換える★（決定5）

```ts
  /**
   * エラー発生回数。
   *
   * ★★本清掃行（`precheck === CLEAN_TIMETABLE_PRECHECK.main`）を指す
   *   `robot_error_reports` の件数★★（TeamWorks 第8回 A7。2026-09-11）—
   *   ★事前チェック行を指すエラーは含めない★（「事前チェックは本清掃を円滑に実施できる
   *   ためのもの」なのでサマリに含めない、という回答）。
   *   ★`clean_timetable_id` が `null` の行 / dangling の行も含めない★（決定3）。
   *
   * ★★以前ここには「事前チェック行に紐づく」と書いてあった★★ — ★第8回で質問する前の
   *   **仮定**であり、回答で逆だと分かった。★古い記述を残さない★（次に読む人が両方を信じるため）。
   *
   * ★注意: `clean_error` が 2（セキュリティ有効）/ 3（D-S制御盤異常）の清掃は
   *   `robot_error_reports` の行が作られないため、この件数に入らない★（第8回 A6-1）—
   *   ★★したがって「エラー発生率」は実質「ロボット異常の発生率」になる★★。
   */
  errorCount: number;
```

### 3-4. ★旧関数を削除する★

★★削除するもの★★（★削除前に `grep` で参照が0件になったことを確かめること★）:

| 関数 | 置き換え先 |
|---|---|
| ★`mainIdByPrecheckId()`★ | ★`mainTimetableStartAtById()`★ |
| ★`errorCountByMainId()`★ | ★`errorCountByMainTimetable()`★ |
| ★`countErrorTypesInRange()`★ | ★`countMainErrorTypesInRange()`★ |

- ★★対応する既存テスト（`describe("mainIdByPrecheckId / errorCountByMainId")` /
  `describe("countErrorTypesInRange")`）も一緒に消すこと★★ —
  ★消し忘れると `tsc` が落ちるので気づけます★
- ★★`PrecheckTimetableLike` が他で使われていないなら、それも消すこと★★（★必ず `grep` で確認★）
- ★★`detectedAtFromError()` は消さないこと★★（`prechecks()` が使っている）

### この段階の完了条件

- ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★
- ★旧関数への参照が0件（`grep` で確認した）★
- ★★コミット（`git add lib/db/index.ts lib/db/report-mapping.ts lib/db/__tests__/report-mapping.test.ts`）★★

---

## 段階3-4 — ★実測での検証★（★コミットなし・DB に1行も書かない★）

### 4-1. ★★「直った」ことの判定基準（★これが群3 の合否です★）★★

| # | 確認 | ★期待★ |
|---|---|---|
| ★1★ | ★当期（今週・今月）の `error_rate`★ | ★★段階3-0 で控えた値より**増えている**★★（★段階3-1 で足した本清掃エラーが分子に入った★） |
| ★2★ | ★`errorBreakdown()` の当期の `type`★ | ★★段階3-1 で入れた英語識別子（`EmergencyStop` 等）になっている★★ |
| ★3★ | ★`errorBreakdown()` の当期に、事前チェック行由来の `error_type` が★**出ていない**★★ | ★★出ていないこと★★ |
| ★4★ | ★B（null）と C（dangling）の2行が、★率にも内訳にもアラートにも出ていない★★ | ★★3箇所すべてで出ていない★★（決定3） |
| ★5★ | ★`db.prechecks()` の件数★ | ★★27 のまま★★（★アラートは変えていない★） |
| ★6★ | ★`db.cleanLogs()` の件数★ | ★★259 のまま★★ |
| ★7★ | ★確定期間（先週・先月）の `error_rate` と内訳★ | ★★段階3-0 と**同じ値**★★（★決定6。サマリは触っていない★） |

### 4-2. ★画面（回帰していないこと）★

- ★`/reports`★: ★★エラー発生率に数字が入っていること★★ / ★エラー種別内訳が英語識別子で出ていること★
  （★★D-077 の通り。日本語に見えなくても正しい★★）
- ★`/alerts`★: ★27件のまま★
- ★`/detail-log`★: ★259件のまま★（★群1・群2 を先に実行しているのでページ送りは付いています★）

### 4-3. ★`USE_FIXTURES=true` での確認★

- ★★fixtures の画面は1つも変わらないこと★★（★群3 は `lib/fixtures/` を触りません★）

### この段階の完了条件

- ★上の1〜7 を実測した★
- ★★DB に1行も書いていない★★
- ★一時スクリプトを消した（`git status` にゴミが出ていない）★

---

## 段階3-5 — ★記録★（1コミット）

### 5-1. `docs/decisions.md` に追記

★★着手直前に実測した空き番号を使うこと★★（見込み番号は本書に書いていません）。

★書くこと★:

1. ★出所（第8回 A7 の原文）★
2. ★決定1〜8 を ★★理由ごと★★★
3. ★★却下・保留したもの★★:
   - ★`null` のエラーを施設・期間に割り当てて分子に入れる案★ →
     ★「清掃1回に紐づかないエラーは清掃のエラー率の分子ではない」ので捨てる★
   - ★確定期間（サマリ）も同じ定義に揃える★ → ★★保留。第9回で照会（申し送り①）★★
4. ★★「実装ミスではなく、明記してあった仮定が回答で覆った」ことを書くこと★★ —
   ★仮定を docstring に明記する運用が機能した例として残す★
5. ★申し送り①②③（後述）★

### 5-2. `docs/handoff.md` に追記

★番号は実測すること★（★作成時点の最新は **追記36**★）。

### 5-3. push とこの段階の完了条件

- ★`git add docs/decisions.md docs/handoff.md`（★パスを明示。`-A` 禁止★）★
- ★★`git diff docs/decisions.md` を先に見て、別作業の未コミット追記を巻き込んでいないこと★★

### ★★群3 の終わり — ここで止めてもよい★★

★群4 に入らなくても、エラー率の分子の修正は単体で完結しています★。

---

# ★★群4 — 小さな積み残し3件★★

★★3件とも独立しています。1コミットにまとめてよい★★。

## 段階4-0 — 実測（★読むだけ・コミットなし★）

```bash
# ①「TeamWorks確認事項の回答待ち」という記述が事実に反することの再確認
grep -rn "算出基準はTeamWorks" lib/ docs/
grep -rn "判定なし\|FACILITY_STATUS_THRESHOLDS" docs/handoff/TeamWorks確認事項_第*.md

# ② fixtures の生成式
grep -n "GRADE_THRESHOLDS" lib/fixtures/clean-logs.ts lib/db/clean-log-mapping.ts

# ③ 足りないテストの場所
grep -n "describe(\"foldProblemDaysByDate\"\|a7:\|a2:" lib/db/__tests__/clean-log-mapping.test.ts
```

★控えること★: ★`npm test` のファイル数・件数（★基準★）★。

---

## ① ★`lib/judge/kpi.ts` の `judgeFacilityStatus()` のコメントが事実に反する★

### 何が問題か

```ts
/**
 * 施設別パフォーマンスの 良好 / 注意 / 要改善 / 判定なし
 *
 * 算出基準はTeamWorks確認事項の回答待ちのため、暫定で稼働率ベース。   ← ★★事実に反する★★
 * 閾値は `lib/config` の FACILITY_STATUS_THRESHOLDS から読む。
```

- ★★第2回〜第8回の確認書のどこにも、該当する質問が存在しません★★
  （★全文検索でヒット0。★着手時に自分でも検索して確かめること★★）
- ★★実際は DFA 側の UI 判断です★★ — ★モックの 80 / 90 という区切りを、
  既存の `FACILITY_STATUS_THRESHOLDS` に寄せたもの★
- ★★放置すると「TeamWorks から回答が来たら直す」という**来ない予定**を次の人が待ち続けます★★

### 直し方

★★「TeamWorks 回答待ち」ではない旨に書き換えること★★。★次の要素を含めること★:

1. ★★稼働率ベースであること★★（事実）
2. ★★それが DFA 側の UI 判断であること★★（★モックの区切りを既存の閾値に寄せた★）
3. ★★TeamWorks への照会は行っていないこと★★（★「回答待ち」ではない★）
4. ★見直すとしたら何がきっかけか★（★★「別の算出基準を採るという判断が出たとき」であって、
   「TeamWorks の回答が来たとき」ではない★★）
5. ★閾値は `lib/config` から読む、という既存の記述は残すこと★
6. ★★`null`（分母0＝判定なし）についての既存の説明は**そのまま残すこと**★★
   （★決定1。2026-09-11 ユーザー確定。★消さない★）

### ★転記先について（★実行者判断★）★

★同じ文言が `docs/handoff/analysis/code-analysis-pure.md` にも転記されています★
（★2977 / 3071 / 3195 行付近。★行番号は実測すること★）。

- ★★これは**分析資料**（コードのスナップショット）なので、直すかは実行者が決めてよい★★
- ★直さない場合は、★★`docs/decisions.md` の記録に「分析資料側は古い文言のまま」と書くこと★★
- ★★`docs/decisions.md:6577` の「⑦ 判定なしステータスの算出基準（暫定で稼働率ベース）」という
  申し送り行は**消さないこと**★★（★過去の記録であり、履歴として正しい★）—
  ★新しい記録の側で「TeamWorks 回答待ちではなかった」と訂正すること★

---

## ② ★`lib/fixtures/clean-logs.ts` の生成式★ — ★★取り下げ。直さない★★

★★監督判断（2026-09-11）で本項は取り下げました。手を付けないでください。★★

- 当初「`lib/fixtures/clean-logs.ts` の生成式が `<` のままで、実装側の `<=`（D-109 決定3）とずれている」
  として直す予定でした
- ★調査の結果、これは既に「意図的な放置」として記録済みでした★ —
  `lib/db/clean-log-mapping.ts` の `isProblemCompletion()` の docstring に
  ★「fixtures の生成式は `<` のままだが、生成される完了率に 70.0 ちょうどは現れないので矛盾しない
  （`lib/fixtures/` は変更禁止）」★ と明記されています
- ★実測でも 70.0 ちょうどは生成され得ません★（生成式は `5 + r*40` / `72 + r*15` / `90.5 + r*9`）
- ★`lib/fixtures/` は変更禁止★という取り決めもあります。
  ★記録済みの判断を、実益なしに覆すことになる★ので取り下げます
- ★将来 fixtures の生成式を触るときに顕在化する潜在的な不一致であることは事実★なので、
  ★段階4-2 の記録にその旨を残すこと★（★直さなかった理由も含めて★）

## ③ ★テストが1ケース足りない★

### 何が足りないか

★★同じ日に「履歴が無い行」と「正常終了の行」が混在するケース★★が固定されていません。

- ★`lib/db/__tests__/clean-log-mapping.test.ts` の `describe("foldProblemDaysByDate")` には
  a1〜a9 が揃っています★ — ★★`a2`（問題行 50 + 正常行 95）と `a7`（未実施 null 単独）はあるが、
  **その2つを組み合わせた形（null + 正常行）が無い**★★
- ★★指示書V で名指ししたのに固定されていません★★

### なぜ重要か

- ★★「履歴が無い行」は `completionRateFromHistory()` が `null` を返します★★
  （★出発失敗・データ収集の欠落のどちらでも `null` になる★）
- ★`isProblemCompletion(null) === true`★ なので、
  ★★同じ日の行を OR で畳むと**その日は問題日**になります★★
- ★★これが期待値です★★ — ★★「正常終了した行があるのだから問題日ではない」ではありません★★
  （★D-109 決定：同一日は OR で1日に畳む★）

### 足すテスト（★期待値表★）

| # | ケース | 入力（施設1・同一日） | ★期待★ |
|---|---|---|---|
| ★★a10★★ | ★★同一日に「履歴が無い行（`completion_rate === null`）」と「正常終了の行（95）」が混在★★ | `09-01(null)` + `09-01(95)` | ★★`[{ date: "2026-09-01", isProblem: true }]`（**1要素**）★★ |

★★併せて `computeConsecutiveProblemDays()` 側でも固定すること★★（★同じ入力で連続日数が 1 になる★）:

| # | ケース | 入力 | ★期待★ |
|---|---|---|---|
| ★★b12★★ | ★同一日に null 行と正常行 → その日は問題日★ | `id1: 09-01(null)` / `id2: 09-01(95)` | ★★`map.get(1) === 1` / `map.get(2) === 1`★★（★★両方 1。`id2` が 0 にならないこと★★） |

★★テストのコメントに理由を書くこと★★:

```ts
// ★★「正常終了した行があっても、同じ日に履歴が無い行があればその日は問題日」★★
//   `isProblemCompletion(null) === true` なので OR で畳むと true になる（D-109 決定）。
//   ★★気づいた人が「正常終了しているのだから false では」と直さないように固定する★★
```

- ★置き場所★: ★`describe("foldProblemDaysByDate")` の a9 の前（a7 の近く）／
  `describe("computeConsecutiveProblemDays")` の b11 の後★
- ★★既存のテストを1つも書き換えないこと★★（★追加するだけ★）

---

## 段階4-1 — ★3件を実装する★（1コミット）

### 手順

1. ★★③ のテストを先に足す★★（★★通ることを確認する★★ — ★この1件は既存実装で通るはずです。
   ★★通らなかったら、それは実装の不具合であり、本書の想定と違います — 止めて報告すること★★）
2. ★① のコメントを書き換える★
3. ★② の比較演算子を直し、★★fixtures の出力に差分が0 であることを実測する★★★

### この段階の完了条件

- ★`npx tsc --noEmit` = 0 / `npm run lint` = 0★
- ★`npm test` 全パス。★★件数が段階4-0 の基準 +1 または +2 になっていること★★★
- ★★fixtures の出力差分が0★★（②）
- ★★「TeamWorks 回答待ち」という文字列が `lib/` から消えていること★★（①）:
  ```bash
  grep -rn "算出基準はTeamWorks" lib/     # ★0件★
  ```
- ★★コミット★★（★パスを明示★）:
  ```bash
  git add lib/judge/kpi.ts lib/fixtures/clean-logs.ts lib/db/__tests__/clean-log-mapping.test.ts
  ```
  （★`docs/handoff/analysis/code-analysis-pure.md` も直したならそれも明示的に追加★）

---

## 段階4-2 — ★記録と push★（1コミット）

- ★`docs/decisions.md` に追記★（★★群3 と同じ番号にまとめてもよい★★。実行者判断）
- ★書くこと★:
  1. ★★① は「TeamWorks 回答待ちではなく DFA 側の UI 判断だった」こと★★
     （★★確認書の全文検索でヒット0 だったという**実測の事実**を書くこと★★）
  2. ★② は「生成式の演算子だけを直し、出力は1件も変わらないことを実測した」こと★
  3. ★③ は「指示書V で名指ししたのに固定されていなかった1ケースを足した」こと★
  4. ★`docs/handoff/analysis/code-analysis-pure.md` を直したか直さなかったか★
- ★`docs/handoff.md` に追記★
- ★push★

### ★★群4 の終わり — ここで止めてもよい★★

---

## ★★申し送り — 本書では決着させないこと（★記録に必ず書くこと★）★★

| # | 論点 | 状態 |
|---|---|---|
| ★①★ | ★★サマリ（確定期間）が事前チェックを除外しているか★★ | ★★未確認★★。★第8回 A7 は「除外するのが正しいでしょう」という**見解**であって、現行の集計バッチがそうなっているという**事実の確認ではない**★。★★第9回の確認事項に載せること★★。★確定期間と当期で分子の定義が食い違っている可能性が残る★ |
| ★②★ | ★★`clean_error` が 2（セキュリティ有効）/ 3（D-S制御盤異常）の清掃はエラー行が作られない★★（第8回 A6-1） | ★★仕様として受け入れる★★。★★帰結として「エラー発生率」は実質「**ロボット異常**の発生率」になる★★ — ★この定義を `docs/decisions.md` と `CleanStatsBucket.errorCount` の docstring の両方に明記すること★ |
| ★③★ | ★`clean_timetable_id` が `null` のエラー行を捨てる判断★ | ★本書で「捨てる」に確定★。★ただし `facility_id` と `errored_at` を持つので**施設・期間には割り当てられる**（入れる選択肢は技術的にある）★。★★実データで null がどれくらい出るかが分かった時点で見直す余地がある★★ |
| ★④★ | ★`errorBreakdown()` の当期の `rate` の分母★ | ★「その期間の全エラー件数」のまま★。★分子の集合を絞ったので、★★確定期間（サマリ由来）の rate と母集団の定義が違う可能性★★がある — ①と同じ照会で解消する★ |
| ★⑤★ | ★`lib/fixtures/clean-logs.ts` の生成式と `isProblemCompletion()` の共通化★ | ★★本書ではやらない★★（群4 ② は演算子1つだけ）。★共通化すると fixtures の値が変わりうるので、別作業として切り出すこと★ |
| ★⑥★ | ★`docs/handoff/analysis/code-analysis-pure.md` の古い転記★ | ★実行者判断（群4 ①）★。★直さなかった場合は記録に書くこと★ |
| ★⑦★ | ★群1・群2 の指示書と現状の行番号のずれ★ | ★★各群の段階0 で実測して埋める運用にした★★。★指示書Z / AA 自体は本書では改訂しません★（★別のエージェントが並行して前提の点検を行っています★） |

---

## 完了条件（全体）

| # | 条件 |
|---|---|
| ★1★ | ★★群1（指示書Z）を頭から実行し、その指示書の完了条件を満たした★★ |
| ★2★ | ★★群2（指示書AA）を頭から実行し、その指示書の完了条件を満たした★★ |
| ★3★ | ★★群3 の段階3-1〜3-5 を実行した★★（★当期の `error_rate` が段階3-0 より増えている★） |
| ★4★ | ★★群4 の①②③ を実行した★★（★`grep -rn "算出基準はTeamWorks" lib/` が0件★） |
| ★5★ | ★★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス★★ |
| ★6★ | ★`db.cleanLogs()` = 259件 / `db.prechecks()` = 27件（★群3・群4 で変わらない★）★ |
| ★7★ | ★★確定期間（先週・先月）の数字が群3 の前後で変わっていない★★ |
| ★8★ | ★`docs/decisions.md` / `docs/handoff.md` に記録した（★実測した番号で★）★ |
| ★9★ | ★★`git status` にゴミ（一時スクリプト・計測用 JSON）が残っていない★★ |
| ★10★ | ★★`README.md` / `docs/code-map.md` / `docs/handoff/管理画面_設計検討_20260910.md` /
`.claude/worktrees/**` に触っていない★★ |

> ★★途中で止めた場合★★: ★どの群のどの段階まで終わったかを作業メモに残し、
> 本書末尾の「実行状態」表を更新すること★。★★止めた状態でも `tsc` / `lint` / `test` は
> 緑であること★★（段階割りはそうなるように組んであります）。

---

## 実行状態

| 群 | 段階 | 状態 | 決定番号 | 備考 |
|---|---|---|---|---|
| 群1 | 指示書Z 段階0〜8 | ★★完了（2026-09-11）★★ | ★D-119★ | `3bfed11` / `9edc547` / `49b27cf` / `53a6d21` / `82d98d5` / `828afe6` + 記録コミット。★指示書Z の実行状態表も更新済み★。★★push は未実施（監督確認待ち）★★ |
| 群2 | 指示書AA 段階0〜7 | ★★完了（2026-09-11）★★ | ★D-120★ | `fd4ca5b` / `fcdcb07` / `9e9704c` / `78fbfac` / `77693c4` + 記録コミット。★指示書AA の実行状態表も更新済み★。★★これで4群すべて完了★★。★★push は未実施（監督確認待ち）★★ |
| 群3 | 段階3-0 | ★★完了（2026-09-11）★★ | — | ★着手前の実測。コミットなし★ |
| 群3 | 段階3-1（シード） | ★★完了★★ | ★D-118★ | `b669af7`。本清掃エラー33件 + null/dangling 各1件。★既存14件は不変★ |
| 群3 | 段階3-2（純粋関数） | ★★完了★★ | ★D-118★ | `fc3d789`。期待値表 a/b/c/d をテストに転記（+25件） |
| 群3 | 段階3-3（配線） | ★★完了★★ | ★D-118★ | `355fe0d`。旧関数3本と `PrecheckTimetableLike` を削除（参照0件を確認） |
| 群3 | 段階3-4（実測） | ★★完了★★ | — | ★★当期の error_rate は 50%→10.714% / 19.44%→12.5% と**減った**★★（本書の想定は「増える」。★理由と、それでも直っている根拠は D-118 項4★） |
| 群3 | 段階3-5（記録） | ★★完了★★ | ★D-118★ | `docs/decisions.md` D-118 / `docs/handoff.md` 追記38。★push は未実施（監督確認待ち）★ |
| 群4 | 段階4-0〜4-2 | ★★完了（2026-09-11）★★ | ★D-117★ | ★①③ のみ実施。★②は取り下げ済みなので `lib/fixtures/` には触っていない★。テスト 1023→★1025★ / `tsc` 0 / `eslint` 0 / `grep "算出基準はTeamWorks" lib/` = ★0件★。★`code-analysis-pure.md` は古い文言のまま★（実行者判断）。★push は未実施（監督確認待ち）★ |
