# 指示書K — エラー値を変換せず生値で表示する（D-077 項1・項2・項4）

| 項目 | 内容 |
|---|---|
| 実行環境 | ★**Cursor 内の Claude が実装します**★（`.claude/commands/d-watch.md` の役割分担表を参照） |
| 実行状態 | ★**完了（2026-09-09。D-088）**★ |
| 実行順 | ★**指示書I とはファイルが重なりません。並行して進められます**★（I は `proxy.ts` / 認証まわり）。ただし `docs/decisions.md` の**決定番号は取り合う**ので、**番号を取る直前に最終番号を確認すること** |
| 取得する決定番号 | ★**D-088 から**★（**D-087 まで使用済み**。★当初は「D-079 から」としていたが、★**D-079〜D-087 は指示書L・M・N の実行や他作業で採番済み**★ なので繰り上げた★。★着手直前に `grep -n "^### D-0" docs/decisions.md` で再確認すること★。**他の指示書と同時に走らせないこと**） |
| 前提 | ★**D-077**★（項1・項2・項4 が本指示書の実装対象）／ ★**D-078 項2**★（B2 の暫定マッチ値 `Handrail Lifted` の承認。★段階7-B の根拠★）／ ★**D-082**★（★B2/B3 は暫定値のまま進め、実データ確定後に修正する。★段階7-B の位置づけと優先度★★）／ D-039（B1〜B3 は P1 に含む） |
| 段階数 | ★**11段階**★ = 段階0〜9 の10段階 ＋ ★**段階7-B**★（D-078 項2 を受けて後から挿入。★**既存の段階番号は繰り下げていない**★。他文書からの参照がズレるため）。★**段階7-B は後回しにしてよいが、飛ばしてはいけない**★（D-082。→ 段階7-B 冒頭） |
| 着手前の基準 | `npm test` が **29ファイル / 587テスト 全パス**、`npx tsc --noEmit` エラー0、`npm run lint` 0（★2026-09-09 実測★。**着手直前に自分で実行して実測値を基準にすること**） |
| 作成 | 2026-09-08（作成時点の決定事項最終番号 **D-077**） |
| 改訂 | ★**2026-09-08**★ — D-078 項2（B2/B3 の扱いの承認）を受けて、冒頭の「着手前にユーザーへ確認すること」を ★**承認済み**★ に書き換え、★**段階7-B を追加**★ |
| 改訂 | ★**2026-09-08**★ — ★**D-082**★（B2/B3 は暫定値のまま進め、実データ確定後に修正する）を受けて、★**段階7-B の冒頭に「この段階の位置づけ」を追加**★（★目的は「暫定値を入れて動く形にする」ことであり「正しく発火させる」ことではない★／★後回しにしてよいが飛ばしてはいけない★）、★**段階9 に「暫定値であること」の記録指示を追記**★。★段階の番号・実装内容そのものは変更していない★ |
| 改訂 | ★**2026-09-09**★ — 本指示書の作成後に ★**指示書L（D-084）・M（D-086）・N（D-087）が実行された**★。L でマスタ画面3枚（facilities / schedules / system）を実装し `inspections` をメニューから隠した／M でローカルDB（`dwatch_local`）経由の `prismaSource.facilities()` と `schedules()` が動くようになった（★`USE_FIXTURES=false` で実DB経由に切替可能★）／N で対応機種レジストリ（`lib/config/index.ts` の `ROBOT_MODELS`）と `lib/format/robot-model.ts` の `isSupportedModel()` を追加し、★fixtures を PUDU CC1 のみにした★。★取得する決定番号・着手前の基準値・file:line 参照を本改訂で実測し直した★ |

> 実行後はこの表と、文書末尾の「実行状態」表を更新してください。

> ★**この指示書を読んでいるあなたへ**★
> `.claude/commands/d-watch.md` に「実装は Cursor 側、Claude Code は指示書作成まで」とありますが、
> これは**2つの環境の役割分担**です。**Cursor の中で動いている Claude は実装担当**です。
> この指示書を渡されたなら、**調査や改訂ではなく実装を行ってください。**

---

## ★★B2 / B3 の扱い — ★承認済み（2026-09-08 / D-078 項2）★★★

★**当初この指示書は「B2 / B3 の扱い」を意図的に含めず、ユーザーの承認待ちにしていました。
2026-09-08 に承認と具体的な値が出たため、★段階7-B★ として本指示書に取り込みました。**★

★**「承認が出るまで `lib/judge/advice.ts` と `lib/config/advice-rules.ts` に触らないこと」という
制約は解除されています。**★ ただし ★**段階7-B の範囲でのみ触ること**★（段階1〜7 では触らない）。

### ★承認された内容（D-078 項2）★

**ユーザー指示（原文）**:

```
status1h === "ハンドルアップ"
となっている部分も、先程のhandrail ltedで変更してください、ただし暫定で変更のおそれはあります
```

| # | 承認された内容 |
|---|---|
| 1 | ★**B2（ハンドルアップ）のマッチ値を `Handrail Lifted` とする**★ |
| 2 | ★**「暫定で変更のおそれはあります」とユーザーが明言している**★。★**確定値ではない**★ |
| 3 | ★**当方が推奨していた「発火条件を `lib/config/` の設定値に外出しし、既定は空」という方式もあわせて採用する**★。暫定値なので、値が変わったときに ★**1箇所の差し替えで済む形**★ にするため。`CLAUDE.md` §7 のハードコード禁止にも合致する |
| 4 | ★**B3（オフライン）は依然として値が不明**★。24値にも `robots.status` の46値にも該当が無い。★**B3 のマッチ文字列は空のままとする（＝発火しない）**★ |
| 5 | **B1（完了率ベース）は影響を受けず、そのまま動く** |

★**`Handrail Lifted` は 24値の受領リストに含まれていません。**★
ユーザーも「〜だと思いますが」と述べており ★**確定ではありません**★。
★**実データに接続できたら、`error_type` に `Handrail Lifted` が実在するかを最優先で確認すること**★
（段階9 の申し送りに書きます）。

★**実装は段階7-B に書いてあります。そちらに従ってください。**★

### 何が起きるのか（★承認の背景。読むだけ★）

- `lib/judge/advice.ts:80-82` と `:83-85` は ★**日本語文字列の完全一致**★ で発火します。

  ```ts
  // lib/judge/advice.ts:79-85
  // B2 / B3: 事前チェックのステータス由来
  if (status1h === "ハンドルアップ" || status30m === "ハンドルアップ") {
    matched.push("B2");
  }
  if (status1h === "オフライン" || status30m === "オフライン") {
    matched.push("B3");
  }
  ```

- `status_1h` / `status_30m` の生成元は ★**`lib/fixtures/clean-logs.ts:53-54` の1箇所だけ**★ で、
  `?? "待機中"` という**日本語のデフォルト値まで埋め込まれています**。
  `prismaSource.cleanLogs()`（`lib/db/index.ts:150-151`。★2026-09-09 実測。作成時点は `:141-142`★。
  ★指示書M で `prismaSource` に `facilities()`/`schedules()` の実装が入り構造が変わったため行がズレた★）は `notImplemented` なので、
  ★**実データ由来の値は一度も流れたことがありません**★。
  つまり現在の値は「DBの生値」ではなく ★**当方が fixtures に書いた日本語**★ です。
- 生値方針に切り替えると、B2/B3 は ★**例外もテスト失敗も出さずに永久に発火しなくなります**★。
  しかも `lib/judge/__tests__/advice.test.ts:132-176` は**日本語リテラルを関数に直接渡している**ため、
  ★**テストは緑のまま**★ です。「静かに壊れる」最悪の組み合わせになります。
- そもそも元になる値が存在しません。`robots.status` の ★**46値**★
  （`-300`〜`999`。★`103` が欠番★で `102` の次が `104`。
  `docs/handoff/TeamWorks回答_第2回_20260903.md:53-98` を数えた実測。
  ★**D-077 起票時の「47件」は誤りで、`docs/decisions.md:1900-1901` で 46 に訂正済み**★）は
  **移動・充電・清掃・帰還の動作フェーズのみ**で、
  ★**ハンドルアップも通信断も表現していません**★（同ファイル `:107-108` が自ら明記）。
  `error_type` 24値（`docs/handoff/analysis/error-type-mapping.md`）にも該当がありません。
- ★**D-039 は「B1〜B3 を P1 に含める」と確定しています**★。したがってこれは Phase B の先送りではなく
  ★**P1 の欠落**★ です。

★**上はすべて「承認前の状態」の説明です。**★
★**段階7-B を実施すると、B2 は暫定値 `Handrail Lifted` で発火するようになります。**★
★**B3 は「マッチ配列が空」という形で明示的に発火しない状態になります**★ ―
★「静かに壊れている」のではなく「値が未判明であることが設定値に明記されている」状態に変わります。★

### ★当方の推奨案 → ★採用された★（D-078 項2）★

| # | 内容 | 状態 |
|---|---|---|
| 1 | B2/B3 の発火条件を `lib/config/` の**設定値**（マッチする文字列の配列）に外出しする | ★**採用**★（段階7-B） |
| 2 | ★**既定は空配列＝発火しない**★ にする | ★**採用**★。ただし ★**B2 だけは暫定値 `Handrail Lifted` を入れる**★。★**B3 は空のまま**★ |
| 3 | 実データで値が判明したら、**設定値に足すだけで復活**する | ★**採用**★ |
| 4 | `CLAUDE.md` §7 のハードコード禁止にも合致し、★**「静かに壊れる」を「明示的に空」に変えられる**★ | ★**採用**★ |
| 5 | **B1（完了率ベース）は影響を受けず生き残る** | 変更なし |

★**根拠: D-078 項2（2026-09-08）**★。実装手順は ★**段階7-B**★ を参照してください。

---

## この指示書の位置づけ

**D-077 項1・項2（2026-09-08 のユーザー指示）を実装に落とします。**

```
ロボットのエラーについては現状PuduAPIから何が返ってくるのか不明なので、
DBから来た文字列をそのまま変換せずに記載する形で実装してください
LostLocation等の-200で想定しているものについても同様です
D-security制御盤のエラーについては正常か正常でないか、をいったん判断するだけで良いです
```

★**D-057 / D-067 で決めていた「サーバ側で7分類に寄せる／未知の値は `other` に寄せる」方針は破棄されました。**★

**なぜ今なのか**: `prismaSource` は `errorBreakdown()`（`lib/db/index.ts:254-255`。★2026-09-09 実測。作成時点は `:174-175`★）も
`cleanLogs()`（`:141-142`）も `notImplemented` のままで、
★**変換ロジックはまだ1行も書かれていません。手戻りが最小のタイミングです。**★

**なぜ生値なのか**: Pudu API から実際に何が返るか不明なため、推測でマッピングしない
（`CLAUDE.md` §7「仕様が不明な箇所を推測で実装すること」の禁止に合致）。

---

## ★禁止事項（この指示書の全段階に効きます）★

