# 指示書Z — エリア別完了率を実装する（501 仮実装をやめ、施設×日付のエリア一覧にする）

| 項目 | 内容 |
|---|---|
| 実行環境 | ★**Cursor 内の Claude が実装します**★（`.claude/commands/d-watch.md` の役割分担表を参照） |
| 実行状態 | ★★**完了（2026-09-11）**★★ — ★段階0〜8 を実行。★★push は未実施（監督確認待ち）★★★。決定番号 ★D-119★ / `docs/handoff.md` ★追記39★ |
| 実行順 | ★★**指示書Y（`docs/handoff/指示書Y_DATETIMEを日本時間として読む.md`）と 指示書W（`docs/handoff/指示書W_連続問題施設を実DBから作る.md`）の完了後に実行すること**★★。★★**同時に走らせないこと**★★（★理由は下の「実行順」節★） |
| 対象 | ★★TeamWorks 第8回 A9-2 で「エリア毎の清掃完了率は取得可能」と回答があった★★。★ただし当方が聞いた粒度（1回の清掃の中を売り場・通路・レジ前に区切った内訳）とは**違う答え**★。★取得できるのは「**1エリア（＝1ロボット）につき1つの完了率**」★。★★そこで `GET /clean-logs/{id}/areas` の意味を「**その清掃ログと同じ施設・同じ日の全エリアの一覧**」に定義し直し、501 仮実装をやめて本実装する★★ |
| 取得する決定番号 | ★★**着手直前に `grep -nE "^### D-(0\|1)[0-9][0-9]" docs/decisions.md \| tail -3` で最終番号を実測すること**★★。★★`^### D-0` だけでは3桁台（D-100 以降）を拾えない★★（★2026-09-07 に `D-068` の二重採番が実際に起きています★）。★★本書作成時点の最終番号は `D-114` ですが、**指示書Y と 指示書W がそれぞれ1つずつ取る見込み**です。★★見込み番号は書きません — 必ず自分で実測してから採番すること★★★ |
| 前提 | ★**D-114**（★第8回回答の受領。★★項4 が本書の出所★★。行番号は着手時に `grep -n "^### D-114" docs/decisions.md` で実測すること）★ / ★**D-034**（`docs/decisions.md:208`。★「確認事項#10 が未確認なので 501 仮実装」と決めた当の決定★。★本書はこれを**部分的に上書き**する★）★ / ★**D-106**（`:5830`。`clean_error` による判定）★ / ★**D-107**（`:5993`。★`isFutureSchedule()` の導入。★★落とし穴1「未実施を 0% にすると null が 0 に化ける」★★）★ / ★**D-109**（`:6328`。当期の分母から未来を外す）★ / ★**D-113**（`:6840`。`precheck` は分数そのもの）★ / **D-035**（`:216`。1清掃エリア=1ロボット）/ **D-040**（`:220`。★API契約はYAMLが先★）/ **D-041**（`:224`。期待値表テスト）/ **D-042**（`:226`。書き込み許可4カラム）/ **D-044**（fixtures で開発）/ **D-050**（段階ごとに検証してからコミット）/ **D-056**（`schema.prisma` は生成物）/ **D-100**（MariaDB 10.11 とローカル MySQL 8.4 の差）/ ★**指示書Y と 指示書W が段階末で追記する記録**（★★着手時に実測して読むこと★★）★ |
| 段階数 | ★**9段階**★（段階0〜段階8）。★コミットは段階1 / 2 / 3 / 4 / 5 / 6 / 8 の**7つ**★ |
| ★着手前確認★ | ★★**なし**★★ — ★「施設内のエリアごとの完了率を出す画面として作る」はユーザー確定済み（2026-09-11）★。★データ元・全体率の出し方・並び順・未実施の扱い・パスを変えないことは、すべて下の「採る方針」で**監督が決定済み**★。★★ユーザーの回答を待たずに段階0 から通しで実行してよい★★。★★ただし「仕様が不明だと感じた点」は勝手に埋めず、段階の切れ目で止めて質問すること★★（`CLAUDE.md` §0-1 / §7） |
| 着手前の基準 | ★★**着手直前に自分で `npm test` / `npx tsc --noEmit` / `npm run lint` を実行し、その実測値を基準にすること**★★。★指示書Y・W の完了後は本書作成時点と数字が変わっています。参考値を鵜呑みにしないこと★ |
| 作成 | 2026-09-11。★作成時点の決定事項最終番号は **D-114**、`docs/handoff.md` の最新は **追記34**★。★★着手時に実測すること★★ |

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

> ★**この指示書を読んでいるあなたへ**★ — `.claude/commands/d-watch.md` の「実装は Cursor 側」は
> **2つの環境の役割分担**です。**Cursor の中で動いている Claude は実装担当**です。
> ★調査や改訂ではなく実装を行ってください。★
> ★★**うまくいかなければ段階の切れ目（コミット単位）で止めて構いません**★★ —
> ★段階1（API契約）→ 段階2（型と純粋関数）→ 段階3（fixtures）→ 段階4（データ層）→
> 段階5（APIルート）→ 段階6（画面）の順に、★どこで止めても `tsc` / `lint` / `test` が緑になる形★に
> 並べてあります★。★★段階5 を終えるまで画面は 501 のメッセージを出したままです（想定内）★★。

---

## ★★冒頭に読むべきこと — 何が確定して、何が確定していないか★★

### 1. ★TeamWorks 第8回 A9-2 の回答（★2026-09-11 受領。原文★）★

```
Q9-2. エリア毎の清掃完了率は取得可能か
A9-2.robot_task_reportsからエリア毎の清掃完了率は取得可能です. 正確に言えば清掃ロボット毎の清掃率ですが,
清掃ロボットはエリアに紐づいているため, もし複数台紐づいているのであれば計算で導き出すことになります.
```

### 2. ★★これは当方が聞いた粒度とは違う答えです（★ここを取り違えないこと★）★★

| | 内容 | 取得可否 |
|---|---|---|
| ★当方が聞いたこと★ | ★**1回の清掃の中**を売り場・通路・レジ前に区切ったときの★★区域別の内訳★★★ | ★★**取得できない**（依然として未解決）★★ |
| ★先方が答えたこと★ | ★**清掃ロボット毎**の清掃率。★★ロボットはエリアに紐づくので「1エリアにつき1つの完了率」★★★ | ★取得できる★ |

★★本書が実装するのは**後者だけ**です★★。
★★前者（確認事項#10 の本来の問い）は依然として未解決であり、本実装で解決したことにはなりません★★
（★第9回の確認事項に「エリアの粒度の聞き直し」として積んであります。D-114 項7-6★）。
★申し送りに必ず残すこと（段階8）★。

### 3. ★ユーザー確定（2026-09-11）★

> ★★「施設内のエリアごとの完了率」を出す画面として作る★★

★モックのエリアモーダルは元から「施設 × 日付 → その施設のエリア一覧」の形★（`d-watch-mock.jsx` の
`getAreaData(facility)` は **施設名だけ** を引数に取る。★おおよそ276-282行。行番号は実測すること★）。
★★つまりモックが要求している形は、A9-2 で取得できる粒度で**そのまま実現できます**★★。

### 4. ★★現状（★実装前にこの目で確かめること★）★★

| 場所 | 現状 |
|---|---|
| `app/api/clean-logs/[id]/areas/route.ts` | ★`db.cleanLogs()` で 404 判定だけして、★★常に 501 を返す★★★ |
| `components/features/detail-log/AreaModal.tsx` | ★★501 のメッセージを表示するだけの器★★。★フィルタボタン（問題エリアのみ / 全件）は `onClick={() => undefined}` の**飾り**★ |
| `components/features/detail-log/DetailLogScreen.tsx` の `openAreas()` | ★★「失敗側が通常経路」というコメント付きで、501 のメッセージを拾ってモーダルに渡している★★ |
| `d-watch-api.yaml`（★おおよそ 362-400 行★） | ★`description` に「確認事項#10 に依存 / 取得不可なら Phase B 送りで 501」★。★`"501"` のレスポンス定義あり★。★冒頭の注記（★おおよそ 23-24 行★）にも同じことが書いてある★ |
| `lib/types/` | ★★`AreaCompletionItem` 相当の型が**無い**★★ |
| `lib/fixtures/` | ★★エリア別のサンプルデータが**無い**★★ |
| `CleanLogItem`（`lib/types/index.ts`） | ★★`section_id` / `section_name` を**持っていない**★★ |

---

## ★★採る方針（★監督が決定済み。理由ごと理解してから実装すること★）★★

