# 指示書AN — 総清掃面積の画面を作る

**作成: 2026-09-16 / ブランチ: `develop` / 前提: 指示書AM 完了（D-139 §8）**

---

## ★Cursor への指示文（ここから下をそのまま貼ってください）★

> ```
> D-Watch の実装をお願いします。ブランチは develop です。
>
> ■ 最初に読むもの（この順で、全部）
>   1. CLAUDE.md（全文。特に §4.2 KPI計算式 / §4.3 端数処理 / §5 モックが正 /
>      §7 禁止事項 / §9 実装規約）
>   2. docs/handoff/指示書AN_総清掃面積の画面を作る.md（全文）
>   3. components/features/reports/ReportsScreen.tsx（★画面の手本★）
>   4. lib/hooks/use-classification-filter.ts と lib/masters/options.ts
>      （★分類の絞り込みは既にある。作り直さない★）
>   5. lib/db/report-mapping.ts の aggregateCleanStatsByFacility()
>      （★面積のΣを既にやっている。読んでから §3 を読むこと★）
>
> ■ やること
>   指示書AN の 段階1 から 段階7 までを、★番号順に★実行してください。
>   ★段階2（集計の純粋関数）は「期待値表をテストに落としてから実装する」
>     順序を崩さないでください★。
>
> ■ 段階ごとに必ずやること
>   1. npx tsc --noEmit    → 0 であること
>   2. npm run lint        → 0 であること
>   3. npm test            → 全パス
>   4. コミットする（★段階ごとに1コミット★。まとめない）
>
> ■ コミットの決まり
>   ・git add -A は使わないでください。必ずファイルパスを明示してください
>   ・コミットメッセージは日本語。「なぜそうしたか」を1〜2行入れてください
>   ・push はしないでください
>
> ■ 絶対に触らないファイル
>   README.md / docs/code-map.md /
>   docs/handoff/管理画面_設計検討_20260910.md / .claude/worktrees/** /
>   d-watch-mock.jsx / lib/judge/** / lib/db/prisma.ts
>
> ■ 絶対にやらないこと
>   ・生SQL（$queryRaw / $executeRaw / *Unsafe / Prisma.raw）— ESLint が落とします
>   ・メインDBのスキーマ変更・マイグレーション作成
>   ・★単位の文字列を画面やAPIに直書きすること★ — lib/config の AREA_UNIT から読むこと（§2-4）
>   ・閾値・件数のハードコード
>   ・新しい CSS に font-family を書くこと
>   ・仕様が不明なところを推測で実装すること
>
> ■ 動作確認
>   ★テストDB（133.125.238.144）は読み取り専用★。書き込まないでください。
>   ★ローカルDB（dwatch_local）には 2グループ・3顧客・11施設★が入っています。
>   期待される結果は指示書AN の §9 に書いてあります。
>   ★dev server はポート3000で動いています（起動済み）★。
>
> ■ 終わったら
>   ・段階ごとのコミットハッシュ
>   ・tsc / lint / test の結果（テスト本数も）
>   ・★§9 の動作確認を実際に行った結果★
>   ・指示書どおりにできなかったところと、その理由
>   ・判断に迷って自分で決めたところ（あれば全部）
> ```

---

## 0. この指示書の位置づけ

★ユーザーの新要件4件の、最後の1件★です。

| 要件 | 状態 |
|---|---|
| 要レスキュー店舗 | ★完了★（D-135 / D-137） |
| 詳細ログの期間上限2年 + CSV上限 | ★完了★（D-136） |
| 分類の復活 | ★完了★（D-138 / D-140） |
| ★総清掃面積の新画面★ | ★★この指示書★★ |

### 0-1. ユーザー指示（原文）

> ・総清掃面積（指定期間での絞り込みも）
> モニタリングに新項目追加、累計の清掃総面積表示、ホールディングス、各グループ（ブランド）、店舗、指定期間での絞り込み

確認したところ:

> 新画面

