# 指示書W — 連続問題施設（`continuousProblems`）を実DBから作る

| 項目 | 内容 |
|---|---|
| 実行環境 | ★**Cursor 内の Claude が実装します**★（`.claude/commands/d-watch.md` の役割分担表を参照） |
| 実行状態 | ★**完了（D-116。2026-09-11）**★ |
| 実行順 | ★**指示書V（D-110）完了後**★。★指示書M / P / R / S / T / U / V が作った `lib/db/clean-log-mapping.ts` / `lib/db/report-mapping.ts` / `lib/db/index.ts` の上に積む作業★です。★★`prisma/schema.prisma` と `lib/fixtures/` は**1文字も変えません**★★が、★★`d-watch-api.yaml` / `lib/types/index.ts` / `lib/i18n.ts` / 画面（`components/features/reports/ReportsScreen.tsx`）は**変えます**★★。★触るコードが重なる未実行の指示書は着手直前に自分で確認すること★ |
| 対象 | ★**`prismaSource.continuousProblems()`（`lib/db/index.ts:1115-1117` の `notImplemented("continuousProblems")`）の実装1本**★。★★これが片付くと監視側の `DataSource` は全部埋まります★★（`inspections` / `inspectionDetail` は点検記録で後回し。★D-111 項6★） |
| 取得する決定番号 | ★**着手直前に `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-112**（`docs/decisions.md:6706`）なので、本書の実行記録は **`D-113`** の見込み★。★それでも必ず自分で実測してから採番すること★。★★作成時点で **`D-111` と `D-112` が未コミット**（`git diff docs/decisions.md` に出る。合計256行）★★ — ★★`git add docs/decisions.md` は2件ごと巻き込みます★★（段階9-3 参照） |
| 前提 | ★**D-112**（`docs/decisions.md:6706`。★TeamWorks 第7回の回答受領★）★ / ★★**D-111**（`docs/decisions.md:6584`。★summary は事実だけを組み立てる。原因も助言も書かない★。★★ただし同 項2 の決定4「混在は出発失敗を優先する」は**本書で C案に差し替わりました**（後述「ユーザー確定」）★★）★★ / ★**D-026**（`:122`。★「連続」はカレンダー日ではなくスケジュール存在日の連続★）★ / ★**D-102**（`:5139`。★項4 が「`continuousProblems` は対応するサマリが無い。詳細テーブルから算出する必要がある（第6歩）」と明記している★）★ / ★**D-103**（`:5222`。`consecutive_problem_days` はA案 ＝ `computeConsecutiveProblemDays()` を流用する / 規則を2箇所に書かない）★ / ★**D-106**（`:5830`。★1週間分の予定がまとめて登録される＝未来の本清掃行が常時存在する★）★ / ★**D-107**（`:5993`。★`isFutureSchedule()` は必須★）★ / ★**D-109**（`:6328`。★連続日数は施設単位・同一日は OR で1日に畳む・70.0 は問題あり★）★ / ★**D-110**（`:6447`。指示書V の実行記録）★ / **D-042**（`:226`。書き込み許可4カラム）/ **D-050**（`:308`。段階ごとに検証してからコミット）/ **D-056**（`:353`。`schema.prisma` は生成物）/ **D-072 項4**（`:864`。年次は P1 では読まない）/ **D-077**（`:1847`。エラー値を変換しない）/ **D-089 項3・D-104**（★「その他」「不明」という語を使わない★）/ **D-071**（月跨ぎ週は月境界で分割する） |
| 段階数 | ★**10段階**★（段階0〜段階9）。★コミットは段階1 / 2 / 3 / 4 / 5 / 6 / 7 / 9 の**8つ**★ |
| ★着手前確認★ | ★★**なし**★★ — ★判定の分かれ目（summary の文面3種 / 混在ランの書き方（C案）/ △と× を畳むこと / 確定期間も詳細から算出すること / i18n はテンプレート方式 / L1 もデータ層は返すこと / ロボット0台の施設を除外しないこと / 連続0日は行を作らないこと）は ★★すべて確定済み★★（ユーザー確定 2026-09-11 + 監督判断）。★★ユーザーの回答を待たずに段階0 から通しで実行してよい★★ |
| 着手前の基準 | ★**着手直前に自分で `npm test` / `npx tsc --noEmit` / `npm run lint` を実行し、その実測値を基準にすること**★。★作成時点の実測（2026-09-11。★鵜呑みにしない★）: `npm test` = **45ファイル / 928テスト（全パス）** / `tsc` = **0** / `lint` = **0** / `db.cleanLogs()` = **259件** / `db.prechecks()` = **27件**★ |
| 作成 | 2026-09-11（作成時点の決定事項最終番号 **D-112**（`docs/decisions.md:6706`）/ `docs/handoff.md` の最新は **追記33**（`docs/handoff.md:2774`）。★着手時に実測すること★） |

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

> ★**この指示書を読んでいるあなたへ**★ — `.claude/commands/d-watch.md` の「実装は Cursor 側」は
> **2つの環境の役割分担**です。**Cursor の中で動いている Claude は実装担当**です。
> ★調査や改訂ではなく実装を行ってください。★
> ★★**うまくいかなければ段階の切れ目（コミット単位）で止めて構いません**★★ —
> ★段階1（API契約）→ 段階2〜5（純粋関数を1本ずつ）→ 段階6（配線）→ 段階7（画面）★ の順に、
> ★どこで止めても単体で意味が通る形★に並べてあります。
> ★★ただし段階6 まで行かないと画面は 500 のままです★★（＝現状と同じ。壊れはしません）。

---

## ★★冒頭に読むべきこと — 作るのは1本、決まっているのは文面3種★★

`prismaSource.continuousProblems()`（`lib/db/index.ts:1115`）は ★★監視側の `DataSource` で最後に残った
未実装1本★★です。★対応するサマリテーブルが無い★（D-102 項4）ので、
★★確定期間（先週・先月）も当期（今週・今月）も、どちらも詳細テーブルから算出します★★。

★★返すもの★★（`ContinuousProblem`。`lib/types/index.ts:258`）:

| フィールド | 作り方 |
|---|---|
| `facility_id` / `facility_name` | ★`facilities`（`deleted_at: null`）から引く。★**`robots` を join しない**★（後述 決定5）★ |
| `consecutive_problem_days` | ★`computeConsecutiveProblemDays()` + `consecutiveProblemDaysAtPeriodEnd()`★（★既存関数。別実装しない★） |
| `escalation_level` | ★`judgeEscalationLevel()`★（★0日は `null` を返す＝**行を作らない**★） |
| `last_problem_on` | ★期間内でその施設の最終エントリの日付★（★`consecutiveProblemDaysAtPeriodEnd()` の内部から切り出す★） |
| `summary` | ★★下の3種のテンプレート文字列のいずれか★★（★数値を埋めずに `{n}` `{m}` のまま返す★） |
| ★`launch_failed_days`（**新設・任意項目**）★ | ★ランのうち出発失敗（未実施）があった日数★。★summary の `{m}` に画面が差し込む★ |

★★summary の3種（これが仕様のすべて）★★:

| # | 条件 | ★戻り値（テンプレートのまま）★ |
|---|---|---|
| ★1★ | ★ランに出発失敗の日が**1日も無い**★ | ★`"{n}日連続で完了率が△以下"`★ |
| ★2★ | ★ランの**全日**が出発失敗★ | ★`"{n}日連続で出発失敗"`★ |
| ★3★ | ★**混在**（C案）★ | ★`"{n}日連続で△以下（うち{m}日は出発失敗）"`★ |

---

## ★★ユーザーが確定させたこと（2026-09-11）★★

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

### ★確定1 — summary は事実だけを組み立てる（D-111）★

★対処の助言も原因の推定も書かない★。
★fixtures の summary（`lib/fixtures/reports.ts:339-437`。「マップ再作成またはオンサイト訪問を検討」等）は
**再現しません**★ — ★★`USE_FIXTURES` の true / false で文言が違うのは想定内★★（D-111 項5。
前例は `status_1h` の日本語 ↔ `Standby.`（D-098 / D-105））。
★★見た目の差分をもって「実装がおかしい」と判断しないこと★★。

### ★確定2 — 混在ランは ★★C案★★（本日確定。★D-111 項2 の決定4 を差し替える★）★

出発失敗の日と完了率低下の日が混ざった連続では:

```
{n}日連続で△以下（うち{m}日は出発失敗）
```

★★却下した2案と理由（記録に残すこと）★★:

| 案 | 内容 | ★却下理由★ |
|---|---|---|
| ★(a)★ | ★「{n}日連続で出発失敗」（＝D-111 項2 の決定4「出発失敗を優先する」）★ | ★★直近に清掃を実施していても「出発失敗」と書くことになり**事実と食い違う**★★（＝確定1「事実だけを組み立てる」に反する） |
| ★(b)★ | ★「{n}日連続で完了率が△以下」★ | ★★出発失敗を隠してしまう★★ |

★★実データに2件実在します★★: ★施設5（2日中1日が出発失敗）★ / ★施設6（4日中3日が出発失敗）★。
★★つまり「混在は起きない」という前提で書いてはいけません★★。

### ★確定3 — × と △ は書き分けない（★「△以下」に畳む★）★

★1つの連続の中で △ と × が混ざるため★ — ★★実測: 施設9 は5日連続で△のあと、最終日だけ 9.9% で ×★★。
★モックの副題（`d-watch-mock.jsx:713`「2日以上連続で△以下または出発失敗の施設」）とも一致します★。

★★帰結★★: ★`judgeGrade()` は今回使わなくてよくなる可能性が高い★（△と× を分けないため）。
★使うかどうかは実装者判断★。★使う場合も**丸める前の生値**で呼ぶこと★（`CLAUDE.md` §4.3）。

---

## ★★監督が決めたこと（★理由ごと記録に残すこと★）★★

### ★決定1 — API契約を先に直す（`CLAUDE.md` §6 作業ルール1）★

★★現在の `d-watch-api.yaml:1324-1329` は summary を「別物」として定義しています★★:

```yaml
summary:
  type: string
  example: "スケジュール存在日ベースで3日連続。カレンダー上は5日間"
  description: >-
    連続判定の内容を説明する文。完了率の数値ではなく、
    D-026(スケジュール存在日ベースの連続)の説明として画面に出す