| # | 禁止 | 理由 |
|---|---|---|
| 1 | ★**変換表・マッピングを新しく作ること**★ | D-077 で破棄された方針そのもの。`docs/handoff/analysis/error-type-mapping.md` の対応表は**参考資料に降格済み**で、実装に使ってはいけません |
| 2 | ★**日本語の分類名（「ハンドルアップ」「ブラシエラー」等）をコードに書くこと**★ | 生値方針では画面に出るのは DB の値。日本語をコードに置くと、それが**変換表の入口**になります |
| 3 | ★**`git add -A` を使うこと**★ | 過去に**別作業の途中ファイルを巻き込む事故**が起きています。**必ずファイルパスを明示** |
| 4 | ★**`lib/db/` の `prismaSource` に手を付けること**★ | 実DB接続は別作業。`notImplemented` のままにする |
| 5 | ★**段階7-B 以外で `lib/judge/advice.ts` / `lib/config/advice-rules.ts` を触ること**★ | ★**承認は出ました（D-078 項2）が、触ってよいのは段階7-B の範囲だけ**★。段階1〜7 では触らない（1コミットの粒度を保つため） |
| 6 | ★**`DsDevice.connection_status` を触ること**★ | すでに2状態（`ok`/`error`）。**指示書G / D-065 / D-076 で完了済み** |
| 7 | ★**`DsAlert.alert_type` / `resolution_status` を触ること**★ | 段階7 の「触らないもの」を読むこと |
| 8 | ★**`"disconnected"` の一括置換**★ | `DsAlertType`（`lib/types/index.ts:306`）/ `DsAlertTypeFilter`（`:310`。★2026-09-09 実測。作成時点は両方 `:291` 付近★）の `"disconnected"` は**別物**。`docs/handoff/指示書G_D-Secログの備考入力と2状態化.md:192-194` の警告を読むこと |
| 9 | ★**メインDBへの書き込み**★ | `CLAUDE.md` §7 / D-042。今回そもそも書き込みは発生しません |

---

## 作業の順序（★崩さないこと★）

| 段階 | 内容 | コミット |
|---|---|---|
| 段階0 | 前提の確認（★読むだけ★） | **コミットなし** |
| 段階1 | ★**API契約を直す**★（`d-watch-api.yaml`）+ `meta` の扱いを決める | 段階1で1コミット |
| 段階2 | 型を緩める（`lib/types/index.ts`） | 段階2〜4をまとめて1コミット（★型変更はコンパイルが通らないと切れない★） |
| 段階3 | ソート順を作り直す（`lib/reports/query.ts` + テスト） | 同上 |
| 段階4 | 画面の色分けを壊さない（`ReportsScreen.tsx`） | 同上 |
| 段階5 | fixtures を実データの形に寄せる（`lib/fixtures/reports.ts` + テスト） | 段階5で1コミット |
| 段階6 | 色分けの重複実装を1本化する（`lib/theme-status.ts`） | 段階6で1コミット |
| 段階7 | D-Sec の `detail` を落とす（D-077 項4 の残作業） | 段階7で1コミット |
| ★**段階7-B**★ | ★**B2 の発火条件を設定値に外出しし、暫定値 `Handrail Lifted` を入れる**★（D-078 項2） | 段階7-Bで1コミット |
| 段階8 | テストを通す（★段階ごとに実行済みのはず。ここは最終確認★） | コミットなし |
| 段階9 | 記録と push | 段階9で1コミット + push |

★**段階7-B の番号について**★: この指示書は段階0〜9 で番号が確定しており、他文書から参照されています。
★**繰り下げると参照がズレる**★ので、★**段階7 と段階8 の間に「段階7-B」として挿入しました**★。
**実行順は 段階7 → 段階7-B → 段階8 です。**

★**段階ごとに `npx tsc --noEmit` / `npm run lint` / `npm test` を実行し、通ってからコミットすること**★（D-050）。

---

## 段階0 — 前提の確認（★読むだけ・コミットなし★）

### 読むもの

| 資料 | 何を読むか |
|---|---|
| `docs/decisions.md` の **D-077**（`:1845` 付近） | ★**項1・項2・項4**★。項3（B2/B3）は ★**D-078 項2 で承認済み**★ → 段階7-B |
| `docs/decisions.md` の **D-078**（`:1995` 付近） | ★**項2 が段階7-B の根拠**★（B2 の暫定マッチ値 `Handrail Lifted` / 設定値への外出し / B3 は空のまま）。★**項1（参照用アカウント受領）は段階9 の申し送りに使います**★ |
| `docs/handoff/analysis/error-type-mapping.md` | ★**24値の「値の一覧（原文）」だけ**★。段階5 で使います |
| `docs/handoff/指示書G_D-Secログの備考入力と2状態化.md:192-194` | ★`DsAlertType` の `"disconnected"` は別物★という警告。段階7 の前に必ず読む |
| `CLAUDE.md` §6-1 / §7 / §9.3 | YAML が先 / 禁止事項 / 一覧APIの `meta` 規約 |

### ★踏まないでほしい地雷（先に頭に入れること）★

| # | 内容 |
|---|---|
| 1 | ★**`docs/handoff/analysis/error-type-mapping.md` は参考資料に降格しました**★（同ファイル `:1-4, :6-30`）。★**「7分類」列と「推定される意味」列は実装に使ってはいけません**★。使えるのは**値の一覧（原文）だけ**です |
| 2 | ★**`d-watch-api.yaml` の enum の実体は `:1205` です**★（★2026-09-09 再実測。指示書K 作成時点は `:1192` だったが、その後の他作業でファイル前方に行が挿入され13行分ズレた★）。`:1160` と書いてある古い参照に騙されないこと。★**`:1160` は現在 `FacilityPerformance.status` の enum で、`ContinuousProblem.consecutive_problem_days`（現在地: `:1173` 付近）とも本件とも無関係**★。`error-type-mapping.md:42-45` では既に訂正済みですが、`docs/decisions.md:1350` には ★**まだ `:1160` という誤記が残っています**★（段階9 で直します。★その際は現在の実体である `:1205` を書くこと★） |
| 3 | ★**`cwd` のドライブレターは必ず大文字 `C:\`**★。小文字だと Vitest が全滅します（既知の罠） |
| 4 | ★**`--reporter=basic` は Vitest 4 で削除済み**★。使うとロードに失敗しても exit 0 になり、**壊れていることに気づけません** |
| 5 | **決定番号は追記直前に最終番号を確認する**。★2026-09-07 に `D-068` の二重採番が実際に起きました★ |

### 段階0の完了条件

- 上の資料を読んだこと
- `git status` がクリーンであること
- `npm test` / `npx tsc --noEmit` / `npm run lint` を実行し、★**実測値を控えたこと**★
  （2026-09-09 改訂時点の基準は **29ファイル / 587テスト**。古くなっている可能性があるので、
  ★**着手直前に自分で実行して実測すること**★）
- ★**コミットしない**★

---

## 段階1 — API契約を直す（`d-watch-api.yaml`）

★**`CLAUDE.md` §6-1「API契約を変えたくなったら、先に `d-watch-api.yaml` を直す」に従い、この段階を最初に置きます。**★
実装だけ変えて YAML と乖離させないためです（D-040 と同じ趣旨）。

### 1-1. `ErrorBreakdownItem.type` の enum を撤去する

現状（★2026-09-09 再実測。指示書K 作成時点は `:1189-1198` だったが、その後の他作業でファイル前方に
13行分挿入されズレた。以下は現在の実際の行番号★ `:1202-1211`）:

```yaml
      properties:
        type:
          type: string
          enum: [handle_up, lost_location, offline, brush_error, emergency_stop, low_battery, other]   # :1205
          example: handle_up
          description: >-                                                                              # :1207
            ハンドルアップ / LostLocation / オフライン / ブラシエラー / 緊急停止 / 充電不足 / その他。
            実DBの robot_error_reports.error_type は PUDU Cloud 由来で列挙が未定義のため、
            サーバ側で本7分類に寄せる。未知の値は other(D-057)                                          # :1210
        label: { type: string, example: "ハンドルアップ" }                                              # :1211
```

| 行 | 変更 |
|---|---|
| `:1205` | ★**`enum:` の行ごと削除**★。`type: string` だけを残す |
| `:1206` | `example` を ★**実在する値**★ に差し替える（例: `EmergencyStop`）。`handle_up` は当方の造語で実在しません |
| `:1207-1210` | ★**description が方針の逆を書いています**★（「サーバ側で本7分類に寄せる。未知の値は other(D-057)」）。**生値方針に書き換える** |
| `:1211` | `label` は ★**変更不要**★。自由文字列なので ★**生値の受け皿としてそのまま使えます**★（ただし example は `type` と揃えると分かりやすい） |
| `:1198-1201` | スキーマ本体の description に「type(英字enum)」とあります。★**enum ではなくなった**★ので直す |

★**着手直前に `grep -n "enum: \[handle_up" d-watch-api.yaml` で行番号を再確認すること**★。
他作業で再び行が増減している可能性があります。

**`:1207-1210` に書くべき趣旨**（文言は任せますが、次の3点を必ず含めること）:

1. ★**`robot_error_reports.error_type` の値をそのまま返す。サーバ側で変換しない**★
2. **PUDU Cloud 由来で列挙が未定義のため、推測でマッピングしない**（D-077）
3. **既知の値は `docs/handoff/analysis/error-type-mapping.md` に24件あるが、
   ★これは参考資料であって enum ではない★。実データで増減しうる**

### 1-2. `/reports/error-breakdown` の description（`:526-532`）

現状:

```yaml
      description: |
        7分類: ハンドルアップ / LostLocation / オフライン / ブラシエラー / 緊急停止 / 充電不足 / その他   # :527

        ## meta を返さない(一覧APIの例外)
        CLAUDE.md 9.3 は「一覧APIは必ず meta を返す」としているが、本エンドポイントは
        **例外として meta を返さない**。7分類固定でページングの余地がなく(limit パラメータも無い)、   # :531
        常に7件または0件を返すため。レスポンスは { items } のみ。                                      # :532
```

| 行 | 変更 |
|---|---|
| `:527` | 「7分類: …」の1行を ★**削除または生値方針の説明に置き換える**★ |
| `:529-532` | ★**下の「1-3」で決めた内容に書き換える**★ |

### 1-3. ★`meta` を返すかどうかを、この段階で決めること★

★**これは判断を要する箇所です。段階1 で決め、決めた理由を YAML と `docs/decisions.md` の両方に書いてください。**★

**何が問題か**: `:531` の「7分類固定でページングの余地がなく、常に7件または0件」が
★**`meta` を返さない唯一の根拠**★ になっています。生値では件数が固定されない（0〜N件）ので、
★**この根拠がそのまま崩れます**★。同じ文言が `app/api/reports/error-breakdown/route.ts:28-34` の
コメントにもあります（★こちらも直すこと★）。

| 案 | 内容 | 評価 |
|---|---|---|
| ★**A（当方の推奨）**★ | ★**`meta` は返さないまま、理由を書き直す**★。新しい理由: 「**エラー種別の内訳は集計結果であり全件返す**。ページングが要るほどの種別数にはならない見込み」 | **実装変更が最小**。フロント（`ReportsScreen.tsx:150`）も無変更で済む |
| B | `meta` を返すようにする | `CLAUDE.md` §9.3 には素直に従えるが、**`limit` も `page` も存在しないエンドポイントに `meta` だけ足すことになる**（`total` と `per_page` が常に同じ値になる） |

★**A を採る場合の必須条件**★:
★**実データで種別が数十に膨らむ可能性があります。段階6 ではなく、実データ接続後に件数を実測して再判断すること**★ を
**YAML の description と `docs/decisions.md` の両方に明記してください。**
（受領済みの `error_type` は24値ですが、★**これが実態と一致する保証はありません**★ ―
`error-type-mapping.md:29-30` が「実データに接続したら突き合わせて確認せよ」としています。）

> ★**注記**★: この指示書の当初案は「段階6 で件数を実測して再判断する」としていましたが、
> **段階6 の時点でも実データは無く、fixtures の件数しか測れません**。
> 実測できるのは ★**実DB接続後**★ です。**再判断の期限はそこに置いてください。**

### 段階1の完了条件

- `d-watch-api.yaml` に `handle_up` / `lost_location` / `offline` / `brush_error` /
  `emergency_stop` / `low_battery` / `other` が ★**enum として1つも残っていないこと**★
  （`grep -n "handle_up" d-watch-api.yaml` で確認）
- `meta` の扱いを A / B のどちらかに決め、★**理由を YAML に書いたこと**★
- `npx tsc --noEmit` / `npm run lint` / `npm test`（★この段階ではまだ全部通るはず★）
- ★**ここで1コミット**★（D-050）

---

## 段階2 — 型を緩める（`lib/types/index.ts`）

★**段階2〜4 は1コミットにまとめます。**★ 型を変えた瞬間にコンパイルが通らなくなり、
段階3・4 を終えるまで緑にできないためです。**逆に言えば、コンパイラが直す場所を教えてくれます。**

### 2-1. `ErrorType` をどうするか（★明示的に決めること★）

現状（`:177-185`）:

```ts
/** エラー種別7分類 */
export type ErrorType =
  | "handle_up"
  | "lost_location"
  | "offline"
  | "brush_error"
  | "emergency_stop"
  | "low_battery"
  | "other";