★**確定: 左メニューの「モニタリング」に4つ目の画面を追加する。**★

---

## 1. ★先に知っておくこと（調査で分かった事実）★

★どれも実測です。★ ★設計の前提なので、着手前に読むこと。★

### 1-1. 面積はどこにあるか

```
clean_status_histories.clean_area   実際に清掃した面積（分子）
clean_status_histories.task_area    清掃対象の面積（分母）
```

★`CLAUDE.md` §4.2 の「清掃完了率 ＝ Σ清掃面積 ÷ Σ清掃対象面積」の、まさにその2つ★です。

### 1-2. ★★サマリテーブルの面積は使えません★★

`facility_summaries` / `customer_summaries` にも `task_area` / `clean_area` があります。
★**合計になっていません。**★ 実測:

| | |
|---|---|
| `task_area` | ★全24行が `1000` ちょうど★（`task_count = 100` の行でも 1000） |
| `clean_area` | ★8月の月次と9月第2週で、施設ごとに完全に同じ値★ |

★期間が違うのに同じ値★です。★合計ではありません。★

★★TeamWorks に照会中★★（`TeamWorks確認事項_第9回_20260915.md` の Q2-1）。

★**したがって、この画面は常に `clean_status_histories` から集計します。**★

★D-102（「サマリはサマリテーブルを読む。履歴から再計算しない」）との関係★:
★D-102 の対象は KPI 4指標です。★ ★面積は画面に出していなかったので、対象外★と読みます。
★★この判断を記録に残すこと。★★

### 1-3. ★丸め誤差は問題になりません（実測済み）★

| 行数 | `double` の順次加算と厳密値の差 | 整数表示 |
|---|---|---|
| 538（実データ） | ★0★ | 一致 |
| 262,800（2年ぶん） | ★0.000006★ | ★一致★ |
| 1,440,000 | ★0.00004★ | ★一致★ |

★★Kahan 加算も整数化も要りません。★★ ★普通に足してください。★

### 1-4. ★面積を画面に出すのは初めてです★

★`d-watch-api.yaml` に面積のスキーマが1つもありません。★
★YAML の追加から始まります★（`CLAUDE.md` §6-1）。

### 1-5. ★索引が無い★

```
clean_status_histories.started_at   ★索引なし★
```

★期間で絞っても全行を走査します。★ ★第9回 1-4 で依頼中★です。

★★ただし今回は致命的ではありません★★ — ★1本の集計クエリの全走査★であり、
★詳細ログのような「塊ごとに全走査 × 254回」にはなりません★。

---

## 2. ★監督が決めたこと（実装前に読むこと。覆すなら理由を残すこと）★

### 2-1. 画面名

★**「清掃面積」（📐）**★

★既存は「清掃詳細ログ / アラート / レポート」で、名詞2〜5文字・動詞なし★です。★語調を揃えます。★

★★「総清掃面積」「累計清掃面積」にしないこと★★ —
★期間で絞れる画面なので、「総」「累計」を名前に入れると定義を先に固定してしまいます。★

### 2-2. ★「累計」と「指定期間」をどう両立させるか★

```
期間の既定 = 未指定（＝全期間）
期間を指定すると、その範囲の合計になる
```

★★既定が「全期間」なので、開いた瞬間が「累計」です。★★
★期間を入れれば「指定期間の合計」になります。★ ★1つの仕組みで両方を満たします。★

★★注意: データの保持は2年★★（`docs/spec.md`）。★厳密には「累計」ではなく「直近2年の合計」★です。
★★画面にその旨を明記すること★★ — ★書かないと、3年目に数字が減ったときに問い合わせが来ます。★

### 2-3. 期間の単位

★**自由な日付範囲（`date_from` / `date_to`）。週・月の粒度にしない。**★

★理由★: ★サマリを読まないので、週・月の粒度に揃える必要がありません。★
★月跨ぎ週の帰属ルール（D-071）に乗る必要もなくなります★ — ★新しい期間バグを作らずに済みます。★