### ★決定1: API のパスは変えない★

★`GET /clean-logs/{id}/areas` のまま★。★★意味だけを「その清掃ログと**同じ施設・同じ日**の
全エリアの一覧（★その清掃ログ自身を含む★）」に定義し直す★★。

- ★理由★: ★画面は**清掃詳細ログの行から**開く★（`CleanLogTable.tsx` の「エリア別詳細を見る →」ボタン）。
  ★id で引く形が自然★。★パスを `/facilities/{id}/areas?date=...` のように変えると呼び出し側も変わる★。
- ★★`{id}` は「その施設・その日を特定するための入口」であって、「その1行だけのデータを引くキー」ではない★★ —
  ★このズレを `d-watch-api.yaml` の `description` に明記すること（段階1）★。

### ★決定2: `d-watch-api.yaml` を先に直す★（`CLAUDE.md` §6-1 / D-040）

- ★`description` の「確認事項#10 に依存 / 取得不可なら Phase B 送りで 501」を書き換える★
- ★★`"501"` のレスポンス定義を**削除する**★★
- ★冒頭の注記（`## データソースに関する注記`）の該当2行も直す★

### ★決定3: レスポンスに「全体の完了率」を足す★

★モックのヘッダは `施設名 | 日付 | 全体 xx% ◎` の形★（`d-watch-mock.jsx` のエリアモーダル。
★おおよそ 1917-1975 行★）。★この「全体」は**その施設・その日の全エリアを合算した値**★。

- ★★面積ベースで合算する★★: ★★`Σclean_area ÷ Σtask_area × 100`★★（`CLAUDE.md` §4.2）
- ★★`clean_ratio` の単純平均は禁止★★ — ★担当面積の違う台を等しく扱うことになる★
- ★★既存の `completionRate(cleanedArea, targetArea)`（`lib/judge/kpi.ts`）を使うこと★★。
  ★新しい関数を作らない★。★分母0で `null` を返す挙動もそのまま使う★

### ★決定4: 未実施のエリアは `completion_rate` を `null` にする★（★0% と混ぜない★）

★「0%しか清掃できなかった」と「そもそも実施していない」は別物★
（★D-106 / D-107 落とし穴1 と**同じ地雷**★。★判定はどちらも × なので、混ぜても画面もテストも赤くなりません★）。

- ★★`completionRateFromHistory()`（`lib/db/clean-log-mapping.ts`）を使えば自動的にこうなります★★。
  ★自前で `history.clean_ratio` を読まないこと★
- ★★YAML の `completion_rate` を `nullable: true` にする★★（段階1）

### ★決定5: 判定は `judgeGrade()` を使う★

- ★★モックの `areaGrade()` は `cp >= 90 ? "◎" : cp >= 70 ? "○" : cp >= 10 ? "△" : "×"` で、
  `CLAUDE.md` §4.1 の「90%**超**」＝ `>` と**違う**★★（`d-watch-mock.jsx`。★おおよそ282行★）
- ★★モックのロジックを移植しないこと★★。★移植してよいのは**文言・列・並び順・レイアウト**だけ★
- ★`judgeGrade(null)` は `×`（未実施）★。★これで未実施エリアも「問題エリア」に入る★

### ★決定6: データ元は `clean_status_histories`★（★`clean_task_reports` は使わない★）

- ★`prismaSource.cleanLogs()` が**既に読んでいる**テーブル★。★面積（`task_area` / `clean_area`）・
  完了率（`clean_ratio`）・エリアID（`section_id`）を持っている★
- ★★理由★★: ★`cleanLogs()` の `completion_rate` と数字が食い違わないため★。
  ★★同じ画面（清掃詳細ログの行 → そこから開くモーダル）に、定義の違う2つの数字が並ぶ状態を作らない★★
- ★`clean_task_reports` は★★シードに1行も無く★★、`clean_status_histories` と値が一致するかも未検証★
- ★A9-2 の `robot_task_reports` は DDL 上の `clean_task_reports` を指すと思われるが★★未確認★★
  （第9回で聞く。D-114 項7-5）

### ★決定7: 完了率の出し方を `cleanLogs()` と揃える（★★非対称を意図して残す★★）★

★★`completionRateFromHistory()` は `clean_ratio` の**生値をそのまま返しており、面積から再計算していません**★★
（`lib/db/clean-log-mapping.ts`。★実装を自分の目で確かめること★）。

| | 出し方 |
|---|---|
| ★各エリアの行の `completion_rate`★ | ★★`clean_ratio` の生値★★（`completionRateFromHistory()` を流用） |
| ★全体の `overall_completion_rate`★ | ★★`Σclean_area ÷ Σtask_area × 100`（面積ベース）★★ |

★★この非対称は意図した挙動です★★。★★合計が行の平均と一致しないことがあります★★。

- ★★`completion_rate` を返す関数と、全体を計算する関数の**両方の docstring に明記すること**★★
- ★★段階8 の申し送りにも書くこと★★
- ★理由★: ★行の値を面積から再計算すると `cleanLogs()` の完了率と食い違う（決定6 と同じ理由）★。
  ★一方、全体を `clean_ratio` の平均で出すと担当面積の違う台を等しく扱うことになる（決定3）★。
  ★★どちらを選んでも「揃わない」ので、**揃わないことを明示する**ほうを選んだ★★

### ★決定8: `filter=problem|all` はサーバ側で絞る★

- ★契約（YAML）にあるので維持する★。★★既定は `problem`★★
- ★★`problem` = 判定が △ または ×★★（★未実施は `judgeGrade(null) = ×` なので入る★）
- ★★`overall_completion_rate` と件数は `filter` に**関係なく全エリアから**計算する★★ —
  ★「問題エリアのみ」を選んだら全体率まで変わる、という挙動にしないこと★

### ★決定9: 未来の予定は除外する★

★`isFutureSchedule()`（`lib/db/clean-log-mapping.ts`）を使う★（D-107 / D-109）。
★`cleanLogs()` と**同じ理由・同じ関数**★ — ★外すと未来の清掃が「未実施」として並びます★。

### ★決定10: 並び順は完了率の昇順。★★未実施（null）は先頭★★★

- ★モックに合わせる★（`areas.sort(function (a, b) { return a.cp - b.cp; })`）
- ★★悪いものから見る画面なので、未実施（＝いちばん悪い）を先頭に置く★★
- ★同値のときは `section_id` の昇順で安定させる★（★並びが実行ごとに変わらないようにするため★）
- ★★フロントで並べ替えないこと★★（`CLAUDE.md` §9.6。★ソート順はAPIから受け取る★）

### ★決定11: `meta` は付けない★

★`CLAUDE.md` §9.3 の「一覧APIは必ず `meta: { total, page, per_page }` を返す」は
**ページングする一覧**の話です★。

- ★★エリア数は最大でも数個（P1 で最多の施設7 でも3件）。ページングの対象ではない★★
- ★先例: `GET /clean-logs/{id}/advice` も `meta` を返していない★（`CleanLogAdviceResponse`）
- ★★ただしモックの件数行（`問題エリア（3 / 16エリア）`）を出すために `total_count` /
  `problem_count` は返す★★（★これは `meta` ではなく本文の一部★。理由は決定12）

### ★決定12: `total_count` と `problem_count` を返す★

★モックの件数行はこうなっています（★文言は段階6 の表を正とする★）★:

```
filter=problem のとき → 問題エリア（3 / 16エリア）
filter=all     のとき → 全16エリア（完了率昇順）
```

★★サーバ側で絞る（決定8）と、`filter=problem` のときフロントは「全体で何エリアあるか」を
知る手段がありません★★。★だから両方の件数を返します★。
★★どちらも `filter` の**前**に数えること★★。

### ★★却下した案（★同じ案を再提案しないために、理由ごと残します★）★★

| 案 | 却下の理由 |
|---|---|
| ★パスを `/facilities/{id}/areas?date=` に変える★ | ★呼び出し側（清掃詳細ログの行）が施設IDと日付を組み立て直すことになる★。★契約変更の影響が広がる★（決定1） |
| ★`clean_task_reports` を読む★ | ★シードに1行も無く、`clean_status_histories` と値が一致するかも未検証★。★同じ画面に定義の違う数字が並ぶ★（決定6） |
| ★行の `completion_rate` も面積から再計算して揃える★ | ★`cleanLogs()` の完了率（＝一覧の行に出ている数字）と食い違う★（決定7） |
| ★全体を `clean_ratio` の単純平均で出す★ | ★担当面積の違う台を等しく扱うことになる★（決定3。`CLAUDE.md` §4.2 違反） |
| ★未実施エリアを 0% として並べる★ | ★D-107 落とし穴1 と同じ地雷★（決定4） |
| ★`CleanLogItem` に `section_id` / `section_name` を足す★ | ★★この API のためだけに一覧の型を変えない★★。★`CleanLogItem` は6画面が参照している★（決定14） |
| ★フロントで絞り込み・並べ替えをする★ | ★`CLAUDE.md` §9.6「判定結果・ソート順はAPIから受け取る。フロントで再計算しない」★ |