```

★これは「D-026 の説明を画面に出す」という定義★で、★★確定1・確定2 のA案／C案とは中身が違います★★。
★★description と example を先に書き換えてから実装に入ること★★（＝段階1）。
★実装だけ変えて YAML と乖離させない★（`CLAUDE.md` §6-1 / §7）。

### ★決定2 — 確定期間（先週・先月）も詳細テーブルから算出する★

- ★★連続問題施設に対応するサマリテーブルは存在しません★★ — ★D-102 項4 自身が
  「対応するサマリが無い / 詳細テーブルから算出する必要がある（第6歩）」と明記している★
- ★★D-102 に反しません★★ — ★D-102 が禁じているのは「サマリがあるのに再計算すること」★
- ★★当期だけにしてはいけない理由★★: ★先週・先月に切り替えたとき、このカードだけ常に「該当なし」になる★。
  ★★同じ画面の施設別パフォーマンス表は確定期間の行を返す★★ので、
  ★同じ期間で「施設別は問題を示しているのに連続問題施設は空」という不整合★になる
- ★★帰結（実装が楽になる点）★★: ★確定期間と当期で処理を分ける必要がありません★。
  ★★`todayJst()` / `isCurrentPeriod()` を使った当期の場合分けを書かないこと★★ —
  ★期間キーはデータ（清掃ログの日付）から列挙するだけ★（段階4）

### ★決定3 — i18n はテンプレート方式（方式A）★

- ★★サーバは「{n}日連続で完了率が△以下」のような**テンプレート文字列のまま**返す★★。
  ★画面側で数値を差し込む★
- ★前例★: `lib/i18n.ts:100`（`"他{n}件を表示 →"`）/ `:163`（`"{n}分前"`）/ `:175`（`"{n}日以上連続"`）。
  ★`lib/i18n.ts:85-99` のコメントが「方式A（プレースホルダ + replace）★推奨★」と明記している★
- ★★完成した日本語文を返すと、EN 切替でこのカードだけ和文が残ります★★
  （`ReportsScreen.tsx:394` は `t()` を通していないので、★そこも直す★）
- ★★`{m}` を使う C案 は replace が2回★★。★プレースホルダ名を分けてあるので `String.replace` で足ります★
  （`String.replace` は文字列指定だと**最初の1件しか置換しない**が、`{n}` も `{m}` も各テンプレートに1回しか出ない）

★★起草時に補った点（★実装者はこの形で作ること★）★★:
★`{m}` に入れる数値の出どころが `ContinuousProblem` に無かった★ため、
★★`launch_failed_days`（任意項目）を API契約と型に足します★★。

- ★★必ず「任意項目」にすること★★ — ★必須にすると `lib/fixtures/reports.ts:339-437` の12件が
  型エラーになり、★★`lib/fixtures/` を変更しない★★（禁止事項）を守れなくなる★
- ★★`prismaSource` は必ず数値を入れる★★（0 を含む）。★任意なのは fixtures を凍結しているためだけ★
- ★画面は `launch_failed_days` が `undefined` のとき `{m}` の置換を行わない★
  （★`?? 0` で 0 を捏造しない★。fixtures の summary にはそもそも `{m}` が無いので置換不要）

### ★決定4 — 連続1日（L1）の行もデータ層は返す★

- ★「2日以上」の絞り込みは ★**`lib/reports/query.ts:236-242` の `filterContinuousProblems()` に実装済み**★
  （閾値は `ESCALATION_THRESHOLDS.level2Days`。★ハードコードされていない★）
- ★★データ層でも絞ると規則が2箇所になる★★（D-103 が禁じた状態）。★片方だけ直したときに食い違う★
- ★★`lib/reports/query.ts` と `app/api/reports/continuous-problems/route.ts` は1文字も変えないこと★★

### ★決定5 — ロボット0台の施設を除外しない★

- ★`facilityPerformance()`（`lib/db/index.ts:883-898`）は ★ロボット0台の施設を行ごと除外している★ が、
  ★★あちらは `FacilityPerformance.serial_number` を出すため★★（`lib/types/index.ts:249`）
- ★★`ContinuousProblem` に `serial_number` は無い★★（`lib/types/index.ts:258-265`）ので ★join 自体が不要★
- ★★除外すると、機体を撤去・交換中の施設のエスカレーションが画面から消えます★★（★fail-silent★）。
  ★連続問題施設は「現場に行くか」を判断するカード★なので、機体が無い施設こそ見えている必要がある
- ★★`prisma.facility.findMany({ where: { deleted_at: null } })` に `include: { robots: ... }` を書かないこと★★

### ★決定6 — 連続0日の施設は行を作らない★

- ★`judgeEscalationLevel()`（`lib/judge/escalation.ts:52-59`）は ★0日で `null` を返す★
- ★`ContinuousProblem.escalation_level` は ★**non-null**★（`EscalationLevel`）
- ★★`as EscalationLevel` でのキャストや `?? "L1"` での握り潰しを禁止★★ —
  ★0日は「問題が続いていない」であって L1（初回）ではない★。
  ★★`null` が返ったら行を作らない（`continue`）★★

---

## ★必ず共有させる既存関数（★別実装したら D-109 の問題が再発します★）★

| 関数 | 場所 | 役割 |
|---|---|---|
| ★`computeConsecutiveProblemDays()`★ | `lib/db/clean-log-mapping.ts:366` | ★行id → 連続日数（施設単位・同一日OR畳み込み済み）★ |
| ★`foldProblemDaysByDate()`★ | `lib/db/clean-log-mapping.ts:322` | ★同一日 OR 畳み込み（D-109 決定2）★ |
| ★`consecutiveProblemDaysAtPeriodEnd()`★ | `lib/db/report-mapping.ts:282` | ★期間内最終エントリの連続日数（A案。D-103）★ |
| `judgeEscalationLevel()` | `lib/judge/escalation.ts:52` | L1〜L3。★0日は `null`★ |
| `latestHistoryByTimetableId()` | `lib/db/clean-log-mapping.ts:214` | 履歴を1件に絞る規則（D-107 落とし穴2） |
| `completionRateFromHistory()` | `lib/db/clean-log-mapping.ts:184` | ★未実施は `null`・丸めない生値★ |
| `isProblemCompletion()` | `lib/db/clean-log-mapping.ts:281` | ★△以下 or 未実施（70.0 は問題あり。D-109 決定3）★ |
| ★`isFutureSchedule()`★ | `lib/db/clean-log-mapping.ts:263` | ★★外すと未来7日ぶんが全部「出発失敗」になり、全施設が常時L3になります★★ |
| `weekOfMonth()` / `periodRange()` | `lib/format/period.ts:126` / `:157` | ★月跨ぎ週の帰属を知っているのは `weekOfMonth()` だけ（D-071）★ |
| `toJstIso()` | `lib/format/date.ts` | ★既存の使い方を1文字も変えない★（タイムゾーンは別件。D-106 項8） |

★`judgeGrade()`★ は ★確定3（△以下に畳む）★ のため今回は不要になる可能性が高い。★実装者判断★。

---

## ★新規に要るもの（★調査で判明。4本の純粋関数★）★

| # | 何 | なぜ既存で足りないか | ★置き場所★ |
|---|---|---|---|
| ★1★ | ★期間内の「最終エントリ」を返す関数★ | ★`consecutiveProblemDaysAtPeriodEnd()`（`lib/db/report-mapping.ts:282-297`）が内部で選んでいるが、★**日数しか返さない**★。`last_problem_on` に日付が要る★ | ★`lib/db/report-mapping.ts`★（★切り出す。★**同関数のシグネチャは変えない**★） |
| ★2★ | ★ランの範囲（日付配列）を返す関数★ | ★`foldProblemDaysByDate()` の戻りから「末尾 n 日」を採る★ | ★`lib/db/report-mapping.ts`（内部で `foldProblemDaysByDate()` を呼ぶ）★ |
| ★3★ | ★ラン内の「出発失敗の日数」を返す関数★ | ★★`foldProblemDaysByDate()` は `{date, isProblem}` しか返さず、未実施 / × / △ の別を落としている★★ | ★同上★ |
| ★4★ | ★期間キーの列挙★ | ★`facilityPerformance()` はサマリ行から期間キーを列挙している（`lib/db/index.ts:906-913`）。★**詳細行から列挙するヘルパは無い**★ | ★`lib/db/report-mapping.ts`★ |
| ★5★ | ★1期間ぶんの `ContinuousProblem[]` を組み立てる関数★ | ★★DB に触らない形にしておかないと「連続0日は行を作らない」等をテストできない★★（禁止事項「テストを DB 依存にしない」） | ★同上★ |

### ★★型を拡張してはいけないもの★★

- ★★`ScheduleDayResult`（`lib/judge/escalation.ts:19-24`）と `foldProblemDaysByDate()` の戻り型を
  拡張しないこと★★ — ★`lib/db/__tests__/clean-log-mapping.test.ts:620-700` が `toEqual` で
  **完全一致比較**しており、フィールドを1つ足すだけで全部落ちます★。
  ★★区別が要るなら、畳み込みの結果とは別に「元の行」を見ればよい★★（新関数2・3 はそうしてある）
- ★★`consecutiveProblemDaysAtPeriodEnd()` のシグネチャを変えないこと★★ —
  ★`lib/db/__tests__/report-mapping.test.ts:400-440` が依存している★。
  ★★中身を新関数1 に置き換えるのは可（引数と戻り値が同じなら既存テストはそのまま通る）★★

---

## ★区別の取り方（材料の作り方）★

| 事象 | ★判定★ |
|---|---|
| ★出発失敗（未実施）★ | ★★`completion_rate === null`★★（`lib/db/clean-log-mapping.ts:176-181` の**不変条件**: `departed === false` ⟺ `completion_rate === null`） |
| ★完了率が△以下★ | ★それ以外で `isProblemCompletion()` が true★（★× と △ を分けない。確定3★） |
| ★同じ日に「未実施の行」と「実施した行」が混在★ | ★★その日は「出発失敗の日」として1日ぶんだけ数える★★（OR。★D-109 決定2 の畳み込みと同じ規則にそろえる★。★同じ日を2回数えない★） |

- ★★丸める前の生値で判定すること★★（`CLAUDE.md` §4.3）。
  ★`Math.round(9.9) = 10` で × が △ に化ける★ — ★今回は△と× を畳むので文面は変わらないが、
  ★★70.0 前後では判定がひっくり返る★★（`isProblemCompletion()` は `<= 70`。70.4 を丸めた 70 と
  70.6 を丸めた 71 で結果が変わる）。★丸めた値を判定に渡さないこと★
- ★★`m ≤ n` が必ず成り立つこと★★（ランの日だけを数えるため）。★ランの外の未実施日を数えない★

---

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

| # | ★してはいけないこと★ | 理由 |
|---|---|---|
| ★1★ | ★★**`git add -A` を使うこと**★★ | `CLAUDE.md` §7。★過去に別作業の途中ファイルを巻き込む事故が起きている★。★★必ずファイルパスを明示すること★★ |
| ★2★ | ★★**`README.md` / `docs/code-map.md` / `docs/handoff/管理画面_設計検討_20260910.md` / `.claude/worktrees/**` に触ること**★★ | ★★**すべて別作業のもの**★★。★**本指示書のコミットに含めない・編集しない・削除しない**★ |
| ★3★ | ★★**メインDBのスキーマ変更（`prisma migrate` / `db push` / `db pull` / `prisma/schema.prisma` の手書き）**★★ | `CLAUDE.md` §7 / D-056。★★本書はスキーマ変更を1つも必要としません★★（★シードにも1行も足しません★） |
| ★4★ | ★★**書き込み許可4カラム（`status` / `comment` / `responded_at` / `resolved_at`）以外への DB 書き込み**★★ | D-042。★★本書は**読むだけ**です。検証でも DB に1行も書きません★★（★指示書V 段階6-2 のような一時 INSERT はしない★） |
| ★5★ | ★★**閾値（90/70/10、猶予30分、L2=2日/L3=3日）をハードコードすること**★★ | `CLAUDE.md` §7。★`lib/config/` から読む★。★★summary の文面に「2日以上」等の閾値を焼き込まないこと★★ |
| ★6★ | ★★**表示用に丸めた値で判定すること**★★ | `CLAUDE.md` §4.3 |
| ★7★ | ★★**`isFutureSchedule()` を外すこと・猶予を足すこと・境界（`>`）を変えること**★★ | D-107 設計4 / D-110。★★外すと未来7日ぶんが全部「出発失敗」になり、**全施設が常時L3**になります★★ |
| ★8★ | ★★**`ScheduleDayResult` / `foldProblemDaysByDate()` の戻り型を変えること**★★ | ★`lib/db/__tests__/clean-log-mapping.test.ts:620-700` が `toEqual` で完全一致比較している★ |
| ★9★ | ★★**`consecutiveProblemDaysAtPeriodEnd()` のシグネチャを変えること**★★ | ★`lib/db/__tests__/report-mapping.test.ts:400-440` が依存している★ |
| ★10★ | ★★**`lib/fixtures/` を1行でも変更すること**★★ | `CLAUDE.md` §9.5 / D-111 項5。★fixtures の summary は人間が書いた文章で、実装では再現しない★ |
| ★11★ | ★**`lib/reports/query.ts` / `app/api/reports/continuous-problems/route.ts` を変更すること**★ | ★★「2日以上」の絞り込み・並べ替え・`meta` は既に実装済みで正しい★★（決定4）。★データ層でも絞ると規則が2箇所になる（D-103）★ |
| ★12★ | ★**`lib/judge/` を変更すること**★ | ★`countConsecutiveProblemDays()` / `judgeEscalationLevel()` / `judgeGrade()` / `lib/judge/advice.ts` は完成している★ |
| ★13★ | ★**`prismaSource` の他メソッド（`cleanLogs` / `prechecks` / `reportKpis` / `facilityPerformance` / `errorBreakdown` / D-Sec 系）の**挙動**を変えること**★ | ★範囲外★。★★段階2 のリファクタは「内部の切り出し」だけで、`facilityPerformance()` の値が1つでも変わったら実装が違います★★（段階8 で突き合わせる） |
| ★14★ | ★**エラー種別を7分類に変換すること・「その他」「不明」という語を使うこと**★ | D-077 / D-089 項3 / D-104 項2。★★summary に原因（オフライン / ブラシエラー等）を書かないこと★★（確定1） |
| ★15★ | ★**summary に対処の助言を書くこと**★ | D-111 項2 の決定2・決定3。★★「現場へ連絡」「オンサイト訪問を検討」は書かない★★（アドバイス R1〜R4 と二重になる） |
| ★16★ | ★**summary を「完成した日本語文」で返すこと**★ | 決定3。★★EN 切替でこのカードだけ和文が残ります★★ |
| ★17★ | ★**テストを DB 依存にすること**★ | ★遅く・不安定になり、DB が空の環境で落ちる★。★テストは純粋関数と手書きの入力だけ★ |
| ★18★ | ★**`.env.local` をコミットすること / DB の認証情報を文書やコードに書くこと**★ | `CLAUDE.md` §9.3。★検証後は `USE_FIXTURES=true` に戻す★ |
| ★19★ | ★★**DBに触る一時スクリプトに `process.exit(0)` を書かないこと**★★ | ★★D-111 項9。`main().catch(...)` だけだと Prisma の接続が残り、★実際に4時間39分ハングした事故★が起きている★★ |
| ★20★ | ★**`npm run dev` を Bash で起動すること**★ | ★Browser pane の `preview_start` を使う★。★無ければ `next build && next start -p <別ポート>` か curl で代替し、その旨を記録する★（★`dwatch_local` と `.env.local` は複数セッションで共有。D-108 項8★） |

---

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

★段階0（前提の確認。★読むだけ・コミットなし★）
→ 段階1（★API契約★: `d-watch-api.yaml` → `lib/types/index.ts`）
→ 段階2（純粋関数①: 期間内最終エントリの切り出し）
→ 段階3（純粋関数②③④: ランの範囲・出発失敗日数・summary テンプレート）
→ 段階4（純粋関数⑤: 期間キーの列挙）
→ 段階5（純粋関数⑥: 1期間ぶんの `ContinuousProblem[]` 組み立て）
→ 段階6（★配線★: `prismaSource.continuousProblems()`）
→ 段階7（画面 + i18n）
→ 段階8（実測での検証。★コミットなし★）
→ 段階9（記録と push）★

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

> ★★**期待値表 → テスト → 実装 の順序を崩さないこと**★★（`CLAUDE.md` §6-2 / §9.4）。
> ★★各段階の `X-1`（期待値表）を先に書き、`X-2`（テスト）で固定し、**落ちることを確認してから** `X-3`（実装）★★。
> ★特に段階3 は、実装を先に書くと「自分の実装がそのまま期待値になる」ため、表を先に埋めること★。

> ★★**なぜこの順番なのか**★★
> - ★段階1 が先頭なのは `CLAUDE.md` §6 作業ルール1「API契約を変えたくなったら先に YAML を直す」★
> - ★段階2〜5 で純粋関数を1本ずつ積むのは、★★段階6 で配線したときに「どの関数が間違っていたか」を
>   テストで切り分けられるようにするため★★。★段階6 は DB を読むのでテストが書けない★
> - ★段階7（画面）を段階6 の後に置くのは、★★実データが出るようになってから見た目を直したいため★★
> - ★段階8（実測）を最後に置くのは、★★期待値（4件・0件・施設5=2日 等）が全段階の積み上げでしか出ないため★★

---

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

### 読むもの

| # | 対象 | 見る点 |
|---|---|---|
| 1 | ★`CLAUDE.md` 全文★ | ★★§4.3（丸め）/ §4.5（R1〜R4・L1〜L3）/ §6（API契約が先・期待値表が先）/ §7（禁止事項）/ §9.4（判定ロジックの作法）★★ |
| 2 | ★`docs/decisions.md` の **D-111**（`:6584`）★ | ★★summary の生成規則（A案）★★。★★ただし 項2 の決定4「混在は出発失敗を優先」は本書の確定2（C案）で差し替わる★★。★項9（一時スクリプトの `process.exit(0)`）も読む★ |
| 3 | ★**D-026**（`:122`）/ **D-102**（`:5139`。★項4★）/ **D-103**（`:5222`）/ **D-106**（`:5830`）/ **D-107**（`:5993`）/ ★**D-109**（`:6328`）★ / **D-110**（`:6447`） | ★スケジュール存在日の連続 / サマリが無いこと / A案 / 1週間分の予定 / `isFutureSchedule()` / 施設単位と同一日OR / 指示書V の実行記録★。★行番号は自分で取り直すこと★ |
| 4 | ★`docs/handoff.md` の末尾（★追記33★。`:2774`）★ | ★指示書V の完了内容と申し送り★。★「次にやること」1番が本書★ |
| 5 | ★`docs/handoff/指示書V_連続日数を施設単位に揃え当期の分母から未来を外す.md`★ | ★段階の切り方・期待値表の書き方・記録の作法★ |
| 6 | ★`lib/db/clean-log-mapping.ts` 全文（392行）★ | ★`isFutureSchedule()`（`:263`）/ `isProblemCompletion()`（`:281`）/ `ConsecutiveInput`（`:286`）/ `foldProblemDaysByDate()`（`:322`）/ `computeConsecutiveProblemDays()`（`:366`）★ |
| 7 | ★`lib/db/report-mapping.ts` 全文（389行）★ | ★`FacilityConsecutiveEntry`（`:261`）/ `consecutiveProblemDaysAtPeriodEnd()`（`:282`）/ `periodMapKey()`（`:31`）★ |
| 8 | ★`lib/db/index.ts:874-1117`★ | ★`facilityPerformance()` の組み立て方（★これが本書のひな形★）★ / ★`continuousProblems()`（`:1115`）★ / ★施設マスタの引き方（`:883-898`。★本書は robots を join しない★）★ |
| 9 | ★`lib/judge/escalation.ts` 全文★ | ★★`countConsecutiveProblemDays()` が**日付を見ていない**こと★★ / ★`judgeEscalationLevel()` が0日で `null` を返すこと★。★★どちらも変更しない★★ |
| 10 | ★`lib/reports/query.ts:232-254` / `:303-308`★ | ★「2日以上」の絞り込みと並べ替えが**既に実装済み**であること★。★★変更しない★★ |
| 11 | ★`app/api/reports/continuous-problems/route.ts` 全文★ | ★`meta.total` の意味（2日以上の絞り込み後・limit 適用前）★。★★変更しない★★ |
| 12 | ★`components/features/reports/ReportsScreen.tsx:358-418` / `ReportsScreen.module.css:251-257`★ | ★カードの描画（`:394` が `t()` を通していない）★ / ★`.contSummary` に省略記号の指定が無いこと（申し送り②）★ |
| 13 | ★`lib/i18n.ts:85-101` / `:160-176`★ | ★方式A の前例と書き方★ |
| 14 | ★`lib/db/__tests__/clean-log-mapping.test.ts:600-700` / `lib/db/__tests__/report-mapping.test.ts:380-450`★ | ★既存テストの書き方（ヘルパー）に揃えるため★ / ★★変えてはいけない `toEqual` 比較の場所を自分の目で確認する★★ |

### 実測して控えるもの（★段階8 の突き合わせに使う★）

```
npx tsc --noEmit
npm run lint
npm test
```

★作成時点の実測（2026-09-11。★鵜呑みにしない★）★: `tsc` = 0 / `lint` = 0 / `npm test` = ★45ファイル・928テスト★

```
USE_FIXTURES=false DATABASE_URL="mysql://root@localhost:3306/dwatch_local" npx tsx -e "
import { db } from './lib/db';
const logs = await db.cleanLogs();
const pre = await db.prechecks();
console.log('cleanLogs', logs.length, 'prechecks', pre.length);
const fp = await db.facilityPerformance();
console.log(JSON.stringify(fp.map(r => [r.period, r.year, r.month, r.week,
  r.item.facility_id, r.item.consecutive_problem_days, r.item.status])));
process.exit(0);            // ★★必ず書くこと（D-111 項9）★★
"
```

★作成時点の実測★: `cleanLogs` = ★259件★ / `prechecks` = ★27件★。
★★`facilityPerformance()` の出力は**丸ごとファイルに控えておくこと**★★
（★段階2 のリファクタで1つでも変わったら実装が違う★）。

- ★`.env.local` の `DATABASE_URL` が `dwatch_local` を指していることを確認する★。
  ★**値そのものを画面・ログ・文書に出さないこと**★（`CLAUDE.md` §9.3）
- ★`.env.local` に `ALLOW_DIRECT_ACCESS=true` があることを確認する★（D-101 項2）
- ★★決定番号を実測する★★（`grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md | tail -3`）。
  ★最終が `D-112` なら本書の記録は `D-113`★

---

## 段階1 — ★API契約と型★（1コミット）

### 1-1. `d-watch-api.yaml` の `ContinuousProblem`（`:1300-1329`）を直す

★★`summary` の `description` と `example` を差し替え、`launch_failed_days` を足す★★。
★他のフィールド（`facility_id` / `facility_name` / `consecutive_problem_days` / `escalation_level` /
`last_problem_on`）と、スキーマ冒頭の `description`（「L1(初回)は返さない(2日以上のみ)」）は
**変えないこと**★ — ★★API の応答としては今も L1 を返さない（絞り込みはルートが行う）ので、記述は正しい★★。

★書き換え後の形（★このとおりに★。既存の記法・インデントに合わせる）★:

```yaml
        summary:
          type: string
          example: "{n}日連続で△以下（うち{m}日は出発失敗）"
          description: >-
            連続の内容を事実だけで述べたテンプレート文字列(D-111)。原因の推定も対処の助言も含まない。
            画面が {n} に consecutive_problem_days を、{m} に launch_failed_days を差し込む(方式A)。
            サーバは数値を埋めずにテンプレートのまま返す(完成した日本語文を返すと EN 切替で和文が残るため)。
            返す文字列は次の3種のいずれか:
            出発失敗の日が無いとき「{n}日連続で完了率が△以下」/
            全日が出発失敗のとき「{n}日連続で出発失敗」/
            混在するとき「{n}日連続で△以下（うち{m}日は出発失敗）」。
            △ と × は書き分けず「△以下」に畳む(1つの連続の中で混ざるため。モック:713 の副題と同じ粒度)
        launch_failed_days:
          type: integer
          example: 3
          description: >-
            連続の中で出発失敗(未実施)があった日の数。0 <= launch_failed_days <= consecutive_problem_days。
            summary の {m} に差し込む。同じ日に未実施の行と実施済みの行が混在する場合は、その日を
            出発失敗の日として1日だけ数える(D-109決定2 の同一日OR畳み込みと同じ規則)。
            ★任意項目★: fixtures(手書きの完成文)を変更しないための措置であり、
            実DB経由(prismaSource)では常に数値が入る
```

### 1-2. `lib/types/index.ts:258-265` の `ContinuousProblem` に足す

```ts
  /**
   * 連続の中で出発失敗（未実施）があった日の数（summary の {m}）。
   * ★任意項目にしてあるのは `lib/fixtures/reports.ts` の手書き12件を変更しないため★
   *   （`CLAUDE.md` §9.5 / D-111 項5）。★`prismaSource` は常に数値を入れる（0 を含む）★。
   */
  launch_failed_days?: number;