★上限★: ★**設けない**★（詳細ログと同じ考え方）。★1本の集計クエリなので、期間が広くても行数は増えません★
（★返るのは施設数ぶん★）。

### 2-4. ★単位は「㎡」で表示する（ユーザー確定 2026-09-16）★

ユーザー指示（原文）:

> 単位は㎡でよいです

★**確定: `㎡` を表示する。**★

★★ただし、TeamWorks からの確認は取れていません。★★ ★その事実は消えません。★

- ★DFA側の資料に「㎡」と書かれています★が、★★これはDFAが書いたもの★★です
- ★TeamWorks からの回答は受け取っていません★（第9回 5章で照会中）
- ★実データは `task_area` が全行 `1000` ちょうどで、★★桁から推定することもできません★★★

★★つまりこれは「DFAの判断で ㎡ として表示する」という決定です。★★
★★もし TeamWorks の回答が違えば、全画面が間違った単位を主張することになります。★★
★★記録に、ユーザーの原文と、この risk の両方を残すこと。★★
★第9回 5章の照会は取り下げないこと。★

★★単位の文字列は `lib/config/index.ts` の1箇所に置くこと★★ —
★回答で変わったときに1行で直せるようにするためです。★
★画面やAPIに `"㎡"` を直書きしないこと。★

★見出しは「清掃面積」「清掃対象面積」で、★数値の側に単位を付ける★。

### 2-5. ★集計はSQLに降ろす。ただし番人を置く★

★`groupBy` + `_sum` を使います★（★`lib/db/index.ts` に `controlReport.groupBy` の前例が1本あります★）。

```ts
prisma.cleanStatusHistory.groupBy({
  by: ["facility_id"],
  where: { started_at: {...}, ...(facilityIds !== null ? { facility_id: { in: facilityIds } } : {}) },
  _sum: { clean_area: true, task_area: true },
  _count: { _all: true },
})
```

★返るのは最大でも施設数ぶん（200行程度）★です。★期間の広さに関係しません。★

★★ただし危険が1つあります★★:

```
1つの clean_timetable に clean_status_histories が2行以上あると、
SUM() が同じ清掃の面積を2回数える。
★例外もテスト失敗も出ない。数字が大きくなるだけ。★
```

★実データは全538行が1対1★でしたが、★既存コードは「最新1行に畳んでから足す」実装★になっており、
★N行を想定しています★（`latestHistoryByTimetableId()`）。

★★TeamWorks に照会中★★（第9回 Q2-3）。

★**決定: `groupBy` + `_sum` を使う。そのうえで番人を置く。**★

★番人★: ★集計と同時に「1つの `clean_timetable_id` に履歴が2行以上ある件数」を数え、
0でなければ `logEvent("warn", ...)` を出すこと★。

★★静かに壊れる代わりに、ログに出るようにします。★★
★`lib/logger/events.ts` の既存の作法（`data.anomaly`）に倣うこと。★
★★原因の断定は書かないこと★★（`docs/logging-design.md` §7）。★差分だけを残す。★

### 2-6. ★完了率も出すこと★

★面積だけでは意味が読み取りにくい★です。

```
清掃面積        341,635
清掃対象面積    494,000
完了率          69%
```

★完了率は `lib/judge/kpi.ts` の `completionRate()` を使うこと★。★自前で割り算を書かないこと。★
★分母0で `null` を返します。★ ★`∞` や `NaN` を出さない。★

★★レポート画面のKPIと数字が違う可能性があります★★ — ★経路が違う★ためです
（★レポートは確定期間をサマリから読む★）。★この画面は常に履歴から集計します。★
★★この差を記録に書くこと。★★

### 2-7. 分類の絞り込み

★**作り直さないこと。**★ ★既にあります。★

```
lib/hooks/use-classification-filter.ts   画面側の状態と取得
lib/masters/options.ts                   選択肢の組み立てとカスケード
resolveFacilityIdsByMasters()            group_id / customer_id → 施設IDの配列
```