```

| 案 | 内容 | 評価 |
|---|---|---|
| ★**A（この指示書で採用する）**★ | ★**型ごと削除し、`ErrorBreakdownItem.type: string` にする**★ | **別名で残すと「7値のどれか」という誤解が残る**ため。`grep` で参照が消えたことも確認できる |
| B | `export type ErrorType = string;` の別名にする | 参照側の変更が少ないが、★**名前だけ残って中身が空になり、後任が union だと思い込む**★ |

★**A を採用します。**★ 参照は次の6ファイルだけです（実測）:

```
components/features/reports/ReportsScreen.tsx:20, 56
lib/fixtures/__tests__/reports.test.ts:30, 60, 62, 73, 292
lib/reports/query.ts:17, 38, 41, 270
lib/reports/__tests__/query.test.ts:504
lib/types/index.ts:197, 207
```

★**`lib/types/index.ts` の2箇所は 2026-09-09 に再実測した値**★（作成時点は `:178, 188` だったが、
その後の他作業でファイル前方に行が挿入され19行分ズレた。他5ファイルはズレていない）。

**すべて段階2〜5 で処理します。**

### 2-2. `ErrorBreakdownItem`（`:187-189`）

```ts
export interface ErrorBreakdownItem {
  type: ErrorType;   // :188  →  type: string;
  label: string;     // :189  ★変更不要★
```

★**`type: string` にしたうえで、次の趣旨のコメントを必ず添えること**★:

> `robot_error_reports.error_type` の生値。★変換しない★（D-077）。
> 既知の値の一覧は `docs/handoff/analysis/error-type-mapping.md`（★参考資料。enum ではない★）。

### 2-3. `status_1h` / `status_30m` のコメント（`:57-61`）

現状:

```ts
  /** ISO8601（+09:00） */
  scheduled_at: string;
  /** 待機中 / 充電中 / ハンドルアップ / 緊急停止 / オフライン / セキュリティ有効 */   // :59
  status_1h: string;                                                                  // :60
  status_30m: string;                                                                 // :61
```

- **型は `string` のままでよい**（変更不要）
- ★**`:59` のコメントが実態と合わなくなります**★。この6つは ★**当方が fixtures に書いた日本語**★ であって、
  実データから来た値ではありません（`lib/fixtures/clean-logs.ts:53-54` が唯一の生成元）
- **書き換える趣旨**: 「事前チェック時点のロボット状態。★実DB上の取得元・値の形式は未確定★
  （`robots.status` の46値は動作フェーズのみで、ハンドルアップ・オフラインを表現していない。D-077 項3）。
  現在の日本語6値は fixtures 由来の暫定値」

### 2-4. 変更不要なもの

| 箇所 | 理由 |
|---|---|
| `lib/types/index.ts:135`（★2026-09-09 実測。作成時点は `:116`★） `PrecheckItem.robot_status: string` | ★**すでに `string`**★。変更不要 |
| `lib/types/index.ts:310`（★2026-09-09 実測。作成時点は `:291`★） `DsAlertTypeFilter` の `"disconnected"` | ★**別物**★。段階7 の「触らないもの」参照 |

### 段階2の完了条件

★**この段階単独では緑になりません。**★ 段階4 まで進めてから検証・コミットします。
段階2 の時点で確認するのは次だけです。

- `grep -n "ErrorType" lib/types/index.ts` が **0件**であること

---

## 段階3 — ソート順を作り直す（`lib/reports/query.ts`）

### 3-1. 何が壊れるか

```ts
// lib/reports/query.ts:37-49
/**
 * `ErrorType` の定義順（`lib/types/index.ts` の union の順）。
 * 件数が同値のときの並びをこの順で安定させる（画面の内訳の順序がリロードで変わらないように）。
 */
const ERROR_TYPE_ORDER: readonly ErrorType[] = [
  "handle_up", "lost_location", "offline", "brush_error",
  "emergency_stop", "low_battery", "other",
];

// lib/reports/query.ts:270-278
/** 件数の降順。同値のときは `ErrorType` の定義順（`ERROR_TYPE_ORDER`）で安定させる */
export function sortErrorBreakdown(items: ErrorBreakdownItem[]): ErrorBreakdownItem[] {
  return [...items].sort((a, b) => {
    if (a.count !== b.count) return b.count - a.count;
    return ERROR_TYPE_ORDER.indexOf(a.type) - ERROR_TYPE_ORDER.indexOf(b.type);   // :276
  });
}
```

★**生値では `indexOf` が全て `-1` を返し、`-1 - (-1) === 0` になります。**★
つまり ★**同値時のタイブレークが効かなくなり、並びが不安定になります**★
（`Array.prototype.sort` は安定ソートですが、**入力の順序が変わればそのまま出力に出ます**）。
**画面の内訳の順序がリロードで変わる**、というのがこの関数が防いでいたことです。

### 3-2. どう直すか

| 項目 | 変更 |
|---|---|
| `:41-49` `ERROR_TYPE_ORDER` | ★**定数ごと削除**★ |
| `:17` の `ErrorType` import | ★**削除**★ |
| `:270` の docstring | ★**新しい規則に書き換える**★ |
| `:274-277` の比較 | ★**件数降順は維持。同値時のタイブレークを「`type` の文字列昇順」に変える**★ |

```ts
/**
 * 件数の降順。★同値のときは type（生値）の文字列昇順★ で安定させる。
 *
 * ★localeCompare を使わない★ — ロケール依存を持ち込まないため。
 * `error_type` は PUDU 由来の ASCII 識別子（`EmergencyStop` 等）で、
 * 単純な `<` 比較で決定的な順序が得られる。**画面の順序がリロードで変わらないことが目的**で、
 * 「人間にとって自然な並び」を作ることは目的ではない。
 */
export function sortErrorBreakdown(items: ErrorBreakdownItem[]): ErrorBreakdownItem[] {
  return [...items].sort((a, b) => {
    if (a.count !== b.count) return b.count - a.count;
    if (a.type === b.type) return 0;
    return a.type < b.type ? -1 : 1;
  });
}
```

★**`localeCompare` を使わない理由をコメントに残すこと**★（`CLAUDE.md` 全社ルール7「関数コメント必須」）。

### 3-3. テストを書き直す（`lib/reports/__tests__/query.test.ts`）

`:504-517`:

```ts
  it("件数が同値なら ErrorType の定義順で安定させる", () => {
    const items: ErrorBreakdownItem[] = [
      breakdownRow({ type: "other", count: 4 }),
      breakdownRow({ type: "low_battery", count: 4 }),
      breakdownRow({ type: "lost_location", count: 4 }),
      breakdownRow({ type: "handle_up", count: 4 }),
    ];
    expect(sortErrorBreakdown(items).map((i) => i.type)).toEqual([
      "handle_up", "lost_location", "low_battery", "other",
    ]);
  });
```

★**このテストを新しい規則で書き直します。**★ 期待値表を先に作ってください（`CLAUDE.md` §9.4）。

| # | 入力（`type`, `count`） | 期待する並び | 何を固定するか |
|---|---|---|---|
| 1 | `Zebra:4` / `Apple:4` / `Mango:4` | `Apple, Mango, Zebra` | ★**同値は文字列昇順**★ |
| 2 | `Apple:1` / `Zebra:9` | `Zebra, Apple` | ★**件数降順が文字列順より優先**★ |
| 3 | `EmergencyStop:4` / `CleanWaterEmpty:4` | `CleanWaterEmpty, EmergencyStop` | ★**実在する値でも同じ規則**★ |
| 4 | ★**大文字小文字が混ざる**★ `apple:4` / `Apple:4` | ★**`Apple, apple`**★（ASCII 順で大文字が先） | ★**`localeCompare` と結果が変わる箇所。ロケール非依存を固定する**★ |
| 5 | ★**空文字**★ `"":4` / `Apple:4` | `"", Apple` | 空文字が来ても落ちない |
| 6 | 0件 | `[]` | 既存の `:519-521` を維持 |

★**#4 は必ず入れてください。**★ `localeCompare` に差し替えられたときに落ちるテストになります。

### 3-4. その他のテスト

`lib/reports/__tests__/query.test.ts:637-657`（`error-breakdown: monthly は week=0 の行だけ取り出す`）は
`breakdownRow({ type: "handle_up" })` を使っています。★**型が `string` になるのでコンパイルは通りますが、
存在しない値を使い続けるのは紛らわしい**★ので、`:61-71` の `breakdownRow` ヘルパの既定値ともども
★**実在する値に差し替えてください**★（例: `EmergencyStop` / `LowBatteryLevel`）。

```ts
// lib/reports/__tests__/query.test.ts:61-71（現状）
function breakdownRow(overrides: Partial<ErrorBreakdownItem>): ErrorBreakdownItem {
  return {
    type: "handle_up",
    label: "ハンドルアップ",   // ★日本語の分類名。禁止事項2 に該当する★
    count: 1,
    rate: 10,
  };
}
```

★**`label` にも `type` と同じ生値を入れること**★（段階5 の方針と揃える）。

### 段階3の完了条件

★**段階4 と合わせて検証します。**★ この段階で確認するのは:

- `grep -n "ERROR_TYPE_ORDER\|ErrorType" lib/reports/query.ts` が **0件**であること
- 期待値表の6ケースがテストに入っていること

---

## 段階4 — 画面の色分けを壊さない（`components/features/reports/ReportsScreen.tsx`）

### 4-1. 何が壊れるか

```tsx
// components/features/reports/ReportsScreen.tsx:56-64
const ERROR_TONE: Record<ErrorType, "ok" | "wn" | "ng" | "ac" | "pr"> = {
  handle_up: "wn", lost_location: "ng", offline: "ng", brush_error: "wn",
  emergency_stop: "ng", low_battery: "ac", other: "pr",
};

// :434-441
{errorBreakdown.map((item) => (
  <div key={item.type} className={styles.errorRow}>
    ...
      <ProgressBar percent={item.rate} tone={ERROR_TONE[item.type]} />
```

| # | 症状 |
|---|---|
| 1 | ★**`ErrorType` が消えると `Record<ErrorType, ...>` が型エラー**★ |
| 2 | ★**未知キーで `ERROR_TONE[item.type]` が `undefined` になり、`ProgressBar` に渡る**★。`ProgressBar` の `tone` は `tone = "ok"` のデフォルト引数（`components/ui/ProgressBar.tsx:17`）があるので**落ちはしません**が、★**意図しない色（ok＝緑）になります**★ |
| 3 | ★**`key={item.type}` は生値に重複があると React のキー衝突を起こす**★ |

### 4-2. どう直すか

| 行 | 変更 |
|---|---|
| `:20` | `ErrorType` の import を ★**削除**★ |
| `:56` | ★**`Record<string, ProgressBarTone>` にする**★ |
| `:57-63` | ★**キーを実在する値に差し替える**★（段階5 の fixtures と揃える）。日本語の分類名は入れない |
| `:439-441` | ★**`ERROR_TONE[item.type] ?? "pr"` のフォールバックを必ず付ける**★ |
| `:434` | ★**`key` を `` `${index}-${item.type}` `` などにする**★（インデックス併用） |
| `:105` | `useState<ErrorBreakdownItem[]>` は ★**変更不要**★（`ErrorType` を直接使っていない） |
| `:148-151` | ★**「7分類固定・meta 無し」前提のコメント**★。段階1-3 で決めた内容に合わせて書き換える |
| `:47-55` | `ERROR_TONE` の docstring。「7色に厳密な意味づけがある仕様は存在しない」は生きているが、★**7分類前提の記述を生値前提に書き換える**★ |

**`ProgressBarTone` は `components/ui/ProgressBar.tsx:3` で定義されていますが `export` されていません。**
★**`Record<string, "ok" | "wn" | "ng" | "ac" | "pr">` とインラインで書くか、`ProgressBarTone` を export するか、どちらかにしてください**★
（**インラインで書くほうが変更が小さい**。既存の `:56` もインラインです）。

★**フォールバックを `"pr"` にする理由をコメントに残すこと**★:

> ★**未知の `error_type` は必ず来る**★（生値方針。D-077）。既知の値だけ色を付け、
> **それ以外は情報寄りの `pr`（中立色）にフォールバックする**。
> ★`ok`（緑）にしないのは、未知のエラーを「正常」に見せないため★。

★**フォールバック先を `"ok"` にしないこと。**★ `ProgressBar` の既定値が `"ok"` なので、
**`?? "pr"` を書き忘れると自動的に緑になります。これは「未知のエラーを緑で表示する」ことを意味します。**

### 4-3. `key` の重複対策

★**生値に重複があると React のキー衝突が起きます。**★
実際には `sortErrorBreakdown` に渡る前の `selectErrorBreakdownRows` が
**重複を除去していないため、DB 側に同じ `error_type` の行が2つあればそのまま2件並びます。**

```tsx
{errorBreakdown.map((item, index) => (
  <div key={`${index}-${item.type}`} className={styles.errorRow}>
```

★**インデックス単独をキーにしないこと**★（並び替えで再マウントが起きる）。**組み合わせにしてください。**

### 段階4の完了条件

★**ここで段階2〜4 をまとめて検証します。**★

- `npx tsc --noEmit` ★**エラー0**★
- `npm run lint` ★**0**★
- `npm test` 全パス（★**テスト数が減っていないこと**★）
- `grep -rn "ErrorType" lib/ components/ app/` が **`lib/fixtures/__tests__/reports.test.ts` にしか残っていない**こと
  （段階5 で消します）
- ★**ここで1コミット**★（段階2〜4 をまとめて。D-050）

---

## 段階5 — fixtures を実データの形に寄せる（`lib/fixtures/reports.ts`）

### 5-1. `errorCounts` を実在する値にする（`:448-463`）

```ts
// lib/fixtures/reports.ts:448-463（現状）
/**
 * エラー種別7分類の件数（★件数降順★）。
 * ...
 */
const errorCounts: ReadonlyArray<Omit<ErrorBreakdownItem, "rate">> = [
  { type: "handle_up",      label: "ハンドルアップ",  count: 18 },
  { type: "offline",        label: "オフライン",      count: 12 },
  { type: "lost_location",  label: "LostLocation",    count: 9 },
  { type: "brush_error",    label: "ブラシエラー",    count: 7 },
  { type: "emergency_stop", label: "緊急停止",        count: 5 },
  { type: "low_battery",    label: "充電不足",        count: 3 },
  { type: "other",          label: "その他",          count: 2 },
];
```

★**`type` を実DBに実在する `error_type` の値に置き換えてください。**★

- **選ぶ元**: ★**`docs/handoff/analysis/error-type-mapping.md` の24値の「原文」**★
  （`:54-56`, `:69`, `:114-119`, `:128-131`, `:137-140`, `:150-152`, `:158-160`）
- **例**: `EmergencyStop` / `LostLocalization` / `LowBatteryLevel` /
  `RobotHardwareErrorToAdmin` / `CleanWaterEmpty` / `TrashFull` / `CanNotReach` など
- ★**日本語の分類名を `type` に入れないこと**★（禁止事項2）
- ★**同ファイルの「7分類」列と「推定される意味」列は使わないこと**★（禁止事項1）

### 5-2. ★`label` には `type` と同じ生値を入れる★

```ts
{ type: "EmergencyStop", label: "EmergencyStop", count: 18 },
```

★**これは「英語識別子が画面にそのまま出る」という見た目になります。**★
★**それが意図した挙動です。**★ 理由:

- **変換しない方針（D-077）なので、画面に生値が出るのが正しい**
- `label` を日本語にすると、それは ★**変換表そのもの**★ です（禁止事項1・2）
- ★**`d-watch-mock.jsx:751-757` は日本語ラベル（「ハンドルアップ」「LostLocation」等）を出しています。
  `CLAUDE.md` §5 では「モックが文言の正」ですが、★D-077 のユーザー指示が上位★ なので
  この1点はモックから意図的に外れます。★このことを `docs/decisions.md` に明記すること★

★**`label` を残す理由**★（消さないこと）: `d-watch-api.yaml:1211`（★2026-09-09 実測。作成時点は `:1198`★）の `label` は**自由文字列**で、
★**将来 TeamWorks から正式な表示名が来たときの受け皿**★になります。
**いま生値を入れておけば、受け皿ができた時点で `label` だけ差し替えれば画面が変わります。**

### 5-3. ★境界値のケースを2件足す★

`docs/fixtures.md` の方針（**境界値・異常系を先に仕込む**）に沿って、**次の2件を追加してください**。

| # | ケース | `type` の値 | 何を確認するか |
|---|---|---|---|
| 1 | ★**未知の値**★ | 受領24値に**無い**値（例: `UnknownErrorFromCloud`） | ★**`ERROR_TONE` のフォールバックが効くこと**★（段階4） |
| 2 | ★**空文字**★ | `""` | ★**画面が落ちないこと・ソートが落ちないこと**★（段階3 の期待値表 #5） |

★**空文字ケースは、画面上で `label` が空になり行が視覚的に潰れます。**★
**それが現状の挙動であることを、fixtures のコメントに明記してください**
（**直すには実データの値が要ります。ここでは直しません**）。

### 5-4. ファイル冒頭コメント（`:17-33`）

`:25` に「エラー種別7分類」とあります。★**「エラー種別（生値。未知の値・空文字を含む）」に書き換えること**★。

### 5-5. `docs/fixtures.md:129`

```
| エラー種別7分類 | ハンドルアップ / LostLocation / オフライン / ブラシエラー / 緊急停止 / 充電不足 / その他 |
```

★**この行を生値方針に書き換えること**★（**用意すべきケース**: 実在する値が複数 / **未知の値** / **空文字**）。

### 5-6. テストを書き直す（`lib/fixtures/__tests__/reports.test.ts`）

#### `:59-73` — コンパイル時の7種網羅強制

```ts
/**
 * `ErrorType` 7種の一覧。
 * `satisfies Record<ErrorType, true>` で**網羅**をコンパイル時に強制している
 * （型に種別を足したらこの配列も直さないとビルドが落ちる）。
 */
const ALL_ERROR_TYPES = Object.keys({
  handle_up: true, lost_location: true, offline: true, brush_error: true,
  emergency_stop: true, low_battery: true, other: true,
} satisfies Record<ErrorType, true>) as ErrorType[];
```

★**この `ALL_ERROR_TYPES` を丸ごと削除してください。**★
`:30` の `ErrorType` import も**削除**します。
★**網羅を強制する対象（閉じた union）が存在しなくなったため、この仕掛けは成立しません。**★

#### `:291-300` — 「7種を重複なく網羅」テスト

```ts
describe("fixtures/reports: エラー種別内訳", () => {
  test("ErrorType 7種を重複なく網羅している", () => {
    groupByPeriod(errorBreakdown).forEach((rows, key) => {
      const types = rows.map((row) => row.item.type);
      expect(new Set(types).size, `${key}: 種別が重複しています`).toBe(types.length);
      expect(new Set(types)).toEqual(new Set(ALL_ERROR_TYPES));   // ★:298 これを消す★
    });
  });
});
```

★**「重複が無いこと」だけを検証する形に書き換えてください。**★

- `:295-297`（重複チェック）は ★**そのまま残す**★。**同一期間内に同じ `type` が2行あるのは fixtures のバグ**です
- `:298`（網羅チェック）は ★**削除**★
- `test` の名前も「**同一期間内で種別が重複していない**」に変える

★**「N件ちょうど」という件数の期待値を新しく書かないこと。**★
**生値では件数が固定されません。件数を固定すると、fixtures にケースを足すたびに落ちるテストになります。**

#### `:302-` 「件数の降順に並んでいる」

★**変更不要**★（件数の降順は維持されます）。ただし段階3 で**同値時のタイブレークを変えた**ので、
★**fixtures に同値の組を作った場合は、文字列昇順になっていることも確認するテストを足すこと**★。

### 段階5の完了条件

- `grep -rn "ErrorType" lib/ components/ app/` が ★**0件**★
- `grep -rn "handle_up\|lost_location\|brush_error\|low_battery" lib/ components/ app/ d-watch-api.yaml` が ★**0件**★
- fixtures に ★**未知の値**★ と ★**空文字**★ のケースが**各1件**あること
- `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス
- ★**`USE_FIXTURES=true npm run dev` で `/reports` を開き、内訳に英語識別子がそのまま出ていることを目視確認する**★
  （★**空文字の行が潰れて見えることも確認**★。それが現状の挙動）
- ★**ここで1コミット**★

---

## 段階6 — 色分けの重複実装を1本化する

### 6-1. 何が起きているか

★**同一ロジックが2箇所にコピーされています。**★

```ts
// lib/theme-status.ts:1-12（ファイル全体。12行しかありません）
import { THEME_TOKENS, type ThemeName } from "@/lib/theme";

export function statusColor(theme: ThemeName, status: string): string {
  const c = THEME_TOKENS[theme];
  if (status === "待機中") return c.ok;
  if (status === "充電中") return c.pr;
  if (status === "ハンドルアップ" || status === "緊急停止" || status === "オフライン") {
    return c.ng;
  }
  if (status === "セキュリティ有効") return c.ac;
  return c.txD;                                          // ★フォールバック＝グレー★
}
```

```ts
// components/features/alerts/AlertsScreen.tsx:29-38
/** モックの stColor() と同じ色分け（d-watch-mock.jsx） */
function robotStatusColor(c: ThemeTokens, status: string): string {
  // ★上と完全に同じ分岐★
  return c.txD;
}
```

**どちらも日本語6文字列の完全一致で色を決め、★それ以外は `c.txD`（グレー）にフォールバック★します。**
★**生値方針では、実データで全部グレーになります。例外もテスト失敗も出ない「静かな劣化」です。**★

### 6-2. どう直すか

| # | 作業 |
|---|---|
| 1 | ★**`AlertsScreen.tsx:29-38` の `robotStatusColor` を削除し、`lib/theme-status.ts` に一本化する**★ |
| 2 | `AlertsScreen.tsx:167` の呼び出しを `statusColor(theme, item.robot_status)` に差し替える |
| 3 | ★**シグネチャの差に注意**★。`statusColor(theme: ThemeName, status)` に対し `robotStatusColor(c: ThemeTokens, status)` は**トークンを受け取ります**。`AlertsScreen` は `theme` を持っているか確認し、無ければ `useTheme()` から取る（`CleanLogTable.tsx:177` が前例） |
| 4 | ★**フォールバックがグレーであることを関数の docstring に明記する**★（`CLAUDE.md` 全社ルール7「関数コメント必須」） |
| 5 | ★**マッピングを `lib/config/` の設定値に外出しする**★（下記 6-3） |

★**触ってはいけない同名関数があります。**★
`components/features/reports/FacilityPerformanceTable.tsx:129` にも `statusColor` がありますが、
★**引数が `FacilityStatus`（閉じた union）で、まったくの別物です。触らないこと。**★

### 6-3. ★マッピングを `lib/config/` に外出しする★

★**この段階では「グレーになること」を直しません。**★
★**直すには実データの値が要ります。推測でマッピングを増やすのは `CLAUDE.md` §7 の禁止事項です。**★

代わりに ★**実データが判明したときに1箇所追記するだけで直せる形にします。**★

```ts
// lib/config/index.ts に追加するイメージ（キー名・構造は任せます）
/**
 * 事前チェックのロボット状態（`status_1h` / `status_30m` / `robot_status`）の色分け。
 *
 * ★実DB上の値の形式は未確定です★（D-077 項3）。
 *   `robots.status` の46値は動作フェーズのみで、ハンドルアップ・オフラインを表現していません。
 *   下の日本語6値は ★fixtures 由来の暫定値★ で、実データからは来ません。
 *
 * ★ここに無い値はすべてグレー（`c.txD`）にフォールバックします★ ―
 *   例外もテスト失敗も出ないので、実データ接続後は
 *   ★実際に出現する値を突き合わせて、この表に追記してください★。
 */
export const ROBOT_STATUS_TONES: Readonly<Record<string, "ok" | "pr" | "ng" | "ac">> = {
  待機中: "ok",
  充電中: "pr",
  ハンドルアップ: "ng",
  緊急停止: "ng",
  オフライン: "ng",
  セキュリティ有効: "ac",
};
```

★**日本語のキーが残ることについて**★: 禁止事項2 は「**日本語の分類名をコードに書くな**」ですが、
これは ★**`error_type` の変換表を作るな**★ という意味です。ここは**変換表ではなく色分けの設定値**で、
★**設定値として外出しすることは `CLAUDE.md` §7（閾値のハードコード禁止）が求めている形**★です。
**それでも、この日本語6値が fixtures 由来の暫定値であることをコメントに必ず書いてください。**

### 6-4. テスト

★**`lib/theme-status.ts` にはテストがありません。**★ この段階で**最低限を足してください**。

| # | 何を固定するか |
|---|---|
| 1 | 設定値にあるキーは**対応する色**を返す |
| 2 | ★**設定値に無いキーはグレー（`c.txD`）を返す**★（実データで起きること） |
| 3 | ★**空文字もグレーを返す**★ |
| 4 | ライト/ダーク両方のテーマで**それぞれのトークンが返る** |

### 段階6の完了条件

- `grep -rn "robotStatusColor" components/ lib/` が ★**0件**★
- `lib/theme-status.ts` が ★**`lib/config/` の設定値を読んでいること**★
- ★**フォールバックがグレーであることが docstring に書かれていること**★
- `lib/__tests__/theme-status.test.ts`（またはそれに準ずる場所）に**4ケース**があること
- `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス（★**テスト数は増えるはず**★）
- ★**ここで1コミット**★

---

## 段階7 — D-Sec の `detail` を落とす（D-077 項4 の残作業）

### 7-1. なぜ落とすのか

★**TeamWorks 回答（D-065 Q1-3）で「アラート詳細文は存在しない」と確定済み**★ですが、
★**未実施のまま残っています**★（`docs/handoff.md:673-674`。★2026-09-09 実測。作成時点は `:652-653`★）。

> | 2 | **F-07 D-Secアラートの「種別」列を落とす** | 制御盤のアラートは**「生死報告がない」の1種類だけ** |
> | 3 | **同じく「詳細」列も落とす** | 種別が1つしかないため詳細文が存在しない |

★**この指示書で落とすのは「詳細」（`detail`）だけです。**★ 「種別」は下記 7-3 の理由で触りません。

**現在 fixtures に入っている12件の `detail`（「清掃終了後もセキュリティが復帰していない」等）は
★当方が書いた作文★で、DBには対応する値がありません。**

### 7-2. 落とすもの

| ファイル | 箇所 | 内容 |
|---|---|---|
| `d-watch-api.yaml` | `:1319`（★2026-09-09 実測。作成時点は `:1306`★） | `detail: { type: string, example: "清掃終了後もセキュリティが復帰していない" }` を**削除** |
| `lib/types/index.ts` | `:327`（★2026-09-09 実測。作成時点は `:308`★） | `DsAlert.detail: string` を**削除**（`DsAlert` は `:314-332`。作成時点は `:295-313`） |
| `lib/fixtures/ds-alerts.ts` | `:28, 42, 56, 70, 87, 101, 115, 129, 143, 157, 171, 185` | ★**12件すべての `detail` 行を削除**★ |
| `lib/ds-alerts/__tests__/query.test.ts` | `:20` | テストヘルパの `detail` を**削除** |
| `components/features/ds-alerts/DsAlertsScreen.tsx` | `:160` | `<div className={styles.detailLine}>{item.detail}</div>` を**削除** |
| `components/features/ds-alerts/DsAlertsScreen.module.css` | `:102` | `.detailLine` が**未使用になる**。**削除する** |

★**`.detail` の参照はリポジトリ全体で `DsAlertsScreen.tsx:160` の1箇所だけです**★（実測）。
**型から消せばコンパイラが漏れを教えてくれます。**

★**`memo` と混同しないこと**★。`DsAlert.memo`（`lib/types/index.ts:331`。★2026-09-09 実測。作成時点は `:312`★、
表示は `DsAlertsScreen.tsx:164`）は ★**残します**★。運用担当の対応メモで別物です。
（★行番号は着手直前に `grep -n "detail\|memo" lib/types/index.ts` で再確認すること★）

### 7-3. ★触らないもの（重要）★

| 対象 | 理由 |
|---|---|
| ★**`DsDevice.connection_status`**★ | ★**すでに2状態（`ok`/`error`）**★。**指示書G / D-065 / D-076 で完了済み**。触らないこと |
| ★**`DsAlert.alert_type`**★（`lib/types/index.ts:316`、`d-watch-api.yaml:1307-1310`。★2026-09-09 実測。作成時点は `:297` / `:1294-1297`★） | `security_off` は ★**`security_statuses` 由来でエラーではありません**★（D-067）。**制御盤のアラートではないので「2状態化」の対象外**です |
| ★**`DsAlert.resolution_status`**★（`:328`、`d-watch-api.yaml:1320-1323`。★2026-09-09 実測。作成時点は `:309` / `:1307-1310`★） | ★**`control_error_histories.status`（0/1/2）に直結する運用の対応状況**★（未対応/対応中/対応済み）。**エラーの種別ではありません。2状態化の対象外** |
| ★**`DsAlertType` / `DsAlertTypeFilter` の `"disconnected"`**★ | ★**`DsConnectionStatus` とは別物**★。`docs/handoff/指示書G_D-Secログの備考入力と2状態化.md:192-194` に「**一括置換すると壊れる**」という警告があります。★**着手前に必ず読むこと**★ |

★**「種別」列そのものを落とすかどうか（`docs/handoff.md:673`。★2026-09-09 実測。作成時点は `:652`★）は、この指示書の範囲外です。**★
`alert_type` は `security_off` と `disconnected` の2種類あり、
★**`security_off` が制御盤由来でない以上、「1種類しかない」という前提が成立しません**★。
**別途ユーザー判断が要ります。段階9 の記録にその旨を書いてください。**

### 段階7の完了条件

- `grep -rn "detail" lib/fixtures/ds-alerts.ts lib/types/index.ts components/features/ds-alerts/` が ★**0件**★
  （`details`（エラーレスポンスの `details`）に誤ヒットしないよう `\.detail\b` で確認すること）
- `grep -n "detail:" d-watch-api.yaml` に `DsAlert` の分が残っていないこと
- `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス
- ★**`USE_FIXTURES=true npm run dev` で `/ds-alerts` を開き、詳細行が消えて崩れていないことを目視確認**★
- ★**ここで1コミット**★

---

## 段階7-B — B2 の発火条件を設定値に外出しし、暫定値 `Handrail Lifted` を入れる

★**根拠: D-078 項2（2026-09-08）／ ★D-082（2026-09-08）★。冒頭の「B2 / B3 の扱い — 承認済み」を先に読むこと。**★

★**この段階は「段階7 と段階8 の間に後から挿入した段階」です。**★
既存の段階0〜9 の番号は ★**繰り下げていません**★（他文書からの参照がズレるため）。

★**この段階でだけ `lib/judge/advice.ts` と `lib/config/advice-rules.ts` を触ります。**★

### ★★この段階の位置づけ — 先に読むこと（D-082）★★

★**この段階の目的は「暫定値を入れて動く形にすること」であって、「正しく発火させること」ではありません。**★

**ユーザー判断（原文。2026-09-08 / D-082）**:

```
アドバイスは連続で清掃失敗したときのものですね、それなら一旦そのままの文字列で出して、後々修正する形にします
```

| # | 位置づけ |
|---|---|
| 1 | ★**正しい値は実データを見てから決めます**★（参照用アカウントで `robot_error_reports.error_type` を数えた後。D-082 項5） |
| 2 | ★**したがって、この段階で `Handrail Lifted` が実在するか悩まないこと。**★ ★設定値に入れて先へ進んでください★ |
| 3 | ★**推測で別の値を足さないこと**★。★誤った原因を現場に伝えることになります★（`CLAUDE.md` §7） |
| 4 | ★**後で直すときは `lib/config/robot-status-match.ts` の1箇所を差し替えるだけで済みます。**★ ★これが設定値にした理由です★ |

#### ★アドバイスの主軸は R1〜R4 であり、B2/B3 が発火しなくても機能は成立します★

★**ユーザー原文の「アドバイスは連続で清掃失敗したときのもの」は正確ではありません。**★
★`CLAUDE.md` §4.5 の R1〜R4 は次のとおりです★:

| # | 条件 | ★連続を見るか★ |
|---|---|---|
| ★R1★ | ★**出発失敗（初回）**★ | ★**見ない（単日で発火）**★ |
| ★R2★ | ★**完了率×（10%未満）**★ | ★**見ない（単日で発火）**★ |
| R3 | 同一施設で2日以上連続（出発失敗 or △以下） | ★見る★ |
| R4 | 同一施設で3日以上連続（出発失敗 or △以下） | ★見る★ |

- ★**連続を見るのは R3 / R4 だけです。**★ ★R1 は初回の出発失敗で、R2 は単日の完了率×で発火します★
- ★**R1〜R4 は事前チェックのロボット状態文字列に一切依存しません。**★
  ★B2/B3 が黙っていてもアドバイス機能の主軸は全部動きます★
- ★**B1（完了率△でマップ更新を疑う）も完了率ベースなので影響を受けません**★
- ★**つまり、この段階を後回しにしても他の段階の完了を妨げません。**★
  ★段階0〜7 と段階8〜9 は、この段階の結果に依存しません★

#### ★★後回しにしてよい。ただし飛ばしてはいけない★★

| | |
|---|---|
| ★**してよいこと**★ | ★**段階の順序を後ろにすること**★（例: 段階8 の後、段階9 の前に回す）。★D-082 により優先度は「中」です★ |
| ★**してはいけないこと**★ | ★**この段階を実施せずに指示書K を完了扱いにすること**★ |

★**飛ばすと何が起きるか**★:
★現状の日本語完全一致（`status1h === "ハンドルアップ"`）が残ります★。
★これが生値方針（D-077 項1。DB から日本語は来ない）と組み合わさると、
B2/B3 は**例外もテスト失敗も出さずに永久に発火しなくなります**★。
しかも ★`lib/judge/__tests__/advice.test.ts` は日本語リテラルを直接渡しているのでテストは緑のまま★ です。
★**「静かに壊れる」状態が残ります。これが飛ばしてはいけない理由です。**★

★**スコープの扱いは変わりません**★ — ★D-039 により B1〜B3 は P1 スコープ内★であり、
Phase B の先送りではありません（D-077 項3 / D-082 項3）。

### 7B-1. ★`lib/config/robot-status-match.ts` を新規作成する（`index.ts` に足さないこと）★

| 項目 | 内容 |
|---|---|
| ファイル | ★**`lib/config/robot-status-match.ts`（新規）**★。ファイル名は例。★**独立ファイルであることが要件**★ |
| ★**`lib/config/index.ts` に足さないこと**★ | `index.ts` は ★**judge 系の閾値（`GRADE_THRESHOLDS` / `ESCALATION_THRESHOLDS` 等）**★ を持っています。ここに足すと ★**不要な依存が広がります**★。★**指示書I が `lib/config/allowed-ips.ts` を独立ファイルにしたのと同じ理由**★ |
| 中身 | B2 / B3 のマッチ文字列を ★**配列**★ として持つ |

★**先例を読むこと（指示書N・D-087。2026-09-09 実行済み）**★:
★**「レジストリ＋判定の純粋関数」という同じ構造が、対応機種の判定ですでに実装されています**★
（`lib/config/index.ts` の `ROBOT_MODELS` がレジストリ、`lib/format/robot-model.ts` の
`isSupportedModel()` が判定の純粋関数）。★**この段階7-B も同じ構造にすると、後任が2箇所で
同じ考え方を学ばずに済みます**★。★**ただし置き場所はこの指示書の指定どおり
`lib/config/robot-status-match.ts`（独立ファイル）のままでよく、`lib/config/index.ts` に
統合する必要はありません**★（上の「`index.ts` に足さないこと」の理由のとおり）。

```ts
// lib/config/robot-status-match.ts（構造・キー名は任せますが、下の趣旨は必ず満たすこと）

/**
 * 事前チェックのロボット状態（`status_1h` / `status_30m`）が
 * B2 / B3 のアドバイスを発火させる値の一覧。
 *
 * ★なぜ設定値なのか★
 *   値は ★暫定★ で、変わる可能性があります（D-078 項2。ユーザーが「暫定で変更のおそれはあります」と明言）。
 *   コードに直書きすると変更時に複数箇所を追うことになるため、★1箇所の差し替えで済む形★ にします
 *   （`CLAUDE.md` §7「閾値のハードコード禁止」と同じ趣旨）。
 *
 * ★マッチの規則★
 *   ★前後の空白を trim し、大小文字を無視して★ 比較します（`lib/judge/advice.ts`）。
 *   理由: `Handrail Lifted` は ★空白入り★ で、他の24値がすべて PascalCase（`EmergencyStop` 等）
 *   なのに対し形が違います。★`error_type` の値の形が1種類ではない可能性があり、
 *   表記ゆれで取りこぼす★ ためです（D-074 / `docs/handoff/analysis/error-type-mapping.md` の指摘）。
 *
 * ★空配列＝発火しない★
 *   既定を空にしているものは ★「実データで値が判明していない」★ という意味です。
 *   ★「静かに発火しなくなる」より「明示的に空」のほうが後から気づける★ ので、
 *   分岐ごと消さずに空配列を残してあります。
 */

/**
 * B2（ハンドルアップ検知）。
 *
 * ★`Handrail Lifted` は暫定値です★（D-078 項2）。
 *   ★受領済みの `error_type` 24値のリストには含まれていません★。
 *   ユーザーも「〜だと思いますが」と述べており確定ではありません。
 *   ★実データに接続できたら、`error_type` に実在するかを最優先で確認してください★。
 */
export const B2_HANDLE_UP_STATUSES: readonly string[] = ["Handrail Lifted"];

/**
 * B3（オフライン検知）。
 *
 * ★空＝発火しません★。実データ上どの値がオフラインを表すのかが ★未判明★ のためです。
 *   `error_type` の24値にも `robots.status` の46値にも該当がありません
 *   （46値は移動・充電・清掃・帰還の動作フェーズのみ。D-077 項3）。
 *   ★実データで値が判明したら、ここに足すだけで復活します★。
 *   ★推測で値を入れないこと★（`CLAUDE.md` §7）。
 */
export const B3_OFFLINE_STATUSES: readonly string[] = [];
```

★**日本語の値を入れないこと**★（禁止事項2）。現在 fixtures に入っている日本語6値は
★**当方が書いた暫定値**★ であって、実データから来た値ではありません。

### 7B-2. ★`lib/judge/advice.ts:79-85` を設定値とのマッチに置き換える★

現状（★行番号は実測★）:

```ts
// lib/judge/advice.ts:79-85
  // B2 / B3: 事前チェックのステータス由来
  if (status1h === "ハンドルアップ" || status30m === "ハンドルアップ") {   // :80-82
    matched.push("B2");
  }
  if (status1h === "オフライン" || status30m === "オフライン") {           // :83-85
    matched.push("B3");
  }
```

| # | 要件（★すべて満たすこと★） |
|---|---|
| 1 | ★**日本語の完全一致をやめ、`lib/config/robot-status-match.ts` の配列とのマッチにする**★ |
| 2 | ★**前後の空白を trim して比較する**★（`Handrail Lifted ` のような値を取りこぼさない） |
| 3 | ★**大小文字を無視して比較する**★（`handrail lifted` / `HANDRAIL LIFTED` でも発火する） |
| 4 | ★**マッチ配列が空のときは発火しない**★（B3 がこれに該当。★空配列に対して「何にでもマッチ」にしないこと★） |
| 5 | ★**`status1h` / `status30m` は `undefined` を取り得る**★（`AdviceInput` の `:38-41` が optional）。`undefined` で落ちないこと |
| 6 | ★**判定関数は純粋関数のまま保つこと**★（`CLAUDE.md` §9.4）。設定値は ★**引数で渡すか、モジュール先頭で `import` する**★。★関数内で I/O をしない★ |
| 7 | ★**ヘルパを1つ作り、B2 と B3 で共用する**★（同じ比較規則が2箇所にコピーされるのを避ける） |

```ts
// イメージ（実装は任せますが、上の7要件を満たすこと）
import { B2_HANDLE_UP_STATUSES, B3_OFFLINE_STATUSES } from "@/lib/config/robot-status-match";

/**
 * ロボット状態が発火条件にマッチするか。
 * ★前後の空白を trim し、大小文字を無視して比較する★（表記ゆれ対策。D-078 項2）。
 * ★マッチ配列が空なら常に false★（＝値が未判明のルールは発火しない）。
 */
function matchesStatus(
  statuses: readonly string[],
  ...values: Array<string | undefined>
): boolean {
  if (statuses.length === 0) return false;
  const normalized = statuses.map((s) => s.trim().toLowerCase());
  return values.some(
    (v) => v !== undefined && normalized.includes(v.trim().toLowerCase())
  );
}
```

★**`lib/judge/advice.ts:20-21` の docstring 表（`| B2 | ハンドルアップ検知 |`）も見直すこと**★。
**ルール名としての「ハンドルアップ検知」は残してよい**（これは ★文言★ であって変換表ではない）が、
★**「発火条件は `lib/config/robot-status-match.ts` の設定値。値は暫定（D-078 項2）」という趣旨を1行足すこと**★。

### 7B-3. ★`lib/config/advice-rules.ts` の `condition` を更新する★

★**行番号の訂正**★: D-078 項2 の影響ファイル表は `:110-129` と書いていますが、★**実測は `:103-122`**★ です
（B2 = `:103-112` / B3 = `:113-122`。`condition` の行は ★**`:111` と `:121`**★）。★**実測を正とすること**★。

```ts
// lib/config/advice-rules.ts:111（現状）
    condition: "1時間前または30分前のステータスがハンドルアップ",
// lib/config/advice-rules.ts:121（現状）
    condition: "1時間前または30分前のステータスがオフライン",
```

| 行 | 変更 |
|---|---|
| `:111`（B2） | ★**実際のマッチ対象を書く**★。趣旨: 「1時間前または30分前のステータスが `lib/config/robot-status-match.ts` の B2 リストに一致（★暫定値 `Handrail Lifted`★。trim・大小文字無視）」 |
| `:121`（B3） | ★**実際のマッチ対象を書く**★。趣旨: 「1時間前または30分前のステータスが B3 リストに一致。★**現在リストは空のため発火しない**★（値が未判明。D-078 項2）」 |
| `:108` `title` / `:118` `title` | ★**変更不要**★（「ハンドルアップ検知」「オフライン検知」は ★運用設定で編集可能な文言★ であって変換表ではない） |
| `:8-10` | ★**「発火条件はここではなく `lib/judge/advice.ts` にある／文言を変えてもテストは壊れない」という記述は維持すること**★。★**`condition` は説明用の文字列で、これを書き換えても発火条件は変わりません**★。この分離は意図的です |

★**`condition` を書き換えても実際の判定は1ミリも変わりません。**★
★**逆に言えば、`condition` だけ直して `advice.ts` を直し忘れると、説明と実装がズレます。**★
**7B-2 と 7B-3 は必ずセットで行ってください。**

### 7B-4. ★`lib/fixtures/clean-logs.ts` を更新する★

★**ここが `status_1h` / `status_30m` の唯一の生成元です**★（`prismaSource.cleanLogs()` は
`lib/db/index.ts:150-151`（★2026-09-09 実測。作成時点は `:141-142`★）で `notImplemented`。★**実データ由来の値は一度も流れたことがありません**★）。

| 行 | 現状 | 変更 |
|---|---|---|
| `:53-54` | `status_1h: input.status_1h ?? "待機中"` / `status_30m: input.status_30m ?? "待機中"` | ★**`?? "待機中"` の日本語デフォルトが埋め込まれています**★。★**この2行に「日本語6値は fixtures 由来の暫定値であり、実データから来る値ではない」旨のコメントを足すこと**★。★**既定値そのものは変えない**★（変えると `boundaryCases` 10件を含む大量の行の見た目が変わり、この指示書の範囲を超えます） |
| `:109` | `status_1h: "ハンドルアップ", status_30m: "ハンドルアップ"` | ★**`"Handrail Lifted"` に変える**★（B2 が発火することを保つため。★これを変えないと B2 のテストが fixtures 側から支えられなくなります★） |
| `:116-118` | `status_1h: "オフライン", status_30m: "オフライン"`（3行） | ★**「オフライン」のまま残す**★（下記の理由） |

#### ★`:116-118` の「オフライン」をどうするか — ★残す★（この指示書で明示的に決定）★

| 案 | 内容 | 採否 |
|---|---|---|
| ★**A（採用）**★ | ★**`"オフライン"` のまま残し、「B3 が発火しないこと」を検証するケースとして使う**★ | ★**採用**★ |
| B | 実DBに存在する値に置き換える | ★**採れません**★。★**オフラインを表す値が実データに存在するかどうかすら未判明**★ です（24値にも46値にも該当なし）。★推測で値を書くのは `CLAUDE.md` §7 の禁止事項★ |

★**A を採用します。**★ `:116-118` の3行に ★**次の趣旨のコメントを必ず添えること**★:

> ★この「オフライン」は fixtures 由来の暫定値で、実データには存在しません★（D-077 項3 / D-078 項2）。
> ★B3 のマッチ配列（`lib/config/robot-status-match.ts` の `B3_OFFLINE_STATUSES`）は空なので、
> この行では B3 は発火しません★。★それが現在の期待挙動です★。
> 実データでオフラインを表す値が判明したら、この値と設定値の両方を差し替えてください。

★**この3行の `escalationCases` は R3 / R4（連続日数）の検証が本来の目的です。**★
★**B3 が発火しなくなっても R3 / R4 のテストは影響を受けません**★（連続日数ベースのため）。
ただし ★**`lib/judge/__tests__/advice.test.ts:151-162` の `toEqual(["R3", "R4", "B3"])` は
`["R3", "R4"]` に変わります**★（7B-5 参照）。

### 7B-5. ★`lib/judge/__tests__/advice.test.ts` を書き直す★

★**現状のテストは日本語リテラルを関数に直接渡しているため、実装を変えてもテストが緑のまま通ってしまいます。**★
★**これが「静かに壊れる」の入口です。必ず書き直してください。**★

★**行番号の訂正**★: D-078 項2 の影響ファイル表は `:133-176` と書いていますが、★**実測は次のとおり**★:

| 実測行 | 内容 |
|---|---|
| `:132-136` | `it("B2: 1時間前がハンドルアップ")` |
| `:138-142` | `it("B2: 30分前がハンドルアップ")` |
| `:144-147` | `it("B3: オフライン検知")` |
| `:151-162` | `it("3日連続のオフライン出発失敗は R3 / R4 / B3 が並ぶ")` → ★`:161` の `toEqual(["R3", "R4", "B3"])`★ |
| `:164-176` | `it("初回のハンドルアップ出発失敗は R1 / B2")` → ★`:174` の `toEqual(["R1", "B2"])`★ |

#### ★期待値表（先に作ること。`CLAUDE.md` §9.4）★

| # | 入力 | 期待 | 何を固定するか |
|---|---|---|---|
| 1 | `status1h: "Handrail Lifted"` | ★`B2` を含む★ | B2 の暫定値で発火する |
| 2 | `status30m: "Handrail Lifted"` | ★`B2` を含む★ | 30分前でも発火する |
| 3 | ★**前後に空白**★ `status1h: "  Handrail Lifted  "` | ★`B2` を含む★ | ★**trim して比較している**★ |
| 4 | ★**大小文字が違う**★ `status1h: "handrail lifted"` / `"HANDRAIL LIFTED"` | ★`B2` を含む★ | ★**大小文字を無視して比較している**★ |
| 5 | `status1h: "ハンドルアップ"`（★旧値★） | ★`B2` を**含まない**★ | ★**日本語完全一致が本当に消えたこと**★。★これが無いと書き換え漏れに気づけません★ |
| 6 | `status1h: "EmergencyStop"` | `B2` も `B3` も**含まない** | 無関係な値では発火しない |
| 7 | ★**B3: マッチ配列が空**★ `status1h: "オフライン"` / `status30m: "オフライン"` | ★`B3` を**含まない**★ | ★**空配列なら発火しない**★（★これが今回の核心★） |
| 8 | `status1h: undefined` / `status30m: undefined` | `B2` も `B3` も含まない・★**例外を投げない**★ | optional 引数で落ちない |
| 9 | `status1h: ""`（空文字） | `B2` も `B3` も含まない | 空文字でマッチしない |
| 10 | 3日連続 + `status1h/30m: "オフライン"` | ★`toEqual(["R3", "R4"])`★ | ★`:151-162` の書き換え後の完全一致★ |
| 11 | 初回出発失敗 + `status1h/30m: "Handrail Lifted"` | ★`toEqual(["R1", "B2"])`★ | ★`:164-176` の完全一致（値だけ差し替え）★ |

★**#3 / #4 / #7 は必ず入れてください。**★
★**#7 が無いと、B3 を「何にでもマッチ」に実装しても気づけません。**★
★**#5 が無いと、日本語一致を消し忘れても気づけません。**★

★**テスト内でマッチ値をハードコードすることについて**★:
★**#1〜#4 はリテラル `"Handrail Lifted"` を直接書いてかまいません**★。
★**テストは「その値で発火すること」を固定するのが仕事**★だからです
（設定値を import して回すと、設定値を空にしたときにテストごと空回りして緑になります）。
★**ただし、暫定値であることをテストファイルのコメントに書くこと**★
（値が変わったときにここも直す必要があるため）。

#### ★#7（B3）のテストの書き方に注意★

★**「B3 が発火しない」テストは、設定値が空である限り何を渡しても通ります。**★
★**それでも入れる価値があります**★ — 実データで `B3_OFFLINE_STATUSES` に値を足した瞬間に
★**このテストが落ち、「B3 の期待挙動が変わった」ことを実装者に知らせる**★ からです。
★**その旨をテストのコメントに書いてください**★（★落ちたら値を足した合図であり、バグではない★）。

### 7B-6. ★他のファイルは触らないこと★

| 対象 | 理由 |
|---|---|
| `lib/theme-status.ts` / `components/features/alerts/AlertsScreen.tsx` の日本語6値 | ★**段階6 の担当**★。★色分け★の設定値であって発火条件ではありません。★段階7-B で二重に触らないこと★ |
| `lib/fixtures/prechecks.ts:27, 90` の `robot_status: "ハンドルアップ"` | ★**B2/B3 の発火に関与しません**★（`PrecheckItem.robot_status` は事前チェック画面の表示用）。★この指示書の範囲外★ |
| `lib/logger/__tests__/smoke.test.ts:139` の `status_30m: "ハンドルアップ"` | ★**ロガーのスモークテスト**★。判定に関与しません。★触らない★ |
| `lib/fixtures/clean-logs.ts:139-141` のコメント内の日本語 | ★**§2.3 の説明コメント**★。★実データの値ではないことが分かる形なら残してよい★ |
| `lib/db/` の `prismaSource` | 禁止事項4。`notImplemented` のまま |

### 段階7-B の完了条件

- ★**`lib/config/robot-status-match.ts`（新規）が存在し、`lib/config/index.ts` から export されていないこと**★
  （`grep -n "robot-status-match" lib/config/index.ts` が ★**0件**★）
- ★**`B2` のマッチ配列が `["Handrail Lifted"]`、`B3` のマッチ配列が `[]` であること**★
- ★**既定を空にする理由（実データで値が判明していない／「静かに発火しなくなる」より「明示的に空」）がコメントに書かれていること**★
- `grep -n "ハンドルアップ\|オフライン" lib/judge/advice.ts` が ★**発火条件の分岐に残っていないこと**★
  （docstring の表のルール名として残るのは可）
- `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス
- ★**`lib/judge/__tests__/advice.test.ts:151-162` と `:164-176` の `toEqual` による完全一致テストが
  新しい値で通ること**★（★`["R3", "R4"]` / `["R1", "B2"]`★）
- ★**期待値表の11ケースがテストに入っていること**★（★特に #3 / #4 / #5 / #7★）
- ★**テスト数が増えていること**★（段階6 に続き、ここでも増えるのが期待値）
- ★**ここで1コミット**★（D-050）

---

## 段階8 — テストを通す（最終確認）

★**段階ごとに実行しているはずなので、ここは最終確認です。**★

```bash
npx tsc --noEmit
npm run lint
npm test
```

### 見るべきテスト

| ファイル | 内容 |
|---|---|
| `lib/reports/__tests__/query.test.ts:490-521` | ソート（段階3。★期待値表の6ケース★） |
| `lib/reports/__tests__/query.test.ts:637-657` | `week=0` の取り出し（`breakdownRow` の既定値を差し替えた分） |
| `lib/fixtures/__tests__/reports.test.ts` | ★**全般**★（段階5。`ALL_ERROR_TYPES` の削除が効いている） |
| `lib/ds-alerts/__tests__/query.test.ts` | `detail` の削除（段階7） |
| `lib/__tests__/theme-status.test.ts`（★新規★） | 色分けのフォールバック（段階6） |
| ★`lib/judge/__tests__/advice.test.ts:118-176`★ | ★**B2/B3 の発火条件（段階7-B。期待値表の11ケース）**★。★`:161` `toEqual(["R3", "R4"])` / `:174` `toEqual(["R1", "B2"])` が新しい値で通ること★ |

### ★踏まないでほしい地雷★

| # | 内容 |
|---|---|
| 1 | ★**`cwd` のドライブレターは大文字 `C:\` でなければ全く動きません**★。`vitest run` の出力に `RUN v4.x C:/Users/...` と大文字で出ているか確認すること |
| 2 | ★**`--reporter=basic` は Vitest 4 で削除済みです**★。使うと ★**ロードに失敗しても exit 0 になり、壊れていることに気づけません**★。素の `npm test` を使うこと |
| 3 | ★**テスト数が減っていないこと**★。基準は2026-09-09 改訂時点で **29ファイル / 587テスト**（★着手直前の実測値を基準にすること★）。★**段階6 と段階7-B で増えるのが期待値**★ |

### 段階8の完了条件

- 上の3コマンドがすべて通ること
- ★**テスト数が着手前の実測値から減っていないこと**★（★段階6 / 段階7-B の分だけ増えているはず★）
- ★**コミットしない**★（段階1〜7-B で済んでいるはず。差分が出たならどの段階の漏れかを特定してその段階に足す）

---

## 段階9 — 記録と push

### 9-1. `docs/decisions.md` に **D-088** を追記する

★**着手直前に `grep -n "^### D-0" docs/decisions.md` で最終番号を確認すること**★
（★**2026-09-09 改訂時点の最終は D-087**★。★当初この指示書は「D-079 から」としていましたが、
D-079〜D-087 は指示書L・M・N の実行（D-084 / D-086 / D-087）や他作業で採番済みなので
★**D-088 に繰り上げました**★。
★2026-09-07 に `D-068` の二重採番が実際に起きています★）。

**記録すべきもの**:

| # | 内容 |
|---|---|
| 1 | 指示書K を実行した（D-077 項1・項2・項4 の実装） |
| 2 | `ErrorType` union を ★**別名で残さず削除した**★ 理由（別名は「7値のどれか」という誤解を残す） |
| 3 | ★**同値時のタイブレークを文字列昇順にし、`localeCompare` を使わなかった**★ 理由（ロケール依存を持ち込まない） |
| 4 | ★**`meta` を返すかどうかの判断（段階1-3）と、その根拠**★。A を採ったなら ★**実DB接続後に件数を実測して再判断する**★ ことも書く |
| 5 | ★**`label` に生値を入れ、画面に英語識別子がそのまま出ることが意図した挙動である**★こと。**`d-watch-mock.jsx:751-757` の日本語ラベルから意図的に外れた**こと（`CLAUDE.md` §5 の「モックが文言の正」に対する明示的な例外） |
| 6 | fixtures に ★**未知の値・空文字**★ のケースを足したこと |
| 7 | 色分けを `lib/theme-status.ts` に一本化し、★**マッピングを `lib/config/` に外出しした**★こと。★**フォールバックがグレーであること・実データ接続後に追記が要ること**★ |
| 8 | `DsAlert.detail` を落としたこと（D-065 Q1-3 の残作業）。★**`alert_type` の「種別」列を落とすかは別途ユーザー判断が要る**★こと（`security_off` が制御盤由来でないため「1種類だけ」の前提が成立しない） |
| 9 | ★**段階7-B（B2/B3）を実施したこと**★。★**D-078 項2 の承認に基づく**★こと。`lib/config/robot-status-match.ts` を ★**独立ファイル**★ にした理由（`index.ts` は judge 系の閾値を持つため。指示書I の `allowed-ips.ts` と同じ理由） |
| 10 | ★**`Handrail Lifted` は暫定値である**★こと。★**受領済みの24値リストに含まれていない**★／★**ユーザーも「〜だと思いますが」と述べており確定ではない**★／★**「暫定で変更のおそれはあります」とユーザーが明言している**★。★**この3点を必ず書くこと**★ |
| 11 | ★**B3 のマッチ配列は空のままにした**★こと（値が未判明。★発火しないのが現在の期待挙動★）。`lib/fixtures/clean-logs.ts:116-118` の「オフライン」を ★**B3 が発火しないことの検証ケースとして残した**★こと |
| 12 | ★**マッチは trim + 大小文字無視にした**★こと。理由（`Handrail Lifted` は空白入りで、他24値の PascalCase と形が違う。★`error_type` の値の形が1種類ではない可能性★。D-074） |
| 13 | ★**申し送り: 実データに接続できたら、`error_type` に `Handrail Lifted` が実在するかを最優先で確認する**★。★**参照用アカウントは 2026-09-08 に受領済み（D-078 項1）**★。★**ただしホスト名が未受領のため、現時点ではまだ接続できない**★ |
| 14 | 実測値（`npm test` のファイル数・テスト数 / `npx tsc --noEmit` / `npm run lint`） |
| ★15★ | ★**D-082 を踏まえ、「B2/B3 に入れた値は暫定であり、正しく発火させることは段階7-B の目的ではなかった」ことを明記する**★。★「一旦そのままの文字列で出して、後々修正する」というユーザー判断（D-082 項1）に沿った実装であること★／★実データで `error_type` を数えた後に設定値を差し替える予定であること（D-082 項5）★／★見つからなければ `prismaSource実装前チェックリスト.md` §H の保留質問を TeamWorks に送ること★ |
| ★16★ | ★**アドバイスの主軸 R1〜R4 は B2/B3 の不発火の影響を受けないこと**★（★R1 は初回の出発失敗、R2 は単日の完了率×で発火する。連続を見るのは R3/R4 だけ★。`CLAUDE.md` §4.5）。★**したがって B2/B3 の優先度は「中」だが、スコープは P1 のまま（D-039 / D-082 項3）**★ |
| ★17★ | ★**`prismaSource実装前チェックリスト.md` §B / §H の B2・B3 の記載は消さないこと**★。★「静かに発火しなくなる」性質は優先度が下がっても変わらないため★（D-082 項3） |

### 9-2. ★既存の誤記を直す★

| 箇所 | 内容 |
|---|---|
| ★`docs/decisions.md:1350`★ | 「7分類（`d-watch-api.yaml:527` / **`:1160`**）」の ★**`:1160` は誤り**★。**実体は `:1205`**（★2026-09-09 実測。指示書K 作成時点は `:1192` だったが、その後の他作業で13行分ズレた。★着手直前に `grep -n "enum: \[handle_up" d-watch-api.yaml` で再確認すること★）。`:1160` は `ContinuousProblem.consecutive_problem_days` ではなく本改訂時点では `FacilityPerformance.status` の enum で、いずれにせよ本件とは無関係。★**この段階では enum 自体が撤去済みなので、「かつて `:1205` にあった enum（★D-088★ で撤去）」と書き換えるのが正確**★ |
| `docs/handoff/analysis/error-type-mapping.md:40-46` | ★**すでに訂正済み**★（`:42-45` に訂正の記述あり）。**ただし `:40` は「`:1192` が正」と現在形で書いているので、★enum が撤去されたことを追記する★** |
| `docs/fixtures.md:129` | 段階5-5 で対応済みのはず。**漏れていればここで** |

### 9-3. `docs/handoff.md` に追記する

- ★**末尾に追記を1つ追加**★
- ★**冒頭の「最新の追記」ポインタ（`:6-8`）も更新すること**★
- ★**`:652-653` の「仕様が変わるもの」の #3（詳細列を落とす）を「対応済み」に更新**★。
  **#2（種別列を落とす）は ★未対応のまま★ で、理由（`security_off` が制御盤由来でない）を書き足す**
- ★**段階7-B の申し送りを書く**★: ★`Handrail Lifted` は暫定値★／★B3 は空のまま（発火しない）★／
  ★実データ接続後に `error_type` への実在を最優先で確認する★／
  ★参照用アカウントは受領済みだがホスト名が未受領で接続できない（D-078 項1）★

### 9-4. コミットと push

★**`git add` は明示的にファイルパスを指定すること。`git add -A` は禁止**★
（**過去に別作業の途中ファイルを巻き込む事故が起きています**）。

```bash
git status                       # 何が変わったか目で確認する
git add docs/decisions.md docs/handoff.md docs/fixtures.md docs/handoff/analysis/error-type-mapping.md
git add "docs/handoff/指示書K_エラー値を変換せず生値で表示する.md"
git commit -m "docs: 指示書K完了(D-088)を記録する"
git push origin develop
```

- ★**`main` へは push しないこと**★。`CLAUDE.md` §6-5 のとおり
  **main へのマージは PR 経由で、もう1人のレビューを通してから**行います
- **`--force` は使わないこと**
- ★**403 が出たら、無理に回避せずユーザーへ報告してください**★
  （認証情報を画面に出すコマンドを実行しないこと。`git credential fill` は**禁止**）

**push 後に確認すること**:

```bash
git log --oneline -1 origin/develop   # ローカルの HEAD と同じコミットか
git status -sb                        # 1行目に ahead/behind が無いか
```

★**この2つを段階9の記録に実測値として書き足すこと。**★

### 段階9の完了条件

- `docs/decisions.md` に **D-088**（★着手直前に確認した番号★）が追記されていること
- ★**`Handrail Lifted` が暫定値であることが D-088 に書かれていること**★
- ★**実データ接続後に `error_type` への実在を確認する申し送りが書かれていること**★（★ホスト名未受領でまだ接続できないことも★）
- ★**D-082 を踏まえ、「段階7-B の目的は暫定値を入れて動く形にすることであり、正しく発火させることではなかった」旨が書かれていること**★（記録すべきもの #15〜#17）
- `docs/handoff.md` の**末尾に追記**があり、**冒頭のポインタも更新**されていること
- `docs/decisions.md:1350` の `:1160` 誤記が直っていること
- ★**`origin/develop` へ push 済みで、`git status -sb` に ahead/behind が無いこと**★
- 文書末尾の「実行状態」表が埋まっていること

---

## ★やらないこと★

| 対象 | 理由 |
|---|---|
| ★**`error_type` の変換表を作ること**★ | D-077 で破棄された方針そのもの（禁止事項1） |
| ★**`lib/db/` の `prismaSource` の実装**★ | 実DB接続は別作業。`notImplemented` のまま（禁止事項4） |
| ★**段階7-B 以外での `lib/judge/advice.ts` / `lib/config/advice-rules.ts` の変更**★ | ★**承認は出ました（D-078 項2）**★。触ってよいのは ★**段階7-B の範囲だけ**★（禁止事項5） |
| ★**B3 のマッチ配列に値を入れること**★ | ★**オフラインを表す値が未判明**★。推測で入れるのは `CLAUDE.md` §7 の禁止事項（段階7-B） |
| ★**`Handrail Lifted` をコードに直書きすること**★ | ★**暫定値**★。★`lib/config/robot-status-match.ts` の設定値として持つ★（段階7-B。テストのリテラルは例外） |
| ★**`lib/fixtures/prechecks.ts` / `lib/logger/__tests__/smoke.test.ts` の日本語**★ | ★**B2/B3 の発火に関与しません**★（段階7-B-6） |
| ★**`DsDevice.connection_status` の変更**★ | 指示書G / D-076 で完了済み（段階7-3） |
| ★**`DsAlert.alert_type` / `resolution_status` の2状態化**★ | 対象外（段階7-3） |
| ★**「種別」列そのものの削除**★ | ★**別途ユーザー判断が要る**★（段階7-3） |
| ★**`FacilityPerformanceTable.tsx:129` の `statusColor`**★ | ★**同名だが別物**★（`FacilityStatus` の閉じた union。段階6-2） |
| ★**メインDBへの書き込み**★ | `CLAUDE.md` §7 / D-042 |
| ★**`robots.status` の46値の日本語変換**★ | D-077 項2 で明確に否定（`-200` を扱う分岐は現在コードに1つも存在しない） |

---

## 完了条件（全体）

1. 段階0〜9 ★**＋ 段階7-B**★ をすべて実施（★実行順は 段階7 → 段階7-B → 段階8★）
2. ★**B2/B3 の扱いは D-078 項2 で承認済み**★。★**段階7-B を実施したこと**★
   （`lib/config/robot-status-match.ts` が ★**新規に存在し、B2=`["Handrail Lifted"]` / B3=`[]`**★）
3. `npx tsc --noEmit` エラー0 / `npm run lint` 0
4. `npm test` 全パス。★**テスト数が着手前の実測値から減っていないこと**★（2026-09-09 改訂時点の基準: 29ファイル / 587テスト）
5. `grep -rn "ErrorType" lib/ components/ app/` が ★**0件**★
6. `grep -rn "handle_up\|lost_location\|brush_error\|low_battery" lib/ components/ app/ d-watch-api.yaml` が ★**0件**★
7. fixtures に ★**未知の値**★ と ★**空文字**★ のケースが**各1件**あること
8. `grep -rn "robotStatusColor" components/ lib/` が ★**0件**★（`lib/theme-status.ts` に一本化）
9. ★**`lib/theme-status.ts` のマッピングが `lib/config/` の設定値になっており、フォールバックがグレーであることが docstring に書かれていること**★
10. `DsAlert.detail` が YAML / 型 / fixtures / 画面 / CSS から消えていること
11. ★**`lib/config/index.ts` から `robot-status-match` が export されていないこと**★（独立ファイル。段階7-B）
12. ★**`lib/judge/advice.ts` の B2/B3 の分岐に日本語リテラルが残っていないこと**★（★trim + 大小文字無視のマッチになっていること★）
13. ★**`advice.test.ts` の期待値表11ケースが入っていること**★（★特に「空配列なら発火しない」「前後の空白」「大小文字違い」★）
14. ★**`USE_FIXTURES=true npm run dev` で `/reports` と `/ds-alerts` を目視確認したこと**★
15. `docs/decisions.md` / `docs/handoff.md` / `docs/fixtures.md` / `error-type-mapping.md` の更新
16. ★**`origin/develop` へ push 済み**★（★`main` へは push しない★）
17. **文書末尾の「実行状態」表を更新した**

---

## 実行状態

| 段階 | 状態 | コミット |
|---|---|---|
| ★**着手前の承認（B2/B3）**★ | ★**済（2026-09-08 / D-078 項2）**★ | — |
| 段階0 前提の確認 | 実施済み | （コミットなし） |
| 段階1 API契約を直す | 実施済み | `b33b40d` |
| 段階2 型を緩める | 実施済み | `67e7014`（★段階2〜5 を1コミットにまとめた。理由はD-088参照★） |
| 段階3 ソート順を作り直す | 実施済み | 同上 |
| 段階4 画面の色分けを壊さない | 実施済み | 同上 |
| 段階5 fixtures を実データの形に寄せる | 実施済み | 同上 |
| 段階6 色分けの重複実装を1本化する | 実施済み | `0474448` |
| 段階7 D-Sec の `detail` を落とす | 実施済み | `bd10bac` |
| ★**段階7-B B2 の発火条件を設定値に外出しする**★ | 実施済み | `2287fa3` |
| 段階8 テストを通す | 実施済み | （コミットなし） |
| 段階9 記録と push | 実施済み | 本コミット |

**実行日**: 2026-09-09
**取得した決定番号**: **D-088**
**着手前の実測値**: 29ファイル / 587テスト / `tsc` 0 / `lint` 0（着手直前に実測して確認）
**完了時の実測値**: 30ファイル / 603テスト / `tsc` 0 / `lint` 0
**段階7-B の暫定値**: `Handrail Lifted`（★**暫定。実データ接続後に `error_type` への実在を確認すること**★。D-078 項2。
参照用アカウントは受領済みだが、ホスト名が未受領のため本記録時点ではまだ確認できていない）