```

★summary のコメントも「テンプレート文字列である」ことが分かるように直すこと★
（★型を読んだ人が完成文を入れないように★）。

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

- ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス（★件数は段階0 と同じ★）★
- ★★`lib/fixtures/` に差分が無いこと★★（＝任意項目にできている）
- ★`git diff --stat` が `d-watch-api.yaml` と `lib/types/index.ts` の2ファイルだけ★
- ★★コミット（ファイルパスを明示）★★
  - 例: `git commit -m "docs/feat: 指示書W 段階1 ContinuousProblem の summary 契約を事実ベースに直す"`

---

## 段階2 — ★純粋関数①: 期間内の最終エントリを切り出す★（1コミット）

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

★対象★: `lastEntryInPeriod(entries, facilityId, range)` → `FacilityConsecutiveEntry | undefined`

| # | 入力 | ★期待★ |
|---|---|---|
| ★1★ | `entries` = `[]` | ★`undefined`★ |
| ★2★ | その施設の行が1件も無い（別施設の行だけ） | ★`undefined`★ |
| ★3★ | その施設の行はあるが全部範囲外（`from` の前日 / `to` の翌日） | ★`undefined`★ |
| ★4★ | 範囲内に3件（日付バラバラ） | ★`scheduled_at` が最大の1件★ |
| ★5★ | ★同じ日に2件（時刻違い）★ | ★★時刻が遅いほう★★（`scheduled_at` の文字列比較。既存実装と同じ） |
| ★6★ | ★境界: `from` ちょうどの行だけ★ | ★その行（範囲に含む）★ |
| ★7★ | ★境界: `to` ちょうどの行だけ★ | ★その行（範囲に含む）★ |
| ★8★ | 範囲内と範囲外が混在（範囲外のほうが新しい） | ★★範囲内の最新★★（範囲外を拾わない） |

★★加えて、`consecutiveProblemDaysAtPeriodEnd()` の既存テスト
（`lib/db/__tests__/report-mapping.test.ts:400-440`）が**1文字も変えずに通ること**★★。

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

- ★`lib/db/__tests__/report-mapping.test.ts` に追記する★（新規ファイルを作らない）
- ★★実装より先に書き、落ちることを確認する★★

### 2-3. 実装

- ★`lib/db/report-mapping.ts` の `consecutiveProblemDaysAtPeriodEnd()`（`:282-297`）の
  ★**ループ部分をそのまま** `lastEntryInPeriod()` に切り出す★
- ★`consecutiveProblemDaysAtPeriodEnd()` は新関数を呼ぶだけにする★:

```ts
export function consecutiveProblemDaysAtPeriodEnd(
  entries: readonly FacilityConsecutiveEntry[],
  consecutiveById: ReadonlyMap<number, number>,
  facilityId: number,
  range: { from: string; to: string }
): number {
  const latest = lastEntryInPeriod(entries, facilityId, range);
  if (!latest) return 0;
  return consecutiveById.get(latest.id) ?? 0;
}
```

- ★★選び方の規則を2箇所に書かないこと★★（D-103）— ★★`continuousProblems()` 側で
  「最終エントリを自分で探す」コードを書かない★★。必ずこの関数を呼ぶ
- ★docstring に「`last_problem_on` の算出にも使う」ことを書く★

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

- ★`tsc` = 0 / `lint` = 0 / `npm test` 全パス（★テスト数は増える★）★
- ★★`consecutiveProblemDaysAtPeriodEnd()` のシグネチャが変わっていない★★
- ★`git diff --stat` が `lib/db/report-mapping.ts` と `lib/db/__tests__/report-mapping.test.ts` の2ファイル★
- ★★コミット★★ — 例: `git commit -m "refactor: 指示書W 段階2 期間内の最終エントリを切り出す"`

---

## 段階3 — ★純粋関数②③④: ランの範囲・出発失敗日数・summary★（1コミット）

### 3-0. ★★この段階の肝 — ランは期間をまたぐ★★

★★`problemRunDates()` には「その施設の**全期間**の行」を渡すこと★★。
★期間内の行だけで畳むと、★★前月から続いている連続でランが途中で切れ、`{m}` が過小になります★★★
（`consecutive_problem_days` は全期間走査で出しているので、★日数だけ 4 なのにランの日付が 2 件★という
食い違いになる）。★★期待値表 (a)-9 でこれを固定します★★。

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

#### (a) ★`problemRunDates(facilityRows, endDate, consecutiveDays)` → `string[]`★

★入力の `facilityRows` は ★**その施設ぶんの `ConsecutiveInput[]`（全期間）**★。
戻りは ★日付昇順の `YYYY-MM-DD` 配列★。

| # | 入力 | ★期待★ |
|---|---|---|
| ★1★ | `facilityRows` = `[]` / `consecutiveDays` = 0 | ★`[]`★ |
| ★2★ | `consecutiveDays` = 0（最終日が問題なし） | ★`[]`★ |
| ★3★ | 1日だけ・問題あり / `n=1` | ★`["2026-09-10"]`★ |
| ★4★ | 09-08 / 09-09 / 09-10 が全部問題 / `endDate="2026-09-10"` / `n=3` | ★`["2026-09-08","2026-09-09","2026-09-10"]`★ |
| ★5★ | ★スケジュール非存在日を挟む★（09-01 / 09-03 / 09-05 の3日だけ行があり全部問題）/ `n=3` | ★★`["2026-09-01","2026-09-03","2026-09-05"]`★★（★カレンダー上は5日。D-026★） |
| ★6★ | ★同一日に2行（1台が問題・1台が正常）★ | ★★その日は1日ぶんだけ★★（OR。D-109 決定2） |
| ★7★ | ランの前に問題なしの日がある（09-01 正常 / 09-02・09-03 問題）/ `n=2` | ★`["2026-09-02","2026-09-03"]`★（★正常日を含めない★） |
| ★8★ | `endDate` が行に存在しない | ★`[]`★（★呼び出し側が同じ行から取った日付を渡す契約。例外にしない★） |
| ★9★ | ★★`n` が行の日数より大きい★★（行は2日ぶんしか無いのに `n=4`） | ★★`slice` が負にならず、先頭から `endDate` まで（2件）★★（`Math.max(0, ...)`） |
| ★10★ | `endDate` より後の日にも行がある | ★★`endDate` より後を含めない★★ |

#### (b) ★`launchFailedDaysInRun(facilityRows, runDates)` → `number`★

| # | 入力 | ★期待★ |
|---|---|---|
| ★1★ | `runDates` = `[]` | ★`0`★ |
| ★2★ | ラン3日・全日 `completion_rate === null` | ★`3`★ |
| ★3★ | ラン3日・`null` が1日も無い（40 / 55 / 62） | ★`0`★ |
| ★4★ | ★混在（3日中1日が `null`）★ | ★`1`★ |
| ★5★ | ★★同じ日に「`null` の行」と「完了率60の行」★★ | ★★`1`（その日を1日として数える。2回数えない）★★ |
| ★6★ | ランの外の日に `null` がある | ★★数えない★★ |
| ★7★ | ★完了率ちょうど 70.0 の日★ | ★★`0`（問題日ではあるが出発失敗ではない）★★ |
| ★8★ | ラン2日・両方 `null`（＝ `m === n`） | ★`2`★ |

#### (c) ★`continuousProblemSummary(consecutiveDays, launchFailedDays)` → `string`★

★★戻り値は「数値を埋めていないテンプレート」★★（`{n}` / `{m}` の文字がそのまま残る）。

| # | `n` | `m` | ★戻り★ |
|---|---|---|---|
| ★1★ | 1 | 0 | ★`"{n}日連続で完了率が△以下"`★ |
| ★2★ | 1 | 1 | ★`"{n}日連続で出発失敗"`★ |
| ★3★ | 2 | 0 | ★`"{n}日連続で完了率が△以下"`★ |
| ★4★ | ★2★ | ★1★ | ★★`"{n}日連続で△以下（うち{m}日は出発失敗）"`★★（★実データの施設5★） |
| ★5★ | 2 | 2 | ★`"{n}日連続で出発失敗"`★ |
| ★6★ | ★4★ | ★3★ | ★★`"{n}日連続で△以下（うち{m}日は出発失敗）"`★★（★実データの施設6★） |
| ★7★ | 6 | 0 | ★`"{n}日連続で完了率が△以下"`★（★実データの施設9★） |
| ★8★ | 4 | 4 | ★`"{n}日連続で出発失敗"`★ |

- ★★「完了率が△以下」と「△以下」の書き分けは意図的★★ — ★混在の文には出発失敗の日が
  含まれるので「完了率が」を付けると事実と食い違う★。★勝手にそろえないこと★
- ★★文字列は `lib/i18n.ts` に足すキーと**1文字も違ってはいけない**★★（全角括弧・「うち」・「は」を含む）。
  ★★違うと `translate()` が未登録キーとして日本語のまま返す（fail-silent。D-111 項8）★★
- ★前提: `n >= 1` かつ `0 <= m <= n`★（★`n <= 0` では呼ばれない。呼び出し側が行を作らない★）

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

- ★`lib/db/__tests__/report-mapping.test.ts` に追記★。★実装より先に書き、落ちることを確認する★

### 3-3. 実装

- ★★3本とも `lib/db/report-mapping.ts` に置く★★（★`lib/db/clean-log-mapping.ts` は触らない★ —
  戻り型を変えないため。禁止事項8）
- ★`problemRunDates()` は内部で `foldProblemDaysByDate()` を呼ぶ★（★独自に遡って数えない★。
  ★★数え方の規則が2箇所になると D-109 の問題が再発する★★）
- ★テンプレート文字列は ★**モジュール先頭の定数3本**★ にして、テストからも参照できるように export する★
  （★文字列リテラルを2箇所に書かない★）
- ★★閾値は登場しません★★（`n` と `m` の大小比較だけ）。★「2日以上」等をここに書かないこと★

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

- ★`tsc` = 0 / `lint` = 0 / `npm test` 全パス★
- ★★(a) 10ケース・(b) 8ケース・(c) 8ケースがすべてテストになっている★★
- ★`git diff --stat` が `lib/db/report-mapping.ts` と `lib/db/__tests__/report-mapping.test.ts` の2ファイル★
- ★★コミット★★ — 例: `git commit -m "feat: 指示書W 段階3 連続の内訳とsummaryテンプレートを作る"`

---

## 段階4 — ★純粋関数⑤: 期間キーの列挙★（1コミット）

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

★対象★: `reportPeriodKeysFromDates(dates)` → `ReportPeriodKey[]`
（★入力は `YYYY-MM-DD` の並び。重複していてよい★）

| # | 入力 | ★期待★ |
|---|---|---|
| ★1★ | `[]` | ★`[]`★ |
| ★2★ | `["2026-09-10"]` | ★月次 `{period:"monthly", year:2026, month:9, week:0}` と 週次 `{period:"weekly", year:2026, month:9, week: weekOfMonth("2026-09-10").week}` の2件★（★★週番号を手計算で書かず `weekOfMonth()` を呼んで期待値にすること★★） |
| ★3★ | 同じ週の2日 | ★2件（重複しない）★ |
| ★4★ | 同じ月・別の週の2日 | ★月次1件 + 週次2件 = 3件★ |
| ★5★ | ★★月跨ぎ（`["2026-08-31","2026-09-01"]`）★★ | ★★月次2件 + 週次2件 = 4件★★（★D-071。同じ日曜〜土曜でも月境界で別の週になる★） |
| ★6★ | 年跨ぎ（`["2025-12-31","2026-01-01"]`） | ★年が分かれて4件★ |
| ★7★ | 並び順 | ★★`year` → `month` → `week` の昇順（`week:0` の月次が先頭）★★。★決定的であること★ |
| ★8★ | ★年次（`month:0, week:0`）★ | ★★作らない★★（P1では読まない。D-072 項4） |

### 4-2. テストを書く → 4-3. 実装

- ★`lib/db/report-mapping.ts` に置く★。★重複判定は既存の `periodMapKey()`（`:31`）を使う★
- ★★週番号は `weekOfMonth()` に必ず通すこと★★ — ★★月跨ぎ週の帰属を知っているのは
  `weekOfMonth()` **だけ**★★（D-071 / `CLAUDE.md` §4.7）。★自分で「1日から数えて7日ごと」と書かない★
- ★`todayJst()` を呼ばないこと★（★当期の特別扱いをしない。決定2★）

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

- ★`tsc` = 0 / `lint` = 0 / `npm test` 全パス★ / ★8ケースがテストになっている★
- ★★コミット★★ — 例: `git commit -m "feat: 指示書W 段階4 清掃ログの日付から期間キーを列挙する"`

---

## 段階5 — ★純粋関数⑥: 1期間ぶんの `ContinuousProblem[]` を組み立てる★（1コミット）

### 5-0. ★なぜ純粋関数に切り出すのか★

★★「連続0日は行を作らない」「ロボット0台の施設も出す」「L1 も返す」は、DB を読む
`continuousProblems()` の中に書くとテストできません★★（禁止事項17）。
★★組み立てを純粋関数にして、段階6 は「DBから材料を集めてこの関数を呼ぶだけ」にすること★★。

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

★対象★:

```ts
export function continuousProblemsInPeriod(
  entries: readonly ConsecutiveInput[],            // ★過去の行のみ・全施設・全期間★
  consecutiveById: ReadonlyMap<number, number>,    // computeConsecutiveProblemDays() の戻り
  facilityNameById: ReadonlyMap<number, string>,   // ★ロボット0台の施設も入っている★
  range: { from: string; to: string }
): ContinuousProblem[]
```

| # | 入力 | ★期待★ |
|---|---|---|
| ★1★ | `entries` = `[]` | ★`[]`★ |
| ★2★ | ★期間内にその施設の清掃ログが無い★ | ★★行を作らない★★ |
| ★3★ | ★連続0日（期間内の最終日が問題なし）★ | ★★行を作らない★★（★`escalation_level` を `as` で作らない・`?? "L1"` にしない★） |
| ★4★ | ★`n=1`（L1）★ | ★★行を**作る**★★・`escalation_level="L1"`（★絞り込みは `lib/reports/query.ts` の仕事。決定4★） |
| ★5★ | `n=2` / `n=3` / `n=6` | ★`"L2"` / `"L3"` / `"L3"`★（★`judgeEscalationLevel()` 経由。閾値を直書きしない★） |
| ★6★ | ★★`facilityNameById` にロボット0台の施設が入っている★★ | ★★その施設の行が**出る**★★（決定5） |
| ★7★ | ★`entries` にあるが `facilityNameById` に無い施設 id★ | ★★行を作らない★★（★名前を空文字や `String(id)` で捏造しない★。`facilityPerformance()` の `buildItem()` が `null` を返すのと同じ作法） |
| ★8★ | ★混在ラン（3日中1日が未実施）★ | ★`summary` = 混在テンプレート / `launch_failed_days` = 1★ |
| ★9★ | ★全日が出発失敗★ | ★`summary` = 出発失敗テンプレート / `launch_failed_days` = n★ |
| ★10★ | ★全日が完了率低下★ | ★`summary` = 完了率テンプレート / `launch_failed_days` = 0★ |
| ★11★ | 通常ケース | ★`last_problem_on` = ★期間内最終エントリの日付（`YYYY-MM-DD`）★★ |
| ★12★ | ★完了率ちょうど 70.0 が最終日★ | ★★問題あり（行が出る）・`launch_failed_days` に数えない★★（D-109 決定3） |
| ★13★ | ★同一日に複数台（1台だけ問題）★ | ★★その日は1日★★（D-109 決定2） |
| ★14★ | ★★ランが期間の前から続いている（前月から連続4日、期間内は2日）★★ | ★★`consecutive_problem_days` = 4 / ランの日付も期間外の2日を含む / `{m}` も4日ぶんで数える★★（★これが「全期間の行を渡す」理由★） |
| ★15★ | 複数施設 | ★★戻りの順序が決定的★★（`facilityNameById` の並び順で作る。★並べ替えは `lib/reports/query.ts` の仕事★） |

### 5-2. テストを書く → 5-3. 実装

★組み立ての順序（★この順で★）★:

1. `facilityNameById` を回す（★`entries` を回さない★ — 名前の無い施設を作らないため）
2. `days = consecutiveProblemDaysAtPeriodEnd(entries, consecutiveById, facilityId, range)` /
   ★`days <= 0` なら `continue`★
3. `level = judgeEscalationLevel(days)` / ★`level === null` なら `continue`★（決定6。★キャスト禁止★）
4. `latest = lastEntryInPeriod(entries, facilityId, range)` / ★無ければ `continue`★
   （★2 で 0 を弾いているので通常は起きない。それでも `!` や `as` を使わない★）
5. `facilityRows = entries.filter(e => e.facility_id === facilityId)`（★全期間★）
6. `runDates = problemRunDates(facilityRows, latest.scheduled_at.slice(0, 10), days)`
7. `m = launchFailedDaysInRun(facilityRows, runDates)`
8. `summary = continuousProblemSummary(days, m)`
9. `push({ facility_id, facility_name, consecutive_problem_days: days, escalation_level: level,
   last_problem_on: latest.scheduled_at.slice(0, 10), summary, launch_failed_days: m })`

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

- ★`tsc` = 0 / `lint` = 0 / `npm test` 全パス★ / ★15ケースがテストになっている★
- ★★`as` / `!`（non-null assertion）/ `?? "L1"` を1つも使っていない★★
- ★★コミット★★ — 例: `git commit -m "feat: 指示書W 段階5 1期間ぶんの連続問題施設を組み立てる"`

---

## 段階6 — ★配線: `prismaSource.continuousProblems()`★（1コミット）

### 6-1. 実装（★`facilityPerformance()` をひな形にする★）

★`lib/db/index.ts:1115-1117` の `notImplemented("continuousProblems")` を置き換える★:

```ts
async continuousProblems() {
  const { getPrisma } = await import("./prisma");
  const prisma = await getPrisma();

  // ★robots を join しない（ContinuousProblem に serial_number は無い。決定5）★
  //   ロボット0台の施設を除外すると、機体を撤去・交換中の施設の
  //   エスカレーションが画面から消える（fail-silent）
  const facilityRows = await prisma.facility.findMany({ where: { deleted_at: null } });
  const facilityNameById = new Map<number, string>(
    facilityRows.map((f) => [Number(f.id), f.facility_name])
  );

  const [timetables, histories] = await Promise.all([
    prisma.cleanTimetable.findMany(),
    prisma.cleanStatusHistory.findMany(),
  ]);
  const mainRows = timetables.filter((r) => r.precheck === CLEAN_TIMETABLE_PRECHECK.main);
  const historyByTimetableId = latestHistoryByTimetableId(histories);

  // ★`now` は1回だけ作って使い回す（行ごとに作ると境界の行の扱いがぶれる）★
  // ★★`isFutureSchedule()` を外さないこと — 外すと未来7日ぶんが全部「出発失敗」になり
  //   全施設が常時L3になる（D-106 / D-107）★★
  const now = new Date();
  const entries: ConsecutiveInput[] = mainRows
    .filter((row) => !isFutureSchedule(row.start_at, now))
    .map((row) => ({ /* cleanLogs() / facilityPerformance() と同じ作り方 */ }));
  const consecutiveById = computeConsecutiveProblemDays(entries);

  // ★確定期間も当期も詳細から算出する（対応するサマリが無い。D-102 項4 / 決定2）★
  //   期間キーはデータの日付から列挙する。todayJst() による当期の場合分けは要らない
  const rows: ContinuousProblemRow[] = [];
  for (const key of reportPeriodKeysFromDates(entries.map((e) => e.scheduled_at.slice(0, 10)))) {
    const range = periodRange(key.period, key.year, key.month, key.week);
    if (!range) {
      // ★自分が weekOfMonth() で作ったキーなので解けないのは実装の誤り（fail-loud）★
      //   既存2箇所（reportKpis / facilityPerformance）と同じ作法にそろえる
      throw new Error(`[db] continuousProblems: 期間範囲を算出できませんでした（${JSON.stringify(key)}）`);
    }
    for (const item of continuousProblemsInPeriod(entries, consecutiveById, facilityNameById, range)) {
      rows.push({ ...key, item });
    }
  }
  return rows;
}
```

★★注意★★:

- ★`ConsecutiveInput` の作り方は ★**`facilityPerformance()`（`lib/db/index.ts:938-947`）と1文字同じ**★
  にすること★ — ★★2画面で連続日数が食い違わないため（D-103）★★
- ★`prisma.robotErrorReport` は読まない★（★summary にエラー種別を出さないため。D-077 / 確定1★）
- ★★`lib/db/index.ts` の import に新関数4本を足す★★（`lint` の未使用/不足で気づける）
- ★`notImplemented()` が他で使われているなら残す。使われなくなったら未使用で `lint` が落ちるので消す★

### 6-2. ★この段階でやらないこと★

- ★★「2日以上」で絞らない★★（決定4。`lib/reports/query.ts` の仕事）
- ★★並べ替えない★★（`sortContinuousProblems()` の仕事）
- ★★当期の特別扱いをしない★★（決定2）

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

- ★`tsc` = 0 / `lint` = 0 / `npm test` 全パス（★この段階でテストは増えない★）★
- ★★`USE_FIXTURES=false` で `db.continuousProblems()` が例外を投げずに配列を返す★★（★件数の検証は段階8★）
- ★`git diff --stat` が `lib/db/index.ts` の1ファイルだけ★
- ★★コミット★★ — 例: `git commit -m "feat: 指示書W 段階6 continuousProblems()を実DBから作る"`

---

## 段階7 — ★画面と i18n★（1コミット）

### 7-1. `lib/i18n.ts` に3キーを足す

★★段階3 の定数と1文字も違わないこと★★（★`translate()` は未登録キーを日本語のまま返す。
fail-silent で気づけない。D-111 項8★）。

| キー（日本語原文） | ★EN★ |
|---|---|
| `"{n}日連続で完了率が△以下"` | `"Completion rate △ or below for {n} consecutive days"` |
| `"{n}日連続で出発失敗"` | `"Launch failed for {n} consecutive days"` |
| `"{n}日連続で△以下（うち{m}日は出発失敗）"` | `"△ or below for {n} consecutive days ({m} with launch failure)"` |

★英文は実装者判断で改善してよいが、★★`{n}` / `{m}` を必ず1回ずつ含めること★★★。
★既存の `"{n}日以上連続": "{n}+ consecutive days"`（`lib/i18n.ts:175`）の語彙に寄せる★。

### 7-2. `ReportsScreen.tsx:391-394` を直す

★現状（★`t()` を通していない＝EN で和文が残る★）★:

```tsx
<span className={styles.contSummary}>{item.summary}</span>
```

★直したあと（例。★`{m}` は値があるときだけ置換する★）★:

```tsx
// ★サーバはテンプレート文字列を返す（決定3。方式A）。数値はここで差し込む★
//   `String.replace` は文字列指定だと最初の1件だけ置換する。各テンプレートに
//   `{n}` `{m}` は1回ずつしか出ないのでこれで足りる。
//   ★`launch_failed_days` は任意項目（fixtures を凍結しているため）。
//     値が無いときは置換しない（0 を捏造しない）★
const summaryText = (() => {
  const base = t(item.summary).replace("{n}", String(item.consecutive_problem_days));
  return item.launch_failed_days === undefined
    ? base
    : base.replace("{m}", String(item.launch_failed_days));
})();
```

- ★★`:391-393` の既存コメント（「モックの『完了率 {n}%』ではなく summary を出す★（指示書F 0-1 の判断）」）も
  書き換えること★★ — ★★D-026 の説明を出す、という理由はもう成り立たない（D-111 / 本書）★★
- ★★`fixtures` の summary は完成文でプレースホルダを含まない★★ので、`replace` は何もしない。
  ★EN 表示では和文のまま出るが、それは想定内★（D-111 項5）
- ★★`ReportsScreen.tsx:364` の副題（「2日以上連続で△以下または出発失敗の施設」）は**本書では直しません**★★
  （★申し送り③★）。★ついでに直さないこと（差分が増えてレビューしづらくなる）★

### 7-3. 確認

- ★JP / EN を切り替えて、★★このカードの文が両方とも言語に追従すること★★を目視する★
- ★★EN でこのカードだけ和文が残っていたら、キーが1文字違う★★（段階3 の定数と突き合わせる）

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

- ★`tsc` = 0 / `lint` = 0 / `npm test` 全パス★
- ★`git diff --stat` が `lib/i18n.ts` と `components/features/reports/ReportsScreen.tsx` の2ファイル★
- ★★コミット★★ — 例: `git commit -m "feat: 指示書W 段階7 連続問題施設のsummaryをi18n対応にする"`

---

## 段階8 — ★実測での検証（★コミットなし・DBに1行も書かない★）★

### 8-1. ★データ層の実測★

★★`.env.local` を書き換えず、コマンドラインで env を渡すこと★★（★戻し忘れと他セッションへの影響を防ぐ★。
D-108 項8）。★★一時スクリプトには成功時にも必ず `process.exit(0)` を書くこと★★（★D-111 項9。
`main().catch(...)` だけだと Prisma の接続が残り、★実際に4時間39分ハングした★★）。

```
USE_FIXTURES=false DATABASE_URL="mysql://root@localhost:3306/dwatch_local" npx tsx -e "
import { db } from './lib/db';
import { ESCALATION_THRESHOLDS } from './lib/config';
const rows = await db.continuousProblems();
const two = rows.filter(r => r.item.consecutive_problem_days >= ESCALATION_THRESHOLDS.level2Days);
console.log('全行', rows.length, '2日以上', two.length);
console.log(JSON.stringify(rows.map(r => [r.period, r.year, r.month, r.week,
  r.item.facility_id, r.item.consecutive_problem_days, r.item.escalation_level,
  r.item.last_problem_on, r.item.launch_failed_days, r.item.summary])));