★★`facility_ids` の `null` と `[]` を区別すること★★（`!== null` で分岐）。
★`?.length` で判定すると「0件ヒット」が「全件」に化けます。★

---

## 3. ★触ってはいけないもの★

| 対象 | 理由 |
|---|---|
| `README.md` / `docs/code-map.md` / `docs/handoff/管理画面_設計検討_20260910.md` / `.claude/worktrees/**` | ★別作業★ |
| `d-watch-mock.jsx` | ★モックは原型として保存する★（§7） |
| ★`lib/judge/**`★ | ★判定ロジックは変えません★ |
| `lib/db/prisma.ts` | ★未コミットの変更が乗っている★ |
| ★テストDB（`133.125.238.144`）への書き込み★ | ★★読み取り専用★★ |

★★禁止★★: `git add -A` / 生SQL / スキーマ変更 / ★単位文字列の直書き★ / 閾値・件数のハードコード /
★新しいCSSへの `font-family`★ / 推測での実装

---

## 4. 段階1 — 設定値

`lib/config/index.ts` に足す。

```ts
/**
 * 面積の表示単位（ユーザー指示 2026-09-16「単位は㎡でよいです」/ 指示書AN）
 *
 * ★★これは DFA の判断であって、TeamWorks の確認ではない★★ —
 *   ★DFA側の資料に「㎡」とあるが、それはDFAが書いたもの★。
 *   ★TeamWorks からの回答は受け取っていない★（第9回 5章で照会中）。
 *   ★実データは task_area が全行 1000 ちょうどで、桁から推定することもできない★。
 *
 * ★★回答が違っていたら、全画面が間違った単位を主張することになる★★。
 *   ★だから1箇所に置いている。直すのはここ1行で済む★。
 *   ★★画面やAPIに "㎡" を直書きしないこと。★★
 */
export const AREA_UNIT = "㎡" as const;

/**
 * 清掃面積の画面で、内訳を何件まで出すか（指示書AN）
 *
 * ★施設数が上限★（施設ごとに最大1行）。★「もっと表示」で全件開く★のは
 *   要レスキュー（D-137）と同じ作法。
 */
export const CLEAN_AREA_INITIAL_VISIBLE = 10;
```

★数値を直書きしないこと。★ ★必ずここから読むこと。★

---

## 5. 段階2 — 集計の純粋関数（★テストが先★）

### 5-1. 置き場所

★新規 `lib/clean-area/`★（`lib/clean-logs/` と同じ階層）。

★★`components/` に判定や集計を書かないこと★★ —
★`vitest.config.ts` の `include` は `lib/**` と `app/**` で、`components/**` は走りません★（D-120 項2）。

### 5-2. 何をする関数か

```
施設ごとの { facility_id, clean_area, task_area, executed_count }
            ＋ 施設マスタ（customer_id / customer_name / group_id / group_name）
　　　　　→ 粒度（group / customer / facility）でロールアップした配列
　　　　　＋ 全体の合計
```

★純粋関数にすること★。★DBアクセスを混ぜない。★

### 5-3. ★期待値表（先にテストへ）★

**A群: 合計そのもの**

| # | 入力 | 期待 |
|---|---|---|
| A-1 | 0件 | `clean_area: 0` / `task_area: 0` / ★`completion_rate: null`★（★0% ではない★） |
| A-2 | `_sum` が `null`（Prisma の0件時） | ★`?? 0` で 0★。★`null` が画面に漏れないこと★ |
| A-3 | `task_area = 0` の行だけ（＝未実施） | 合計0 / ★`completion_rate: null`★ |
| ★A-4★ | ★`clean_area > 0` かつ `task_area = 0`★ | ★★`completion_rate: null`★★（★`∞` / `NaN` にしない★） |
| A-5 | 小数を持つ行の合計 | 厳密値と一致（§1-3。★誤差が出たら実装が壊れた合図★） |