### ★決定13: 未実施行の面積は「DBの値をそのまま足す」★

★★既存の `aggregateCleanStatsByFacility()`（`lib/db/report-mapping.ts`）と**同じ扱い**にする★★:

```ts
taskArea  += history?.task_area  ?? 0;
cleanArea += history?.clean_area ?? 0;
```

- ★★未実施だからといって当方で 0 に潰さないこと★★。★履歴の行が無いときだけ 0★
- ★理由★: ★同じ「面積ベースの完了率」がレポート画面（F-04）とこの画面で違う定義になるのを避ける★
- ★ローカルDBでは未実施行の `task_area` は 0 なので差は出ません★
  （`scripts/seed-local-db.ts` の `areaFields`。★★シードは読むだけ。編集しないこと★★）。
  ★★実DBで未実施行の `task_area` が非0だと全体率が下がります★★ → ★申し送りに書くこと★

### ★決定14: 型は areas 専用に新設する★（`CLAUDE.md` §9.2）

- ★★`lib/types/` に `AreaCompletionItem` を新設★★（★YAML のスキーマ名と揃える★）
- ★★`CleanLogItem` は変更しないこと★★

---

## ★調査済みの事実（★現物で裏を取ってから使うこと。★★行番号は必ず実測すること★★）★

### 1. ★`clean_status_histories` が持っているもの（`prisma/schema.prisma`）★

| カラム | 型 | 備考 |
|---|---|---|
| `facility_id` / `section_id` | `BigInt` | ★エリアの特定に使う★ |
| `clean_timetable_id` | `BigInt` | ★★FK も KEY も無い★★（`include` で辿れない。手動 join） |
| `clean_error` | `Int` | ★1 = 異常(清掃未実施)★ |
| `task_area` / `clean_area` | `Float` | ★★面積ベースの全体率の分母・分子★★ |
| `clean_ratio` | `Float` | ★行の `completion_rate` の元★（`double(4,1)`） |

### 2. ★`section_name` の取り方★

★`clean_timetables.section_id` → `sections.section_name`★。
★`prismaSource.cleanLogs()` は既に `include: { section: { include: { robots: ... } } }` で
`section` を引いています★ — ★★同じ書き方が使えます（`section` にはリレーションがあるので
`include` してよい）★★。

★ローカルDBの `section_name` は `清掃エリア（ロボット107）` の形★
（`scripts/seed-local-db.ts`。★★編集しないこと★★）。
★★モックの「1F 生鮮エリア」のような名前にはなりません。これは想定内です★★ —
★`section_name` は DB の値をそのまま出すこと（当方で作らない）★。

### 3. ★同じ施設・同じ日に複数エリアがあるデータは実在する★

| 施設 | ロボット（＝清掃エリア） |
|---|---|
| ★施設7（メガバリューマート緑野店）★ | ★★3台★★（`8111R5628060099` / `...0100` / `...0177`） |
| 施設5 | 2台 |
| ★それ以外★ | ★1台★ |

- ★★施設7 で3エリアが返ること★★を段階7 で実測すること
- ★★1台しかない施設で**1件だけ**返るのが正しい挙動★★（★エラーではない★）
- ★施設7 の3台が**同じ日**に予定を持つのは `daysBack: 1` の1日だけ★
  （`lib/fixtures/clean-logs.ts`。★実測すること★）。★指示書V が足した検証データ（`daysBack: 2/3/4`）は
  2台ずつ★

### 4. ★★`lib/fixtures/` にエリア別のデータが無い★★

- ★★新設が要る★★（`lib/fixtures/areas.ts`。★`lib/fixtures/index.ts` に `export *` を足す★）
- ★★既存の値は1行も変更しないこと★★。★★追加のみ★★
  （`lib/fixtures/clean-logs.ts` の生成式に手を入れない。指示書U / V が同じ制約で通っています）

### 5. ★画面側の現状（★配線が要る★）★

| 場所 | 現状 | 段階6 でやること |
|---|---|---|
| `DetailLogScreen.tsx` の `openAreas()` | ★501 のメッセージを拾う★ | ★200 のレスポンスを受け取り、`filter` の状態を持つ★ |
| `AreaModal.tsx` | ★メッセージ1行を表示するだけ★ | ★★一覧・バー・判定バッジ・件数行・空表示★★ |
| `AreaModal.tsx` のフィルタボタン | ★`onClick={() => undefined}` の飾り★ | ★★再取得に配線する★★ |
| `components/ui/ProgressBar.tsx` | ★`percent` / `tone`（ok / wn / ng / ac / pr）★ | ★★これを使う。バーを自作しないこと★★ |
| `components/ui/GradeBadge.tsx` | ★`grade` を受けて色分け★ | ★★これを使う★★ |

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

| # | 落とし穴 |
|---|---|
| ★1★ | ★★未実施を 0 にする★★ — `judgeGrade(0)` も `judgeGrade(null)` も `×` なので**画面もテストも赤くならない**（D-107 落とし穴1）。★★`completionRateFromHistory()` を通すこと★★ |
| ★2★ | ★★`??` を `\|\|` にする★★ — `completion_rate` は `0` が正当な値。`\|\|` だと ★0 が null に化ける★ |
| ★3★ | ★★モックの `>=` を移植する★★ — 完了率ちょうど 90.0 が ◎ になる（正しくは ○）。★★`judgeGrade()` を通すこと★★ |
| ★4★ | ★★丸めた値で判定する★★ — `Math.round()` は**表示のときだけ**（`CLAUDE.md` §4.3） |
| ★5★ | ★★`isFutureSchedule()` を通し忘れる★★ — 未来の予定が「未実施」として並ぶ（D-107） |
| ★6★ | ★★日付の突き合わせに `row.start_at` の生の `Date` を使う★★ — ★★`jstIsoFromDb()` を通してから `.slice(0, 10)` で JST の日付にすること★★（指示書Y / D-114 項1。`aggregateCleanStatsByFacility()` が同じことをしています） |
| ★7★ | ★★`BigInt` を `Map` のキーに `===` で使う★★ — ★`.toString()` すること★（既存コードと同じ） |
| ★8★ | ★★`filter=problem` のときに絞ったあとの配列から全体率や件数を計算する★★ — ★絞る**前**に計算すること★（決定8 / 決定12） |
| ★9★ | ★★分母0で `0` を返す★★ — ★`completionRate()` は分母0で `null` を返します。それをそのまま使うこと★（★全エリアが未実施の日は「全体 -」になるのが正しい★） |

---

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

| # | 禁止 |
|---|---|
| ★1★ | ★★`git add -A` の使用★★（`CLAUDE.md` §7。★必ずファイルパスを明示する★） |
| ★2★ | ★★メインDBのスキーマ変更・マイグレーション作成★★（`prisma/schema.prisma` は生成物。D-056） |
| ★3★ | ★★許可4カラム以外へのDB書き込み★★（D-042）。★★本書は**読み取りのみ**★★ |
| ★4★ | ★★閾値のハードコード★★（90 / 70 / 10 を直書きしない。`lib/config` から読む。`CLAUDE.md` §7） |
| ★5★ | ★★丸めた値で判定すること★★（`CLAUDE.md` §4.3） |
| ★6★ | ★★モックの `areaGrade()`（`>=`）を移植すること★★（決定5） |
| ★7★ | ★★`clean_ratio` の単純平均を全体率にすること★★（決定3） |
| ★8★ | ★★`lib/fixtures/` の既存値を変更すること★★（★追加のみ★） |
| ★9★ | ★★`CleanLogItem` を変更すること★★（決定14） |
| ★10★ | ★★`clean_task_reports` を使うこと★★（決定6） |
| ★11★ | ★★`README.md` / `docs/code-map.md` / `docs/handoff/管理画面_設計検討_20260910.md` /
`.claude/worktrees/**` / 他の指示書に触れること★★ |
| ★12★ | ★★一時スクリプトをリポジトリに残すこと★★（★過去に `scripts/tmp-measure.ts` が残った★。段階7 参照） |
| ★13★ | ★★仕様が不明な箇所を推測で実装すること★★（`CLAUDE.md` §0-1 / §7。★段階の切れ目で止めて質問する★） |
| ★14★ | ★★`section_name` を当方で作る・整形すること★★（DBの値をそのまま出す） |