const logs = await db.cleanLogs();
const pre = await db.prechecks();
console.log('cleanLogs', logs.length, 'prechecks', pre.length);
const fp = await db.facilityPerformance();
console.log(JSON.stringify(fp.map(r => [r.period, r.year, r.month, r.week,
  r.item.facility_id, r.item.consecutive_problem_days, r.item.status])));
process.exit(0);            // ★★必ず書くこと★★
"
```

★`npx tsx` でエイリアス（`@/`）が解決できない場合★は ★dev サーバ＋curl に切り替えてよい★
（`/api/reports/continuous-problems?period=weekly&year=...`）。★★その場合も下の期待値をすべて実測すること★★。

★★期待値（★作成時点の実測。★着手直前に段階0 で自分が控えた値を基準にすること★★）★★:

| # | 項目 | ★期待★ |
|---|---|---|
| ★1★ | ★2日以上の件数★ | ★★当週 **4件** / 当月 **4件** / 確定6期間は**すべて0件**★★ |
| ★2★ | ★施設5★ | ★★2日 / `L2` / `launch_failed_days` = **1** / summary = `"{n}日連続で△以下（うち{m}日は出発失敗）"`★★ |
| ★3★ | ★施設6★ | ★★4日 / `L3` / `launch_failed_days` = **3** / summary = 混在テンプレート★★ |
| ★4★ | ★施設7★ | ★4日 / `L3`★。★`launch_failed_days` は ★**0 か 4 のどちらか**★（★混在は施設5・6 の2件だけ★）。★実測して記録すること★ |
| ★5★ | ★施設9★ | ★★6日 / `L3` / `launch_failed_days` = **0** / summary = `"{n}日連続で完了率が△以下"`★★（★5日連続で△のあと最終日だけ 9.9% で ×。★★「△以下」に畳むので1本の連続になる★★） |
| ★6★ | ★L1（1日）★ | ★★施設8・施設10 が **`escalation_level: "L1"` で行として返る**★★（★データ層は L1 も返す。決定4★） |
| ★7★ | ★確定期間の行★ | ★★期間キー自体は存在する（8月ぶんの週次・月次）が、連続0日なので `ContinuousProblem` が0件★★ |
| ★8★ | ★`cleanLogs()` / `prechecks()`★ | ★★**259 / 27**（★1件も変わっていないこと★）★★ |
| ★9★ | ★★`facilityPerformance()` の全出力★★ | ★★段階0 で控えた内容と**完全一致**★★（★段階2 のリファクタが値を変えていないこと★） |
| ★10★ | ★`summary` に数値が埋まっていないこと★ | ★★戻り値に `{n}` `{m}` の文字がそのまま残っている★★（★埋まっていたら決定3 違反★） |
| ★11★ | ★`last_problem_on`★ | ★`YYYY-MM-DD`（時刻を含まない）★。★その日が問題日であること（施設5 なら 70.0 の日）★ |
| ★12★ | ★`launch_failed_days` <= `consecutive_problem_days`★ | ★★全行で成立★★ |

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

| 画面 | 期待 |
|---|---|
| ★`/reports`★ | ★★連続問題施設カードが **500 → 200**、当週で **4件**★★（★limit 既定10 なので「他N件を表示」は出ない★）。★KPI・施設別・エラー内訳が回帰していないこと★ |
| ★`/reports`（先週・先月に切替）★ | ★★「該当施設なし」（0件）が出て、**エラーにならない**★★ |
| ★`/detail-log`★ | ★200 / **259件**★ |
| ★`/dashboard` / `/alerts`★ | ★200 / **9 / 9** / **27件**★ |
| `/facilities` / `/schedules` / `/ds-logs` / `/ds-alerts` | ★200。回帰していないこと★ |

- ★★**404 が出たら Edge Runtime バンドル破壊の回帰を疑うこと**★★（D-086）
- ★★`next dev` は `.env.local` を共有する別セッションに影響します（D-108 項8）★★ —
  ★`USE_FIXTURES=false npm run start -- -p <別ポート>` のように環境変数を都度渡す★

### 8-3. ★`USE_FIXTURES=true` での確認★

- ★★fixtures 側の12件が1件も変わっていないこと★★（★summary は手書きの完成文のまま★）
- ★★`launch_failed_days` が無くても画面が壊れないこと★★（★`{m}` の置換をスキップする分岐★）
- ★EN 切替で fixtures の summary が和文のまま出るのは★★正常★★（D-111 項5）

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

- ★8-1 の12項目・8-2 の回帰確認・8-3 をすべて実測した★
- ★★DB に1行も書いていないことを確認した★★（★本書に INSERT / UPDATE は1つもありません★）
- ★★`.env.local` が着手前と同じ（`USE_FIXTURES=true`）であることを確認した★★
- ★`git status` に `lib/` `docs/` `components/` `d-watch-api.yaml` 以外の差分が無いことを確認した★
  （★`README.md` / `docs/code-map.md` / `docs/handoff/管理画面_設計検討_20260910.md` /
  `.claude/worktrees/**` が★**着手前と同じ状態のまま**★であること。禁止事項2）

---

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

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

★見出しの形式は既存に揃える★:
`### D-113 \`[確定]\` 指示書W（連続問題施設を実DBから作る）を実行した`

★最低限入れる項目★:

| # | 項目 |
|---|---|
| 1 | ★★`continuousProblems()` を実装し、監視側の `DataSource` が全部埋まったこと★★（★残るは点検記録2本（D-111 項6）と Pudu Cloud 待ちの `GET /clean-logs/{id}/areas`（501）★） |
| 2 | ★★確定2（C案）: 混在ランは「{n}日連続で△以下（うち{m}日は出発失敗）」★★。★★**D-111 項2 の決定4（出発失敗を優先）を差し替えたこと**を明記する★★（★却下理由: 直近に清掃を実施していても「出発失敗」と書くことになり事実と食い違う / (b) は出発失敗を隠す★）。★実データに2件実在（施設5・施設6）★ |
| 3 | ★★確定3: △ と × は書き分けず「△以下」に畳む★★（★施設9 は5日連続で△のあと最終日だけ 9.9% で ×★。★モック `d-watch-mock.jsx:713` の副題と同じ粒度★） |
| 4 | ★決定1: API契約を先に直した★（`d-watch-api.yaml` の summary の description / example が D-026 の説明になっていた）。★`launch_failed_days` を**任意項目**として足した理由（fixtures を凍結するため）★ |
| 5 | ★★決定2: 確定期間も詳細テーブルから算出した★★（★対応するサマリが無い（D-102 項4）★ / ★D-102 に反しない（禁じているのは「サマリがあるのに再計算すること」）★ / ★当期だけにすると先週・先月でこのカードだけ常に空になり、施設別パフォーマンス表と不整合になる★） |
| 6 | ★決定3: i18n はテンプレート方式（方式A）★。★サーバはテンプレートのまま返し、画面が `{n}` `{m}` を差し込む★。★`ReportsScreen.tsx:394` が `t()` を通していなかったことも直した★ |
| 7 | ★決定4: L1 もデータ層は返す★（★「2日以上」は `lib/reports/query.ts:236` に実装済み。データ層でも絞ると規則が2箇所になる（D-103）★） |
| 8 | ★★決定5: ロボット0台の施設を除外しない★★（★`facilityPerformance()` が除外しているのは `serial_number` を出すためで、`ContinuousProblem` には無い★ / ★除外すると機体を撤去・交換中の施設のエスカレーションが消える（fail-silent）★） |
| 9 | ★決定6: 連続0日は行を作らない★（★`judgeEscalationLevel()` が `null` を返す。`as` キャストや `?? "L1"` で握り潰さない★） |
| 10 | ★★共有した既存関数と、新設した6本★★（★`lastEntryInPeriod()` / `problemRunDates()` / `launchFailedDaysInRun()` / `continuousProblemSummary()` / `reportPeriodKeysFromDates()` / `continuousProblemsInPeriod()`。名前は実装に合わせる★） |
| 11 | ★★型を拡張しなかった理由★★（★`ScheduleDayResult` と `foldProblemDaysByDate()` の戻り型は `toEqual` 比較のテストが依存している / `consecutiveProblemDaysAtPeriodEnd()` のシグネチャも同様★） |
| 12 | ★★ランは期間をまたぐので、ランの抽出には「その施設の全期間の行」を渡すこと★★（★踏むと `{m}` が過小になる★） |
| 13 | ★段階8 の実測値（★2日以上が当週4件・当月4件・確定6期間0件 / 施設5=2日(m=1) / 施設6=4日(m=3) / 施設7=4日(m=実測値) / 施設9=6日(m=0) / L1 は施設8・施設10 / `cleanLogs` 259 / `prechecks` 27 / テスト数★） |
| 14 | ★申し送り（下記「申し送り」節をそのまま移す）★ |

### 9-2. `docs/handoff.md` に追記（★追記34★）

- ★見出しは `## 2026-09-1X（追記34）`★（★番号は実測すること★。★別担当が先に使っていたら35★）
- ★冒頭の「最新の追記」ブロック（`docs/handoff.md:6` 付近）も更新すること★
- ★入れる内容★:
  - ★★「連続問題施設の summary は**事実だけ**。テンプレート文字列（`{n}` / `{m}`）で返し、画面が数値を差し込む」★★
  - ★★「混在は C案。△ と × は畳む」★★
  - ★「確定期間も詳細から算出する（対応するサマリが無い）」★
  - ★「ロボット0台の施設も出す / L1 もデータ層は返す / 連続0日は行を作らない」★
  - ★次に触る人が踏みやすい落とし穴（★ランは期間をまたぐ★ / ★`foldProblemDaysByDate()` の戻り型を拡張すると
    `toEqual` のテストが全部落ちる★ / ★i18n のキーは1文字違うと fail-silent★）★
  - ★★「監視側の `DataSource` は全部埋まった。残りは点検記録2本」★★
  - ★申し送り★

### 9-3. push とこの段階の完了条件

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

---

## ★★申し送り — 本書では直さない論点★★

★D-113 に列挙して残すこと★。

| # | 論点 | 状態 |
|---|---|---|
| ★①★ | ★★全件取得をやめて SQL に絞り込みを降ろす件★★（★ユーザー提起 2026-09-11★）— 現状 `cleanLogs()` / `facilityPerformance()` / `continuousProblems()` はいずれも `clean_timetables` / `clean_status_histories` を**全期間・全件**読む（★`DataSource` が引数を取らない契約★。`lib/db/index.ts:216-218` / D-090 項15）。★年間13万行（`docs/spec.md:45`）では重い（D-092 項3-3）★ | ★★`DataSource` の契約変更なので**全メソッドに波及する**★★。★テストDBで実測してから**専用の指示書**で行う★。★★第7回で取り下げた索引の依頼は、これを実施するなら**再依頼が要る**★★ |
| ★②★ | ★`.contSummary`（`components/features/reports/ReportsScreen.module.css:251-257`）に省略記号の指定が無く、長い文は折り返して行が伸びる★ — ★施設名（`.contFacility`）は `nowrap` で縮まない★ | ★★C案の文は22文字前後で境界★★（例:「4日連続で△以下（うち3日は出発失敗）」）。★`text-overflow: ellipsis` を足すか、行の高さが伸びるのを許容するかの判断が要る★ |
| ★③★ | ★`ReportsScreen.tsx:364` の副題「2日以上連続で△以下または出発失敗の施設」が ★`t()` 未適用★ かつ ★「2」がハードコード★（`ESCALATION_THRESHOLDS.level2Days` から読んでいない）★ | ★本書では直さない（差分を段階7 の1箇所に限るため）★。★★閾値を変えると副題だけ嘘になる★★ |
| ★④★ | ★★当期は同じ全件スキャンが2〜3回重なる★★ — `reportKpis()` / `facilityPerformance()` / `continuousProblems()` がそれぞれ独立に全件を読む（★`/reports` を1回開くと3回★） | ★①と一体で見直す★ |
| ★⑤★ | ★`periodRange()` が解けないと一覧APIが 500★（★本書の `continuousProblems()` も既存2箇所と同じく `throw` する★） | ★指示書V 申し送り⑤ と同じ。「その行だけ落とす」に変えるか要検討★ |
| ★⑥★ | ★★`consecutiveProblemDaysAtPeriodEnd()` は「期末時点の連続」であって「期間中の最長連続」ではない★★（A案。D-103）— ★期間の途中で5日連続していても、期末の日が問題なしなら 0★ | ★★施設別パフォーマンスと同じ規則なので**意図した挙動**★★。★確定期間が0件になる主因でもある★。定義を変えたくなったら D-103 から見直す |
| ★⑦★ | ★論理削除された施設（`facilities.deleted_at != null`）の清掃ログは、施設名が引けないので行が作られない（fail-silent）★ | ★`facilityPerformance()` の `buildItem()` と同じ作法にそろえてある★。★方針として妥当かの確認が要る（指示書V 申し送り⑥ と同種）★ |

---

## 完了条件（全体）

| # | 条件 |
|---|---|
| ★1★ | ★`d-watch-api.yaml` の `summary` の description / example が**事実ベースの3種**に書き換わり、`launch_failed_days`（任意項目）が足されている★ |
| ★2★ | ★`lib/types/index.ts` が YAML に追従している（`launch_failed_days?: number`）★ |
| ★3★ | ★★`prismaSource.continuousProblems()` が `notImplemented` でなくなり、`USE_FIXTURES=false` で 200 が返る★★ |
| ★4★ | ★確定期間（先週・先月）も詳細テーブルから算出しており、期間キーがデータの日付から列挙されている（`todayJst()` による当期の場合分けが無い）★ |
| ★5★ | ★summary が★**テンプレート文字列**★で返り、画面が `{n}` `{m}` を差し込んでいる。★`lib/i18n.ts` に3キーの EN 対訳がある★★ |
| ★6★ | ★★混在ランが C案の文面になっている（実データの施設5・施設6 で確認）★★ |
| ★7★ | ★△ と × を書き分けていない（施設9 が1本の連続として出る）★ |
| ★8★ | ★L1 の行がデータ層から返り、API 応答には含まれない（絞り込みは `lib/reports/query.ts` のまま）★ |
| ★9★ | ★ロボット0台の施設が除外されていない（`prisma.facility.findMany` に `include: { robots: ... }` が無い）★ |
| ★10★ | ★連続0日の行が作られていない。★`as` / `!` / `?? "L1"` を使っていない★★ |
| ★11★ | ★`computeConsecutiveProblemDays()` / `foldProblemDaysByDate()` / `consecutiveProblemDaysAtPeriodEnd()` / `isFutureSchedule()` / `weekOfMonth()` を**共有**しており、同等の処理を別実装していない★ |
| ★12★ | ★★`ScheduleDayResult` / `foldProblemDaysByDate()` の戻り型と `consecutiveProblemDaysAtPeriodEnd()` のシグネチャが変わっていない★★ |
| ★13★ | ★期待値表（段階2 の8 / 段階3 の 10+8+8 / 段階4 の8 / 段階5 の15）がすべてテストになっている★ |
| ★14★ | ★`npx tsc --noEmit` = 0 / `npm run lint` = 0 / `npm test` 全パス（★テスト数は着手前より増えている★）★ |
| ★15★ | ★段階8 の実測: 2日以上が★当週4件・当月4件・確定6期間0件★ / 施設5=2日(m=1) / 施設6=4日(m=3) / 施設9=6日(m=0) / L1 は施設8・施設10★ |
| ★16★ | ★★`cleanLogs()` = 259件 / `prechecks()` = 27件 / `facilityPerformance()` の全出力が段階0 と完全一致★★ |
| ★17★ | ★`prisma/` / `lib/fixtures/` / `lib/judge/` / `lib/reports/query.ts` / `app/api/` に差分が無い★ |
| ★18★ | ★`README.md` / `docs/code-map.md` / `docs/handoff/管理画面_設計検討_20260910.md` / `.claude/worktrees/**` に触っていない★ |
| ★19★ | ★`docs/decisions.md`（★D-113★）と `docs/handoff.md`（追記34）に記録した★ |
| ★20★ | ★8コミット（段階1 / 2 / 3 / 4 / 5 / 6 / 7 / 9）で push した★ |
| ★21★ | ★`.env.local` が `USE_FIXTURES=true` に戻っている★ |
| ★22★ | ★★DB に1行も書いていない★★ |

---

## 実行状態

| 項目 | 内容 |
|---|---|
| 状態 | ★**完了**★ |
| 実行日 | 2026-09-11 |
| 決定番号 | ★**D-116**★（着手時点の最終番号はD-115。指示書Y完了後に実行したため見込みのD-113ではない） |
| コミット | `e24e177`(1) / `985ce60`(2) / `e136256`(3) / `1aba267`(4) / `f73b6b7`(5) / `2974653`(6) / `b214449`(7) / 本コミット(9) |
| 実測値 | `tsc`0 / `lint`0 / `npm test` 45ファイル1023テスト / 当週・当月とも2日以上4件(施設5=2日m1/施設6=4日m3/施設7=4日m0/施設9=6日m0) / L1が施設8・10 / `facilityPerformance()`全出力が段階0と完全一致。詳細は `docs/decisions.md` の **D-116** / `docs/handoff.md` **追記36** を参照 |