**B群: 組織のロールアップ**

| # | 入力 | 期待 |
|---|---|---|
| B-1 | 1グループ → 1屋号 → 3店舗 | ★グループ合計 ＝ 屋号合計 ＝ 3店舗の和★ |
| B-2 | 1グループ → 2屋号 → 各2店舗 | ★各階層で和が一致★ |
| ★B-3★ | ★`group_id = null` の顧客★ | ★★「グループなし」として1行にまとめる★★。★「グループ0」を作らない★。★★その屋号の面積が全体合計から落ちないこと★★ |
| B-4 | 施設マスタに無い `facility_id` | ★落ちないこと★。★どう扱うかを決めて、理由をコメントに書くこと★ |
| ★B-5★ | ★全体合計★ | ★★粒度を変えても全体合計は変わらないこと★★ |

★★B-5 が要です★★ — ★粒度タブを切り替えて合計が変わったら、ロールアップが壊れています。★

**C群: 並びと表示**

| # | 入力 | 期待 |
|---|---|---|
| C-1 | 面積がばらばら | ★`clean_area` の降順★ |
| C-2 | 同じ面積が2件 | ★IDの昇順★（★安定した順序★） |
| C-3 | 11件・初期表示10件 | ★10件＋「もっと表示（あと1件）」★ |
| C-4 | ★ちょうど10件★ | ★★「もっと表示」を出さない★★ |

★C-3 と C-4 の両方を入れること★（`>` と `>=` の取り違え防止）。

---

## 6. 段階3 — データ取得

### 6-1. `DataSource` に追加

★`fixtureSource` と `prismaSource` の**両方**に実装すること★。
★★fixtures 側を `notImplemented` にしないこと★★。

### 6-2. Prisma 側

★§2-5 のとおり `groupBy` + `_sum`。★

★★`include` を使わないこと★★ — ★親行の数ぶん `IN` が膨らみます★（D-136 で実測済み）。
★施設マスタは別に1回読んで、JSで突き合わせること★（★施設は200件程度★）。

### 6-3. ★★番人（§2-5）★★

```
1つの clean_timetable_id に履歴が2行以上ある件数を数える
0 でなければ logEvent("warn", { event: "data.anomaly", metric: "...", expected: 0, actual: n })
```

★★原因の断定を書かないこと★★。★差分だけを残す★（`docs/logging-design.md` §7）。

★このクエリが重くならないようにすること★ — ★対象期間・対象施設に絞って数えること★。
★全期間・全施設を毎回数え直さない。★

### 6-4. fixtures 側

★`lib/fixtures/` の既存データから、同じ集計を通すこと★（`CLAUDE.md` §9.5）。
★★fixtures 用に別の集計を書かないこと。★★

★面積を持つ fixtures は `lib/fixtures/areas.ts` にあります★。★足りなければ足すこと★
（★足したら、既存のテストが落ちないか確かめる★）。

---

## 7. 段階4 — API契約とルート

### 7-1. ★YAML を先に直すこと★（`CLAUDE.md` §6-1）

### 7-2. 案

```yaml
  /clean-area:
    get:
      tags: [clean-area]
      summary: 清掃面積の集計(ホールディングス/屋号/店舗 × 期間)
      description: |
        指定期間の清掃面積を、指定した粒度で集計して返す。

        ★clean_status_histories の clean_area / task_area の合計★。
        ★サマリテーブルの task_area / clean_area は読まない★ —
        実データで全行 1000 固定であり、期間内の合計になっていないため(指示書AN §1-2。
        TeamWorks 第9回 Q2-1 で照会中)。
        ★group_summaries は存在しない★ため、ホールディングス単位は
        facilities → customers → groups のロールアップで作る。

        ★期間を指定しないときは全期間★。ただしデータの保持は2年なので、
        実際には直近2年の合計になる(docs/spec.md)。

        ★単位は ㎡(ユーザー確定 2026-09-16)。ただし TeamWorks の確認は未取得(第9回 5章で照会中)★。
      parameters:
        - name: date_from
          in: query
          schema: { type: string, format: date }
          description: 清掃開始日時の範囲(from)。★省略時は下限なし★
        - name: date_to
          in: query
          schema: { type: string, format: date }
          description: 清掃開始日時の範囲(to)。★省略時は上限なし★。★日数の上限は設けない★
        - $ref: "#/components/parameters/GroupId"
        - $ref: "#/components/parameters/CustomerId"
        - name: facility_name
          in: query
          schema: { type: string }
          description: 施設名(部分一致)。記号と大文字小文字を無視して照合する(D-129)
        - name: group_by
          in: query
          schema:
            type: string
            enum: [group, customer, facility]
            default: facility
          description: 集計の粒度
      responses:
        "200": ...  # total（全体合計）+ items（内訳）+ meta
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/ValidationError" }
```