---

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

```
段階0  前提の確認と実測                    （コミットなし）
  ↓
段階1  d-watch-api.yaml を直す（+ spec.md） （1コミット）★API契約が先★
  ↓
段階2  型 + 純粋関数 + 期待値表テスト        （1コミット）★テストを先に書く★
  ↓
段階3  fixtures 追加 + docs/fixtures.md      （1コミット）
  ↓
段階4  DataSource.areaCompletions()          （1コミット）fixtures版 → prisma版
  ↓
段階5  APIルート（501 を外す）               （1コミット）
  ↓
段階6  画面（AreaModal の配線）              （1コミット）
  ↓
段階7  実測での検証                          （コミットなし）
  ↓
段階8  記録と push                           （1コミット）
```

★★各段階の終わりで `npx tsc --noEmit` / `npm run lint` / `npm test` が**すべて緑**であること★★（D-050）。

---

## ★★実行順 — 指示書Y と 指示書W の後★★

| 相手 | 関係 |
|---|---|
| ★★指示書Y★★ | ★★必ず先に完了させること★★ — ★本書は `jstIsoFromDb()` / `fromDbJst()` に依存します★（落とし穴6）。★また指示書Y の段階4 でローカルDBが再シードされます★。★★本書の実行中にDBが作り直されると段階7 の実測が無意味になります★★ |
| ★★指示書W★★ | ★★先に完了させること★★ — ★`lib/db/index.ts` を両方が触ります★。★★同時に走らせるとコンフリクトします★★ |
| 指示書Y・W が触るファイル | ★`lib/db/` / `lib/format/date.ts` / `lib/config/` / `scripts/seed-local-db.ts` / `docs/decisions.md` / `docs/handoff.md`★ |

★★着手時に `git status` が clean であること、`git log --oneline -5` に指示書Y と 指示書W の
完了コミットがあることを確認してから始めること★★。

---

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

### 0-1. 読むもの

| 資料 | 見るところ |
|---|---|
| ★`CLAUDE.md` 全文★ | ★§4.1 判定 / §4.2 面積ベース / §4.3 丸め / §5 モックが正 / §6-1 API契約が先 / §7 禁止事項 / §8 1エリア=1ロボット / §9.2 命名★ |
| `docs/decisions.md` | ★★D-114 項4（本書の出所）★★ / D-034（確認事項#10 の出所）/ D-106 / D-107 / D-109 / D-113。★★行番号は `grep -n "^### D-1" docs/decisions.md` で実測すること★★ |
| `docs/spec.md` | F-02（★「エリア別完了率グラフ: 501仮実装で先行」の記述がある★） |
| ★`d-watch-mock.jsx`★ | ★★エリアモーダル（おおよそ 1917-1975 行）★★ / ★`getAreaData()`（おおよそ 276-282 行）★ / `areaGrade()`。★★文言・列・並び順・ヘッダの形を**実物から引くこと**★★ |
| `d-watch-api.yaml` | ★`/clean-logs/{id}/areas`（おおよそ 362-400 行）★ / ★冒頭の注記（おおよそ 23-24 行）★ |
| 実装 | `components/features/detail-log/AreaModal.tsx` / `DetailLogScreen.tsx`（`openAreas()`）/ `CleanLogTable.tsx`（「エリア別詳細を見る →」）/ `app/api/clean-logs/[id]/areas/route.ts` / ★`lib/db/index.ts` の `cleanLogs()`★ / ★`lib/db/clean-log-mapping.ts`★ / ★`lib/judge/grade.ts` / `lib/judge/kpi.ts`★ |

### 0-2. ★現状の基準を控える★

```bash
npx tsc --noEmit          # 0 であること
npm run lint              # 0 であること
npm test                  # ファイル数 / テスト数を控える
git log --oneline -5      # 指示書Y・W の完了を確認
git status --short        # clean であること
```

### 0-3. ★★DBに何が入っているかを控える（段階7 の突き合わせに使う）★★

★★`USE_FIXTURES=false` で `/api/clean-logs` を叩き、施設7 の同じ日付の行が3件あることを確認する★★。
★その3件の `id` を控えること★（★段階7 でその id を `/areas` に渡します★）。

```bash
# 例（実際のポート・認証は .env.local と proxy.ts の現状に合わせること）
curl -s 'http://localhost:3000/api/clean-logs?per_page=200' | ...
```

★★DBを直接書き換えないこと★★。★読むだけ★。

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

- [ ] ★上の資料をすべて読んだ★
- [ ] ★`tsc` / `lint` / `test` の基準値を控えた★
- [ ] ★施設7 の3エリアが同じ日にある清掃ログの id を控えた★

---

## 段階1 — ★`d-watch-api.yaml` を直す★（1コミット）

★★`CLAUDE.md` §6-1 / D-040: API契約を変えたくなったら、先に YAML を直す★★。

### 1-1. ★`/clean-logs/{id}/areas` の書き換え★

★★以下は**内容の指定**です。行番号・インデントは現物に合わせること★★。

| 箇所 | やること |
|---|---|
| `summary` | ★「エリア別完了率(施設×日付)」のように、施設×日付であることが分かる文言にする★ |
| ★`description`★ | ★★「確認事項#10 に依存 / 取得不可なら Phase B 送りで 501」を**削除**★★し、代わりに次を書く: ①★`{id}` はエリアを特定するキーではなく、**施設と日付を特定するための入口**であること★ ②★★返すのは「その清掃ログと同じ施設・同じ日の**全エリア**（その清掃ログ自身を含む）」★★ ③★★`completion_rate` は `clean_ratio` の生値、`overall_completion_rate` は面積ベース（`Σclean_area ÷ Σtask_area`）で、**両者は一致しないことがある**★★（決定7） ④★並び順は完了率の昇順・★★未実施(null)は先頭★★・同値は `section_id` 昇順★ ⑤★★`overall_*` と件数は `filter` に関係なく全エリアから計算する★★ ⑥★TeamWorks 第8回 A9-2 が出所であること★ |
| ★`"501"`★ | ★★レスポンス定義ごと**削除**★★ |
| `completion_rate` | ★★`nullable: true` を足す★★（未実施。決定4） |
| レスポンス本体 | ★下の 1-2 の形にする★ |

### 1-2. ★★レスポンスの形（★これを正とする★）★★

```yaml
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [overall_completion_rate, overall_grade, total_count, problem_count, items]
                properties:
                  overall_completion_rate:
                    type: number
                    format: float
                    nullable: true
                    example: 63.2
                    description: |
                      その施設・その日の全エリアを合算した完了率(丸めない生値)。
                      Σclean_area ÷ Σtask_area × 100(面積ベース。CLAUDE.md 4.2)。
                      分母が0(全エリア未実施など)のときは null。
                      ★items[].completion_rate は clean_ratio の生値なので、
                      この値は items の平均とは一致しない★
                  overall_grade:
                    type: string
                    enum: ["◎", "○", "△", "×"]
                    example: "△"
                    description: overall_completion_rate を judgeGrade() にかけた判定(null は ×)
                  total_count:
                    type: integer
                    example: 3
                    description: その施設・その日の全エリア数(filter の前に数える)
                  problem_count:
                    type: integer
                    example: 2
                    description: 判定が △ または × のエリア数(filter の前に数える)
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/AreaCompletionItem" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }
```

★`components/schemas` に `AreaCompletionItem` を新設する★:

```yaml
    AreaCompletionItem:
      type: object
      required: [section_id, section_name, completion_rate, grade]
      properties:
        section_id: { type: integer, example: 107 }
        section_name:
          type: string
          example: "清掃エリア（ロボット107）"
          description: ★sections.section_name の生値。当方で整形しない★
        completion_rate:
          type: number
          format: float
          nullable: true
          example: 42.5
          description: ★clean_status_histories.clean_ratio の生値(丸めない)。★★未実施は null(0 ではない)★★
        grade:
          type: string
          enum: ["◎", "○", "△", "×"]
          example: "△"
```

- ★★`422` は既存の共通レスポンスがあればそれを参照する。無ければ `Error` スキーマで書くこと★★
  （★現物を確認してから決める。★★勝手に新しいエラー形式を作らない★★）
- ★`filter` パラメータの定義（`problem` / `all`、既定 `problem`）は**そのまま残す**★