★`items` の各行★:

```
scope / id（nullable。★グループなしは null★）/ name /
group_id / group_name / customer_id / customer_name /
clean_area / task_area / completion_rate（nullable）/ executed_count
```

★★`completion_rate` は丸めない生値★★（`CLAUDE.md` §9.3）。

### 7-3. ルート

★手本は `app/api/reports/facility-performance/route.ts`★（★分類の絞り込みが既に入っている★）。

- ★認証はルートに書かない★（`proxy.ts` が横断処理）
- ★`GET` だけ★
- ★`resolveFacilityIdsByMasters()` を使うこと★。★新しい解決関数を作らない★

---

## 8. 段階5 — 画面

### 8-1. メニューに足す位置

`components/layout/AppShell.tsx` の `MENU_MONITORING`:

```
{ href: "/detail-log", ... },
{ href: "/alerts", ... },
{ href: "/reports", ... },
★ここに足す★
// ★デモまで一時的に隠す（D-079 項3）★  ← 点検記録のコメント塊
```

★★点検記録のコメント塊より**前**に置くこと★★ — ★後ろに置くとコメントアウトの続きに見えます。★

### 8-2. 作るファイル

| ファイル | 手本 |
|---|---|
| `app/(dashboard)/clean-area/page.tsx`（3行） | `app/(dashboard)/reports/page.tsx` |
| `components/features/clean-area/CleanAreaScreen.tsx` | `components/features/reports/ReportsScreen.tsx` |
| `components/features/clean-area/CleanAreaScreen.module.css` | 同ディレクトリ |
| `lib/i18n.ts` に語を追加 | — |

### 8-3. 画面の構成

```
┌ ヘッダ ─────────────────────────────┐
│ h1「清掃面積」                            │
├ 絞り込み ────────────────────────────┤
│ [全ホールディングス▼][全屋号▼][施設名__][検索] │ ← ★既存の共有ロジックに乗せる★
│ [開始日___][終了日___]                      │ ← ★未指定なら全期間★
├ 数値カード ──────────────────────────┤
│ 清掃面積 341,635 / 清掃対象面積 494,000 / 完了率 69% │
│ ★「直近2年ぶん」と明記★                      │
├ 内訳 ───────────────────────────────┤
│ [ホールディングス][屋号][店舗] ← 粒度タブ        │
│ 面積の降順。初期10件＋「もっと表示」            │
└────────────────────────────────────┘
```

### 8-4. 作法

- ★数値は3桁区切りにすること★（`341635` は読めません）。
  ★★`lib/format/` に数値整形の関数がありません。新設すること★★
  （★`toLocaleString` / `Intl.NumberFormat` の使用は本番コードに0件★）
- ★★単位は `AREA_UNIT` から出すこと★★（§2-4）。★`"㎡"` を直書きしない★
- ★取得は `apiFetch`★。★`fetch` を画面に直書きしない★
- ★draft と確定を分けること★（★検索ボタンを置く★。`ReportsScreen` と同じ）
- ★色は CSS 変数★。★★新しいCSSに `font-family` を書かない★★
- ★文言は `t()` を通す★。★数値は画面側で差し込む★
- ★「もっと表示」の判定は `lib/` 側の純粋関数に切り出すこと★
  （★`lib/dashboard/rescue-visibility.ts` と同じ形★）

### 8-5. ★モックに対応する画面がありません★

★実測: `d-watch-mock.jsx` に「面積」「㎡」「累計」のヒットは0件★です。

`CLAUDE.md` §5 は「モックが画面レイアウト・文言・遷移の正」と定めています。★前例が3件★:

| 決定 | 何を |
|---|---|
| D-120 | モックに無いページ送りUIを新規作成 |
| D-121 | モックと違う色に |
| ★D-137★ | ★サイドバーのスクロールを独立させた★ |

★**同じ作法を踏襲すること:**★

1. ★`d-watch-mock.jsx` は1文字も書き換えない★
2. `docs/decisions.md` に ★「`CLAUDE.md` §5 の例外。ユーザーの明示指示（2026-09-15）に基づく」★
   と記録し、★指示の原文を引用する★
3. ★`CleanAreaScreen.tsx` の冒頭に理由コメントを残す★
   — ★★書かないと、次にモックと突き合わせた人が「余計なものがある」と判断して消します★★

---

## 9. ★動作確認★

### 9-1. 前提

★dev server はポート3000で動いています。★ ★`.env.local` はテストDBを向いています。★
★★テストDBは読み取り専用。書き込まないこと。★★

★テストDBの実測値★（自分で確かめ直すこと）:

```
clean_status_histories  538行
Σ clean_area            341,635
Σ task_area             494,000
```

### 9-2. 確かめること

```
GET /api/clean-area
GET /api/clean-area?group_by=group
GET /api/clean-area?group_by=customer
GET /api/clean-area?date_from=2026-09-01&date_to=2026-09-13
GET /api/clean-area?group_id=9999
```

| # | 期待 |
|---|---|
| ★9-1★ | ★期間指定なしの合計が Σ341,635 / Σ494,000 と一致すること★ |
| ★9-2★ | ★★粒度を変えても全体合計が変わらないこと★★ |
| 9-3 | 期間で絞ると合計が減ること |
| ★9-4★ | ★`group_id=9999` で0件（★全件に化けない★）★ |
| 9-5 | ★完了率が丸められていないこと★ |
| ★9-6★ | ★単位が `lib/config` の `AREA_UNIT` から出ていること★（★画面やAPIに直書きされていないこと★） |
| 9-7 | ★数値カードに「直近2年ぶん」の注記が出ること★ |

★★自分でSQLを書いて検算すること。★★ ★APIの出力をそのまま信じないこと。★

### 9-3. ★ローカルDBで階層を確かめる★

★ローカルDB（2グループ・3顧客・11施設）★で:

| # | 期待 |
|---|---|
| ★9-8★ | ★★`group_by=group` の合計 ＝ `group_by=customer` の合計 ＝ `group_by=facility` の合計★★ |
| 9-9 | ★グループなしの顧客が「グループなし」として出ること★（★「グループ0」でないこと★） |

★`.env.local` を書き換えないこと★ — ★環境変数を上書きして別に起動するか、
ルートハンドラを直接呼ぶこと。★

### 9-4. 画面

★★ログインが要るので、目視は難しいかもしれません。★★
★できなければ「できなかった」と正直に報告すること。★ ★推測で「動いているはず」と書かないこと。★

---

## 10. 段階6 — 記録

★★採番の前に必ず実測すること★★:

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

★番号順に並んでいません。★

書くこと:

- ★サマリの面積を使わず、常に履歴から集計すると決めた理由★（★実データで合計になっていない★）
- ★D-102 との関係★（★D-102 の対象はKPI 4指標で、面積は対象外と読んだ★）
- ★★レポート画面のKPIと数字が違いうること★★と、その理由（経路が違う）
- ★単位を ㎡ にしたのは★★DFAの判断★★であり、★★TeamWorks の確認ではない★★こと（ユーザー原文を転記。第9回 5章の照会は継続）
- ★`groupBy` + `_sum` を選び、番人を置いた理由★（1:N で静かに多重計上する）
- ★期間の既定を「全期間」にした理由★と、★★実際には直近2年であること★★
- ★画面名を「清掃面積」にした理由★（「総」「累計」を名前に入れない）
- ★モックに無いUIを作ったこと★（`CLAUDE.md` §5 の例外。★ユーザー原文を転記★）

`docs/handoff.md` に追記:

- ★★面積の単位は TeamWorks 未確認のまま ㎡ で表示している★★（第9回 5章）。★回答が違えば `AREA_UNIT` 1行で直す★
- ★サマリの面積が合計かどうか未確認★（第9回 Q2-1）
- ★1タスクに履歴が何行できるか未確認★（第9回 Q2-3）— ★番人がログを出したら、ここを見ること★
- ★`clean_status_histories.started_at` の索引が未着★（第9回 1-4）
- ★レポート画面のKPIと数字が違いうること★

---

## 11. ★実装者へ — 特に踏みやすい落とし穴★

| # | 落とし穴 | どうなるか |
|---|---|---|
| ★1★ | ★サマリテーブルの面積を使う★ | ★全行 1000 固定の嘘の数字が出る★。★例外もテスト失敗も出ない★ |
| ★2★ | ★1タスクに履歴が2行あると多重計上する★ | ★★数字が大きくなるだけ。番人を置くこと★★ |
| ★3★ | ★`facility_ids` を `?.length` で分岐する★ | ★「0件ヒット」が「全件」に化ける★ |
| ★4★ | ★粒度を変えると全体合計が変わる★ | ★ロールアップが壊れている。B-5 でテストすること★ |
| ★5★ | ★`group_id = null` の顧客を落とす★ | ★全体合計と内訳の和が合わなくなる★ |
| ★6★ | ★`"㎡"` を画面やAPIに直書きする★ | ★★TeamWorks の確認が未取得。回答が違ったとき、直す場所が散らばる★★ |
| ★7★ | ★分母0で `∞` や `NaN` を出す★ | ★`completionRate()` を使えば `null` になる★ |
| ★8★ | ★`include` を使う★ | ★親行の数ぶん `IN` が膨らむ★（D-136 の実測） |
| ★9★ | ★3桁区切りにしない★ | ★`341635` は読めない★ |
| ★10★ | ★「直近2年」の注記を出さない★ | ★3年目に数字が減って問い合わせが来る★ |
| ★11★ | ★fixtures 側を `notImplemented` にする★ | ★`USE_FIXTURES=true` で画面が落ちる★ |
| ★12★ | ★判定や集計を `components/` に書く★ | ★テストが走らない★ |
| ★13★ | ★新しいCSSに `font-family` を書く★ | ★漢字が中華字形になる★ |
| ★14★ | ★テストDBに書き込む★ | ★★読み取り専用★★ |

---

## 12. やらないこと（★この指示書の範囲外★）

| 項目 | なぜ |
|---|---|
| ★面積のCSVエクスポート★ | ★ユーザーが求めていない★。要るなら別途 |
| ★週・月の粒度での期間指定★ | ★自由な日付範囲にした★（§2-3）。★月跨ぎ週の帰属に乗る必要が無くなる★ |
| ★サマリテーブルからの集計★ | ★面積が合計になっていない★（第9回 Q2-1 で照会中） |
| ★グループ単位のサマリテーブル★ | ★TeamWorks の領域★（第9回 6章） |
| ★単位が ㎡ であることの TeamWorks への確認★ | ★第9回 5章で照会中。回答待ち★ |
| ★点検記録の絞り込み★ | 画面自体が未実装 |
| ★スマートフォン幅のレイアウト★ | 別課題 |
| ★`started_at` の索引★ | ★TeamWorks に依頼済み★（第9回 1-4） |