### 1-3. ★冒頭の注記を直す★

★`## データソースに関する注記` の
「GET /clean-logs/{id}/areas はPudu Cloud APIのエリア別完了率取得可否(確認事項#10)に依存する。
取得不可の場合はPhase B送りとし、501を返す実装で仮置きする。」★を書き換える:

- ★★TeamWorks 第8回 A9-2 で「1エリア(＝1ロボット)につき1つの完了率」が取得可能と確定したこと★★
- ★★データ元は `clean_status_histories`（`clean_task_reports` ではない）★★
- ★★1回の清掃の中の区域別内訳（確認事項#10 の本来の問い）は**依然として未解決**★★

### 1-4. ★`docs/spec.md` の F-02 を直す★

★「エリア別完了率グラフ: **Pudu Cloud API依存（確認事項#10が未解決）。501仮実装で先行**」の行と、
M2 のチェックリスト・「外部待ち」表の該当行を、★★本書の内容に合わせて更新する★★
（★★該当箇所は `grep -n "501" docs/spec.md` / `grep -n "#10" docs/spec.md` で実測すること★★）。

★★`docs/decisions.md` / `docs/handoff.md` / `CLAUDE.md` はこの段階では触らない★★（段階8 でまとめて）。

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

- [ ] ★YAML から `501` の記述が消えている★（`grep -n "501" d-watch-api.yaml` で確認）
- [ ] ★`completion_rate` が `nullable: true`★
- [ ] ★`AreaCompletionItem` スキーマがある★
- [ ] ★YAML が構文として妥当★（★リポジトリに検証手段があれば使う。無ければ目視★）
- [ ] `npx tsc --noEmit` / `npm run lint` / `npm test` が段階0 の基準どおり
- [ ] ★コミット（★ファイルパスを明示★）★: `docs: 指示書Z 段階1 エリア別完了率のAPI契約を501仮実装から本実装に改める`

---

## 段階2 — ★型と純粋関数と期待値表テスト★（1コミット）

★★テストを先に書くこと★★（`CLAUDE.md` §6-2 / §9.4 / D-041）。

### 2-1. ★型を新設する（`lib/types/index.ts`）★

```ts
/** GET /clean-logs/{id}/areas の1件（YAML: AreaCompletionItem） */
export interface AreaCompletionItem {
  section_id: number;
  section_name: string;
  /**
   * ★`clean_status_histories.clean_ratio` の生値（丸めない。CLAUDE.md §4.3）★
   * ★★未実施は `null`。`0`（0%で実施した）と混ぜないこと★★（D-107 落とし穴1）
   * ★★面積から再計算した値では**ない**★★ — `cleanLogs()` の `completion_rate` と
   *   同じ出し方に揃えてある（指示書Z 決定7）。
   *   ★このため `AreaCompletionsResponse.overall_completion_rate`（面積ベース）とは
   *   一致しないことがある。★★意図した非対称★★★
   */
  completion_rate: number | null;
  grade: Grade;
}

/** GET /clean-logs/{id}/areas のレスポンス */
export interface AreaCompletionsResponse {
  /**
   * ★★面積ベース: `Σclean_area ÷ Σtask_area × 100`（CLAUDE.md §4.2）★★
   * ★`clean_ratio` の単純平均ではない★（担当面積の違う台を等しく扱わないため）。
   * ★分母0のときは `null`★。★`filter` に関係なく全エリアから計算する★
   */
  overall_completion_rate: number | null;
  overall_grade: Grade;
  /** ★`filter` の前に数えた全エリア数★ */
  total_count: number;
  /** ★`filter` の前に数えた △× のエリア数★ */
  problem_count: number;
  items: AreaCompletionItem[];
}

/**
 * データ層が返す行（`ReportKpiRow` と同じ流儀）。
 * ★API に出る形ではない★ — 絞り込み・並べ替え・全体率の計算は APIルート側で行う。
 */
export interface AreaCompletionRow {
  /** 本清掃の `clean_timetables.id`（= `CleanLogItem.id`） */
  clean_log_id: number;
  facility_id: number;
  /** ★JST の日付 `YYYY-MM-DD`★（`jstIsoFromDb()` の先頭10文字） */
  date: string;
  section_id: number;
  section_name: string;
  /** ★`completionRateFromHistory()` の戻り（未実施は null）★ */
  completion_rate: number | null;
  /** ★面積ベース全体率の分子。履歴の行が無ければ 0★ */
  clean_area: number;
  /** ★面積ベース全体率の分母。履歴の行が無ければ 0★ */
  task_area: number;
}
```

★★`CleanLogItem` は変更しないこと★★（決定14）。

### 2-2. ★`lib/judge/grade.ts` に `isProblemGrade()` を足す★

★「問題エリア」の定義を1箇所にするため★:

```ts
/** ★「問題あり」の判定＝ △ または ×★（出発の有無を見ない版） */
export function isProblemGrade(grade: Grade): boolean {
  return grade === "△" || grade === "×";
}
```

★★既存の `isProblem()` はこれを呼ぶように書き換える★★（★★挙動は変えないこと★★。
`lib/judge/__tests__/grade.test.ts` が緑のままであること）。

### 2-3. ★純粋関数を新設する（`lib/clean-logs/areas.ts`）★

★`lib/clean-logs/query.ts` と同じ流儀★（★DBアクセスを混ぜない★）:

```ts
export type AreaFilter = "problem" | "all";

/** `filter` の解釈。★既定は "problem"★。不正値は 422 */
export function parseAreaFilter(
  searchParams: URLSearchParams
): QueryParseResult<AreaFilter>;

/** その清掃ログと同じ施設・同じ日の行だけを取り出す（★自分自身を含む★） */
export function selectAreaRowsForLog(
  rows: readonly AreaCompletionRow[],
  target: { facility_id: number; date: string }
): AreaCompletionRow[];

/**
 * 判定・並べ替え・件数・全体率・絞り込みをまとめてレスポンスの形にする。
 *
 * ★★全体率は `completionRate(Σclean_area, Σtask_area)`（`lib/judge/kpi.ts`）★★ —
 *   ★`completion_rate` の平均ではない★。★★items の平均と一致しないのは意図した挙動★★
 *   （指示書Z 決定7）。
 * ★★`total_count` / `problem_count` / `overall_*` は `filter` の**前**に計算する★★。
 * ★並び順: 完了率の昇順。★★未実施(null)は先頭★★。同値は `section_id` の昇順★
 */
export function buildAreaCompletions(
  rows: readonly AreaCompletionRow[],
  filter: AreaFilter
): AreaCompletionsResponse;
```

- ★★`parseAreaFilter()` は `parseCleanLogQuery()` と同じ `fail(422, "validation_failed", ...)` を使うこと★★
- ★★閾値を直書きしないこと★★ — 判定は `judgeGrade()`、問題判定は `isProblemGrade()`

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

#### ★A. `buildAreaCompletions()` の並び順と絞り込み★

★入力（`clean_area` / `task_area` は下のBで使うので、ここでは並び順に影響しない値でよい）★:

| section_id | completion_rate | 期待 grade |
|---|---|---|
| 101 | 95.0 | ◎ |
| 102 | 42.5 | △ |
| 103 | ★null（未実施）★ | ★×★ |
| 104 | 5.0 | × |
| 105 | 70.0 | ★△（「70%**超**」が○なので 70.0 は △）★ |

| 期待 | 値 |
|---|---|
| ★`filter=all` の並び★ | ★★`103(null) → 104(5.0) → 102(42.5) → 105(70.0) → 101(95.0)`★★ |
| ★`filter=problem` の並び★ | ★`103 → 104 → 102 → 105`（★101 が落ちる★）★ |
| `total_count` | ★★どちらの filter でも `5`★★ |
| `problem_count` | ★★どちらの filter でも `4`★★ |

#### ★B. 全体率（★★面積ベースであることが分かるケース★★）★

★★単純平均と面積ベースで答えが変わる2台★★:

| section_id | task_area | clean_area | clean_ratio |
|---|---|---|---|
| 201 | 1000 | 500 | 50.0 |
| 202 | 100 | 100 | 100.0 |

| 出し方 | 値 | 判定 |
|---|---|---|
| ★★面積ベース（正）★★ | `600 ÷ 1100 × 100 = ` ★★`54.5454...`★★ | ★△★ |
| ★単純平均（禁止）★ | `(50 + 100) ÷ 2 = 75.0` | ○ |

★★テストは面積ベースの値を期待値にすること★★（★浮動小数なので `toBeCloseTo` を使う★）。
★★このケースを必ず入れること★★ — ★単純平均で実装しても、面積が同じデータだけでは
テストが緑のまま通ってしまいます★。

#### ★C. 全体率の境界と分母0★

| ケース | Σclean_area | Σtask_area | 期待 `overall_completion_rate` | 期待 `overall_grade` |
|---|---|---|---|---|
| ちょうど90.0 | 900 | 1000 | 90.0 | ★○（90%**超**が◎）★ |
| 90.0 のすぐ上 | 901 | 1000 | 90.1 | ◎ |
| ちょうど70.0 | 700 | 1000 | 70.0 | ★△★ |
| ちょうど10.0 | 100 | 1000 | 10.0 | ★△（10%**未満**が×）★ |
| 0% | 0 | 1000 | 0 | × |
| ★★全エリア未実施★★ | 0 | ★0★ | ★★`null`★★ | ★★`×`★★ |

#### ★D. エリア数★

| ケース | 期待 |
|---|---|
| ★1エリアだけの施設★ | ★★`items` は1件。エラーにしない★★ |
| ★★3エリアの施設（施設7）★★ | ★`items` は3件★ |
| ★該当なし（`filter=problem` で全部 ◎○）★ | ★★`items` は**空配列**（200。404 ではない）★★。`total_count > 0` / `problem_count === 0` |

#### ★E. `selectAreaRowsForLog()`★

| ケース | 期待 |
|---|---|
| 同じ施設・同じ日 | ★含む★ |
| ★その清掃ログ自身★ | ★★含む★★ |
| 同じ施設・違う日 | ★含まない★ |
| 違う施設・同じ日 | ★含まない★ |

#### ★F. `parseAreaFilter()`★

| 入力 | 期待 |
|---|---|
| ★指定なし★ | ★★`"problem"`（既定）★★ |
| `filter=problem` | `"problem"` |
| `filter=all` | `"all"` |
| ★`filter=critical`（不正値）★ | ★★422 `validation_failed`★★ |
| ★`filter=`（空文字）★ | ★★仕様を決めて**テストで固定**すること★★（★`parseCleanLogQuery()` の `result` の扱いに合わせるのが自然★） |

#### ★G. `isProblemGrade()`★

| grade | 期待 |
|---|---|
| ◎ / ○ | `false` |
| △ / × | `true` |

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

- [ ] ★上のA〜Gがすべてテストになっている★（`lib/clean-logs/__tests__/areas.test.ts` 等）
- [ ] ★★Bのケース（面積が違う2台）が入っている★★
- [ ] ★`isProblem()` の既存テストが緑のまま★
- [ ] `npx tsc --noEmit` / `npm run lint` / `npm test` が緑（★テスト数が段階0 より増えている★）
- [ ] ★コミット★: `feat: 指示書Z 段階2 エリア別完了率の型と純粋関数を足す`

---

## 段階3 — ★fixtures を追加する★（1コミット）

### 3-1. ★★`lib/fixtures/` の既存値は1行も変更しないこと★★

★新規ファイル `lib/fixtures/areas.ts` を作り、`lib/fixtures/index.ts` に `export *` を1行足す★。
★★それ以外の fixtures ファイルには触らないこと★★。

### 3-2. ★何を作るか★

★`AreaCompletionRow[]` を作る★。★`lib/fixtures/clean-logs.ts` が公開している `cleanLogs` の
**既存の行**から `clean_log_id` / `facility_id` / `date` を引くこと★
（★★手書きの日付を新たに作らない★★ — `at()` / `dateOnly()` を使い、`cleanLogs` と日付がズレないようにする）。

★★仕込むケース（★境界値を必ず入れる★）★★:

| # | ケース | `completion_rate` | `task_area` / `clean_area` |
|---|---|---|---|
| 1 | ★1エリアだけの施設★ | 任意 | 面積を持つ |
| 2 | ★★3エリアの施設（施設7）★★ | ★3件ぶん★ | ★面積を持つ★ |
| 3 | ★★未実施を含む★★ | ★★`null`★★ | ★`0` / `0`★ |
| 4 | ★★面積が違う2台★★（単純平均と面積ベースで答えが変わる） | 50.0 / 100.0 | `1000/500` と `100/100` |
| 5 | ちょうど 90.0 | 90.0 | 面積も 90.0 と整合させる |
| 6 | ちょうど 70.0 | 70.0 | 同上 |
| 7 | ちょうど 10.0 | 10.0 | 同上 |
| 8 | 0 | 0 | `1000` / `0` |
| 9 | ★該当なし（全部 ◎○ の施設×日）★ | 95.0 等 | 面積を持つ |

- ★★`clean_area = task_area × completion_rate ÷ 100` になるように作る★★
  （★ケース4 を除く★。★ケース4 は「面積が違う」ことが目的なので、行の `clean_ratio` と
  面積が整合していてよい — ★★整合させたうえで全体が単純平均とズレることを確認する★★）
- ★★未実施の行は `completion_rate: null` / `task_area: 0` / `clean_area: 0`★★（決定13）
- ★`section_name` は `清掃エリア（ロボット107）` のような、★★シードと同じ形★★にすること
  （★実在の店舗名・住所を入れないこと。`docs/fixtures.md` §1★）

### 3-3. ★`docs/fixtures.md` に追記する★

★`## 2. 清掃詳細ログ（clean-logs）` の後ろに `### 2.5 エリア別完了率` のような節を足し、
★★上の表と同じケース一覧★★を書くこと★。★★既存の節を書き換えないこと★★。

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

- [ ] ★`lib/fixtures/areas.ts` が新設され、`index.ts` から export されている★
- [ ] ★★既存 fixtures の diff が `index.ts` の1行追加だけ★★（`git diff` で確認）
- [ ] ★`docs/fixtures.md` に節が増えている★
- [ ] `npx tsc --noEmit` / `npm run lint` / `npm test` が緑
- [ ] ★コミット★: `feat: 指示書Z 段階3 エリア別完了率のサンプルデータを足す`

---

## 段階4 — ★データ層（`DataSource.areaCompletions()`）★（1コミット）

### 4-1. ★インターフェースに足す★

```ts
export interface DataSource {
  // ...既存...
  /**
   * エリア別完了率の行（★全期間・全施設★）。
   * ★引数を取らない契約に従う★ — 施設・日付の絞り込みと `filter` はAPIルート側。
   */
  areaCompletions(): Promise<AreaCompletionRow[]>;
}
```

★★`DataSource` は引数を取らない、という既存の契約を崩さないこと★★
（`lib/db/index.ts` のインターフェースのコメント参照）。

### 4-2. ★fixtures 実装★

```ts
async areaCompletions() {
  return areaCompletions;   // lib/fixtures/areas.ts
},
```

### 4-3. ★★prisma 実装（★`cleanLogs()` と同じ作法で書くこと★）★★

★★新しい読み方を発明しないこと★★。`prismaSource.cleanLogs()` を読んで、同じ形で書く:

| # | やること | 使うもの |
|---|---|---|
| ★1★ | `clean_timetables` を `include: { section: true }` で読む | ★`section` にはリレーションがあるので `include` してよい★ |
| ★2★ | `clean_status_histories` を別クエリで読む | ★★FK が無いので `include` で辿れない（手動 join）★★ |
| ★3★ | ★`latestHistoryByTimetableId()` で履歴を1件に絞る★ | ★★`cleanLogs()` と**同じ関数**★★（規則を2箇所に散らさない） |
| ★4★ | ★`precheck === CLEAN_TIMETABLE_PRECHECK.main` の行だけにする★ | ★事前チェック行を混ぜない★ |
| ★5★ | ★★`isFutureSchedule(fromDbJst(row.start_at), now)` を除外★★ | ★`now` は1回だけ作って使い回す★（決定9） |
| ★6★ | ★`date` は `jstIsoFromDb(row.start_at).slice(0, 10)`★ | ★★落とし穴6★★ |
| ★7★ | ★`completion_rate` は `completionRateFromHistory(history)`★ | ★★決定4 / 決定7★★ |
| ★8★ | ★`task_area` / `clean_area` は `history?.task_area ?? 0` / `history?.clean_area ?? 0`★ | ★★決定13（`aggregateCleanStatsByFacility()` と同じ扱い）★★ |
| ★9★ | ★`section_name` は `row.section.section_name` の生値★ | ★整形しない★ |
| ★10★ | ★`BigInt` は `Number()` / `.toString()` で扱う★ | ★既存と同じ★ |

★★`clean_task_reports` を読まないこと★★（決定6）。
★★`robots` を `include` する必要はありません★★（`section_name` があれば足りる）。
★★`cleanLogs()` のようにロボット0台で例外を投げる処理は入れないこと★★ —
★この API はロボットではなくエリアを列挙するので、ロボットの有無を見る必要がない★。

### 4-4. ★docstring に書くこと★

- ★★`completion_rate` は `clean_ratio` の生値であり、面積から再計算していないこと★★
- ★★全体率だけ面積ベースで出すため、**合計が行の平均と一致しない**こと（意図した挙動）★★
- ★データ元が `clean_status_histories` であり、`clean_task_reports` ではない理由★
- ★TeamWorks 第8回 A9-2 が出所であること★

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

- [ ] ★`USE_FIXTURES=true` / `false` の両方で `areaCompletions()` が動く★
- [ ] ★docstring に 4-4 の4点が書いてある★
- [ ] `npx tsc --noEmit` / `npm run lint` / `npm test` が緑
- [ ] ★コミット★: `feat: 指示書Z 段階4 エリア別完了率をデータ層から取れるようにする`

---

## 段階5 — ★APIルート（501 を外す）★（1コミット）

### 5-1. ★`app/api/clean-logs/[id]/areas/route.ts` を書き換える★

```
1. id を整数にパース → 失敗なら 422（★既存のまま★）
2. filter を parseAreaFilter() で解釈 → 不正なら 422
3. db.cleanLogs() で対象ログを引く → 無ければ 404（★既存のまま★）
   ★facility_id と JST日付（scheduled_at.slice(0, 10)）を取り出す★
4. db.areaCompletions() を読む
5. selectAreaRowsForLog(rows, { facility_id, date })
6. buildAreaCompletions(selected, filter) → 200 で返す
```

- ★★`db.cleanLogs()` の呼び出しを残す理由★★: ★404 の判定と、施設・日付の特定★。
  ★`scheduled_at` は既に JST の ISO 文字列なので `.slice(0, 10)` でよい★
  （★★`areaCompletions()` 側の `date` と**同じ作り方**であることを確認すること★★）
- ★エラーレスポンスは既存の `errorBody()` / `errorResponse()` をそのまま使う★
- ★★`try/catch` と `logApiError()` の形を既存に合わせること★★
- ★★501 を返すコードとコメントを完全に消すこと★★（`grep -rn "501" app/ components/` で確認）

### 5-2. ★判定根拠のログ★

★`advice/route.ts` が `logEvent("info", { event: "judge.advice", ..., thresholds_used })` を
出しています★。★★同じ流儀で `event: "judge.areas"` を出すこと★★
（★このルートも1回のクリックで1回しか呼ばれないので件数の問題は無い★）。
★★`overall_completion_rate` の生値と、分子・分母（Σclean_area / Σtask_area）を含めること★★ —
★★あとで「全体率が行の平均と違う」と言われたときに検算できるようにするため★★。

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

- [ ] ★リポジトリ全体から 501 の仮実装が消えている★
- [ ] ★`filter` 未指定で `problem`、`filter=all` で全件★
- [ ] ★存在しない id で 404★ / ★`filter=xxx` で 422★
- [ ] `npx tsc --noEmit` / `npm run lint` / `npm test` が緑
- [ ] ★コミット★: `feat: 指示書Z 段階5 エリア別完了率APIを本実装にする(501を外す)`

---

## 段階6 — ★画面（`AreaModal` の配線）★（1コミット）

### 6-1. ★★モックから引く文言・列・並び順（★これを正とする★）★★

★`d-watch-mock.jsx` のエリアモーダル（おおよそ 1917-1975 行）から★:

| 箇所 | 文言・仕様 |
|---|---|
| タイトル | ★`エリア別清掃完了率`★ |
| ヘッダ2行目 | ★★`{施設名} | {日付} | 全体 {xx}% {判定}`★★（★★`全体` は `overall_completion_rate` を `displayRate()` で丸めた値★★。★★行の完了率ではない★★） |
| フィルタボタン | ★`問題エリアのみ`★ / ★`全件`★（★2つだけ★） |
| 件数行（problem） | ★★`問題エリア（{problem_count} / {total_count}エリア）`★★ |
| 件数行（all） | ★★`全{total_count}エリア（完了率昇順）`★★ |
| 空表示 | ★★`問題エリアはありません（全エリア◎○）`★★ |
| 行の列 | ★★`エリア名（幅120・省略記号）` → `バー（可変幅）` → `{xx}%（右寄せ・幅40）` → `判定バッジ`★★ |
| バーの色 | ★◎○ → `ok` / △ → `wn` / × → `ng`★ |
| 行の強調 | ★★△× の行はエリア名を太字＋濃色にする★★（モックの `fontWeight: 600` / `color: c.tx`） |
| フッタ | ★`閉じる`★ |
| 並び順 | ★★完了率の昇順（★APIが返した順をそのまま使う。フロントで並べ替えない★）★★ |

★★`t()` を通す文言は既存の `AreaModal.tsx` の書き方に合わせること★★（JP/EN の器を壊さない。`CLAUDE.md` §9.6）。

### 6-2. ★未実施エリアの表示★

- ★★完了率は `displayRate(null)` → `-`★★（★`0%` と表示しないこと★）
- ★★バーは 0 幅★★（`ProgressBar` に 0 を渡す）
- ★判定バッジは `×`★

### 6-3. ★配線★

| 場所 | やること |
|---|---|
| `DetailLogScreen.tsx` の `openAreas()` | ★★「失敗側が通常経路」のコメントごと書き換える★★。★`AreaCompletionsResponse` を受け取る★。★取得失敗時はモーダル内にエラー文言を出す（★★モーダルを開かない、にしないこと★★ — ボタンを押して何も起きないのが最悪）★ |
| `AreaModal.tsx` | ★`items` / `overall_*` / 件数 / `loading` / `error` を受け取る★。★フィルタボタンを `onClick` で親に伝える★ |
| フィルタ切替 | ★★`filter` を state に持ち、変わったら**再取得する**★★（サーバ側で絞るため。決定8） |
| `AreaModal.module.css` | ★行・バー・件数行のスタイルを足す★。★★`ProgressBar` / `GradeBadge` を使い、バーやバッジを自作しないこと★★ |

★★`grade` をフロントで再計算しないこと★★（`CLAUDE.md` §9.6）。

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

- [ ] ★モーダルにエリア一覧が出る★
- [ ] ★「問題エリアのみ」「全件」の切替が効く★（★★再取得されていることをネットワークで確認★★）
- [ ] ★未実施エリアが `-` で先頭に出る★
- [ ] ★該当なしのとき `問題エリアはありません（全エリア◎○）` が出る★
- [ ] ★ダーク/ライト、JP/EN が壊れていない★
- [ ] `npx tsc --noEmit` / `npm run lint` / `npm test` が緑
- [ ] ★コミット★: `feat: 指示書Z 段階6 エリア別完了率モーダルを実データに配線する`

---

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

### 7-1. ★★一時スクリプトの作法（★過去に事故が起きています★）★★

★★一時スクリプトには**成功時にも `process.exit(0)` を書くこと**★★。

```ts
main()
  .then(async () => {
    await prisma.$disconnect();
    process.exit(0);      // ★★これが無いと Prisma の接続が残る★★
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });
```

★★`main().catch(...)` だけだと Prisma の接続が残り、実際に**4時間39分ハングした事故**があります★★。

★★一時ファイルをリポジトリに残さないこと★★ — ★過去に `scripts/tmp-measure.ts` が残りました★。
★検証が終わったら削除し、`git status` が clean であることを確認すること★。

### 7-2. ★`USE_FIXTURES=true` での確認★

- [ ] ★段階3 で仕込んだ境界値のケースが画面に出る★
- [ ] ★★面積が違う2台のケースで、全体率が単純平均と違う値になっている★★
- [ ] ★全部 ◎○ の施設×日で `問題エリアはありません（全エリア◎○）` が出る★

### 7-3. ★★`USE_FIXTURES=false`（ローカルDB）での確認★★

- [ ] ★★施設7 の清掃ログから開くと **3エリア**返る★★（★段階0-3 で控えた id を使う★）
- [ ] ★★1台しかない施設から開くと **1件だけ**返る★★（★これが正しい挙動★）
- [ ] ★★モーダルの各行の完了率が、清掃詳細ログ一覧の同じ施設・同じ日の行の完了率と**一致する**★★
      （★★決定6・決定7 が守れているかの決定的な確認★★）
- [ ] ★未実施の清掃がある日で、そのエリアが `-` で先頭に出る★
- [ ] ★★未来の予定の日付が混ざっていない★★（決定9）
- [ ] ★`filter=problem`（既定）と `filter=all` で件数が変わり、★★`overall_completion_rate` は変わらない★★

★★ローカルDBでは `task_area` が全行 1000 の固定値です★★（`scripts/seed-local-db.ts`）。
★★つまり実DBでしか「面積が違う」ケースは出ません — 面積ベースであることの証明は
段階2 のテスト（期待値表B）と段階7-2（fixtures）で行います★★。
★★シードを編集して面積を変えないこと★★（★指示書Y が触ったばかりのファイルです★）。

### 7-4. ★回帰していないこと★

- [ ] ★清掃詳細ログ一覧・ダッシュボード・アラート・レポートが従来どおり★
      （★★`lib/db/index.ts` を触っているので必ず見ること★★）
- [ ] `npm test` / `npx tsc --noEmit` / `npm run lint`

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

- [ ] ★上のチェックがすべて埋まっている★
- [ ] ★★一時スクリプトを削除し、`git status` が clean★★

---

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

### 8-1. `docs/decisions.md` に追記（★★着手直前に実測した空き番号を使うこと★★）

```bash
grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md | tail -3
```

★★`^### D-0` だけでは3桁台を拾えません★★。★見込み番号を書かないこと★。

★書く内容★:

| # | 内容 |
|---|---|
| 1 | ★TeamWorks 第8回 A9-2 の原文と、★★聞いた粒度と違う答えであること★★ |
| 2 | ★`/clean-logs/{id}/areas` の意味を「同じ施設・同じ日の全エリア」に定義し直したこと（決定1）★ |
| 3 | ★★D-034（501 仮実装）を**部分的に上書き**したこと★★。★★ただし確認事項#10 の本来の問い（区域別内訳）は**未解決のまま**であること★★ |
| 4 | ★データ元が `clean_status_histories` で、`clean_task_reports` を使わない理由（決定6）★ |
| 5 | ★★行は `clean_ratio` の生値、全体は面積ベースという非対称（決定7）★★ |
| 6 | ★未実施を `null` にしたこと（決定4）★ |
| 7 | ★`total_count` / `problem_count` を返すことにした理由（決定12）★ |
| 8 | ★却下した案（上の表）★ |
| 9 | ★★申し送り（下の節をそのまま）★★ |

### 8-2. `docs/handoff.md` に追記（★番号は実測すること★）

★`grep -n "^## 20" docs/handoff.md | tail -3` で最新の追記番号を確認してから採番する★。
★段階ごとのコミットと、段階7 の実測結果を表にする★。

### 8-3. `CLAUDE.md` の更新（★該当があれば★）

★§5 の資料表や §4 に「エリア別完了率は 501 仮実装」と書かれた箇所が無いか
`grep -n "501\|エリア別" CLAUDE.md` で確認し、あれば直す★。
★★無ければ触らないこと★★。

### 8-4. push

```bash
git push origin develop
```

---

## ★★申し送り（★★段階8 で `docs/decisions.md` に必ず書くこと★★）★★

| # | 申し送り |
|---|---|
| ★①★ | ★★元の確認事項#10（**1回の清掃の中の区域別内訳**）は依然として未解決★★。★★本実装はそれとは**別のもの**★★ — 実装できたのは「1エリア（＝1ロボット）につき1つの完了率」であり、売り場・通路・レジ前といった内訳ではない。★第9回の確認事項6 として積んである（D-114 項7-6）★ |
| ★②★ | ★★A9-2 の `robot_task_reports` が DDL 上の `clean_task_reports` のことか**未確認**★★（第9回の確認事項5）。★仮にそうだとしても、本実装は `clean_status_histories` を読んでいるので影響しない★ |
| ★③★ | ★★行の完了率は `clean_ratio` の生値、全体は面積ベース（`Σclean_area ÷ Σtask_area`）という**非対称**★★。★★合計が行の平均と一致しないのは**意図した挙動**★★。★将来これを揃えるなら、`cleanLogs()` の `completion_rate` の出し方ごと変える必要がある（画面の一覧の数字が変わる）★ |
| ★④★ | ★★1エリアに複数台が紐づく場合の計算★★（A9-2 の「もし複数台紐づいているのであれば計算で導き出す」）は ★P1 では発生しない★（1清掃エリア=1ロボット。`CLAUDE.md` §8 / D-035）。★★式を `buildAreaCompletions()` の1箇所に閉じ込めてあるので、必要になったら**そこに足すだけで済む**★★ |
| ★⑤★ | ★★未実施行の `task_area` を DBの値のまま分母に足している★★（決定13。レポートの `aggregateCleanStatsByFacility()` と同じ扱い）。★ローカルDBでは 0 なので差が出ないが、★★実DBで未実施行に非0の `task_area` が入ると全体率が下がる★★。TeamWorks に確認する価値がある★ |
| ★⑥★ | ★★`section_name` はローカルDBでは `清掃エリア（ロボット107）` の形★★で、モックの「1F 生鮮エリア」のような名前ではない。★実データが入れば実名になる（当方で整形しない）★ |
| ★⑦★ | ★★`areaCompletions()` は `DataSource` の契約どおり**全期間・全件**を読む★★。★`cleanLogs()` と同じボトルネックを持つ（年間13万行）。期間で絞るなら `DataSource` の契約から見直すこと（D-092 項3-3 と同じ課題）★ |

---

## 完了条件（全体）

- [ ] ★`d-watch-api.yaml` から 501 の記述が消え、`AreaCompletionItem` が定義されている★
- [ ] ★`docs/spec.md` の F-02 が更新されている★
- [ ] ★★期待値表A〜Gがテストになっている（★特にB: 面積が違う2台★）★★
- [ ] ★`lib/fixtures/areas.ts` が新設され、既存 fixtures の値は変わっていない★
- [ ] ★`docs/fixtures.md` にケースが追記されている★
- [ ] ★`DataSource.areaCompletions()` が fixtures / prisma の両方で動く★
- [ ] ★APIが 200 を返し、`filter` がサーバ側で効いている★
- [ ] ★モーダルが実データを表示し、フィルタボタンが飾りでなくなっている★
- [ ] ★★施設7 で3エリア、1台の施設で1件が返ることを実測した★★
- [ ] ★★モーダルの行の完了率が一覧の完了率と一致することを実測した★★
- [ ] ★一時ファイルが残っていない（`git status` が clean）★
- [ ] `npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 緑
- [ ] ★`docs/decisions.md` / `docs/handoff.md` に記録し、push した★

---

## 実行状態

| 段階 | 内容 | 状態 | コミット |
|---|---|---|---|
| 0 | 前提の確認と実測 | ★★完了★★ | — |
| 1 | `d-watch-api.yaml`（+ `docs/spec.md`） | ★★完了★★ | `3bfed11`。★422 は既存の共通レスポンス `ValidationError` を参照（`ValidationFailed` は存在しない）★ |
| 2 | 型 + 純粋関数 + 期待値表テスト | ★★完了★★ | `9edc547`。`lib/clean-logs/areas.ts` 新設 / `isProblemGrade()` 追加 |
| 3 | fixtures + `docs/fixtures.md` | ★★完了★★ | `49b27cf`。`lib/fixtures/areas.ts` 新設（9ケース）。★既存 fixtures は `index.ts` の1行追加のみ★ |
| 4 | `DataSource.areaCompletions()` | ★★完了★★ | `53a6d21`。★`cleanLogs()` と同じ作法（`latestHistoryByTimetableId()` / `isFutureSchedule()` / `jstIsoFromDb()`）★ |
| 5 | APIルート（501 を外す） | ★★完了★★ | `82d98d5`。★`judge.areas` ログのため `lib/logger/events.ts` に `JudgeAreasLogEvent` を追加（allowlist 型のため）★ |
| 6 | 画面（`AreaModal` の配線） | ★★完了★★ | `828afe6`。★`filter` は state に持ち、変わったら再取得★ |
| 7 | 実測での検証 | ★★完了★★ | — ★施設7 で3エリア / 一覧との完了率不一致0件（259行）/ 未来0件 / `cleanLogs()` 259・`prechecks()` 27 不変★。★★ブラウザでの目視は未実施（監督が別途）★★ |
| 8 | 記録と push | ★★記録のみ完了★★ | 本コミット。★D-119 / `docs/handoff.md` 追記39★。★★`git push` は監督の確認後（本作業では実行していない）★★ |
