# 指示書O — エラー種別ラベルの折り返しと空値表示

| 項目 | 内容 |
|---|---|
| 実行環境 | ★**Cursor 内の Claude が実装します**★（`.claude/commands/d-watch.md` の役割分担表を参照） |
| 実行状態 | ★**完了（2026-09-09 / D-089）**★ |
| 実行順 | ★**指示書K（D-088）の後**★。★指示書K が生値表示に切り替えたことで表面化した問題を直す作業★です。★他の未実施指示書とファイルは重なりません★（本指示書が触るのは `ReportsScreen.module.css` / `ReportsScreen.tsx` / `lib/format/` の新規1本 + そのテスト / `lib/i18n.ts` / `docs/`） |
| 取得する決定番号 | ★**着手直前に `grep -n "^### D-0" docs/decisions.md` で最終番号を確認すること**★。★作成時点の最終は `D-088`（`docs/decisions.md:3084`）なので、最初の空き番号は `D-089` の見込み★。★**2026-09-07 に `D-068` の二重採番が実際に起きています。必ず自分で確認してください**★ |
| 前提 | ★**D-077**★（エラー種別を変換しない＝生値をそのまま出す。本指示書の思想的な土台） / ★**D-088**★（指示書K の実行。生値表示への切替） / **D-050**（通ってからコミット） / **D-044**（fixtures で開発する） |
| 段階数 | ★**4段階**★（段階1〜段階4） |
| 着手前の基準 | `npm test` が ★**30ファイル / 603テスト 全パス**★、`npx tsc --noEmit` エラー0、`npm run lint` 0（★2026-09-09 実測★）。**着手直前に自分で実行して実測値を基準にすること** |
| 作成 | 2026-09-09（作成時点の決定事項最終番号 **D-088** / `docs/handoff.md` の最新は **追記24**） |

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

> ★**この指示書を読んでいるあなたへ**★
> `.claude/commands/d-watch.md` に「実装は Cursor 側、Claude Code は指示書作成まで」とありますが、
> これは**2つの環境の役割分担**です。**Cursor の中で動いている Claude は実装担当**です。
> この指示書を渡されたなら、**調査や改訂ではなく実装を行ってください。**

---

## ★★着手前にユーザーへ確認すること — ★なし★★★

★**2件の論点はいずれも 2026-09-09 にユーザーから回答を得ています。段階1 からそのまま進めてください。**★

```
問題1は折り返して2行分になるようにしてください
問題2は文字列が入っていれば生値のまま、入っていなければその他や不明としてしまうのは特に矛盾しないと思います
```

---

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

指示書K（D-088）で ★エラー種別を生値のまま表示する★ 方針に切り替えた結果、
レポート画面「エラー種別の内訳」で ★2つの表示上の問題★ が表面化しました。
以前は日本語の短い分類名（「ハンドルアップ」等）だったため出ていなかった問題です。

| # | 問題 | 現物（★着手前に自分で確認すること★） |
|---|---|---|
| 1 | ★ラベルがはみ出してバーに重なる★ | `.errorLabel` は `width: 110px; flex: none;` だが ★折り返しも省略も指定が無い★（`components/features/reports/ReportsScreen.module.css:269-274`）。`RobotHardwareErrorToAdmin` は必要幅160pxで ★約50px がバーに重なる★。`UnknownErrorFromCloud` は138pxで約28px |
| 2 | ★空値の行が空白のまま出る★ | `lib/fixtures/reports.ts:473` の `{ type: "", label: "", count: 1 }`（★意図的な境界ケース。同 `:472` にコメントあり★）が、`ReportsScreen.tsx:440` の `{item.label}` でそのまま空文字として描画される |

★行の高さは固定されていない★ので（`.errorRow` は `display: flex; align-items: center; padding: 8px 20px;`。
`components/features/reports/ReportsScreen.module.css:261-267`）、★ラベルが折り返せば行が自然に伸びます★。

---

## ★禁止事項★

| # | ★してはいけないこと★ | 理由 |
|---|---|---|
| 1 | ★ラベルをクランプ（`-webkit-line-clamp` 等で切り詰め）しないこと★ | ★生値をそのまま見せるのが D-077 の趣旨★。★切り詰めると値が読めなくなり、方針と矛盾する★ |
| 2 | ★値が入っているものを加工しないこと★ | ★D-077。トリム以外の変換をしない★ |
| 3 | ★代替文言を複数箇所に直書きしないこと★ | ★1箇所の定数にまとめる★。直書きすると i18n のキーとズレる |
| 4 | ★`status_1h` / `status_30m` / `robot_status` には今回適用しないこと★ | ★実データで空になるか未確認★。段階3 で申し送りとして記録するだけにとどめる |
| 5 | ★`ERROR_TONE`（`components/features/reports/ReportsScreen.tsx:57-64`）を触らないこと★ | ★フォールバック付き（`?? "pr"`）で正しく動いている★ |
| 6 | ★`git add -A` を使わないこと★ | `CLAUDE.md` §7。★必ずファイルパスを明示する★ |

---

## 段階1: ラベルを折り返す（CSS のみ）

`components/features/reports/ReportsScreen.module.css` の `.errorLabel` に ★折り返しを許可する指定を足す★。

### やること

- ★`overflow-wrap: anywhere`（または `word-break: break-all`）★を足す
  - ★英語識別子は途中に改行できる場所（空白・ハイフン）が無い★ので、★単語の途中で折れる指定が必要★
  - ★`overflow-wrap: break-word` だけでは折れない場合がある★（`break-word` は「単語がボックスに収まらないとき」の
    挙動が実装依存。`anywhere` のほうが確実）
- ★`line-height` の調整が要るか確認すること★（2行になったとき行間が詰まりすぎないか。
  現状 `.errorLabel` に `line-height` の指定は無い＝継承値）

### ★やらないこと★

- ★★クランプ（2行に切り詰め）はしない★★ — 禁止事項1。
  ★実測した識別子は110px幅で2行に収まる★ので通常は2行になる。
  ★3行以上になる値が来ても、行が伸びるだけで崩れない★（`.errorRow` に高さ固定が無いため）
- ★`width: 110px` は変えなくてよい★（変えると他の列とのバランスが崩れる）。
  ★ただし実装者が「2行に収まらない」と判断したら幅を調整してよい。その場合は段階4 で記録すること★

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

1. `.errorLabel` に折り返しを許可する指定が入っている
2. ★クランプ指定（`-webkit-line-clamp` / `line-clamp` / `text-overflow: ellipsis`）が**入っていない**こと★
3. `npx tsc --noEmit` エラー0 / `npm run lint` 0
4. ★CSS のみなのでテストは増えない★（`npm test` が着手前と同数で全パスすること）
5. コミットする

---

## 段階2: 空値の表示を決める純粋関数を作る

★`lib/format/` に新規ファイルを作る★（名前は実装者が決めてよい。例 `lib/format/raw-label.ts`）。
★前例: `lib/format/robot-model.ts`（`isSupportedModel()`。指示書N で作成）★ — 設定値は `lib/config`、
それを読む純粋関数は `lib/format` という既存の分け方を踏襲する。

### ★仕様★

| 入力 | 出力 |
|---|---|
| 値が入っている | ★生値をそのまま返す★（D-077。★加工しない★） |
| `null` | ★代替文言★ |
| `undefined` | ★代替文言★ |
| 空文字 `""` | ★代替文言★ |
| 空白のみ `"   "` | ★代替文言★ |

★上記4つ（`null` / `undefined` / 空文字 / 空白のみ）は**同じ扱い**にすること★。

### ★代替文言は `(値なし)` を推奨★

| 案 | 評価 |
|---|---|
| ★`(値なし)`★ | ★**推奨**★。★「DBのカラムが空である」という**事実だけ**を述べるので、推測が入らない★ |
| 「その他」 | ★D-067 の「未知の値を『その他』に寄せる」という**破棄済みの方針**（D-077 で破棄）と紛らわしい★ |
| 「不明」 | ★「エラーの内容が不明」とも読めてしまう★（実際は「種別の文字列が空」であって、エラーの内容の話ではない） |

★ユーザーは「その他や不明」でよいと言っているので、この3案のどれを選んでも方針違反ではありません★。
★実装者が変えたい場合は変えてよい。ただし文言は1箇所の定数にまとめ、あちこちに直書きしないこと★（禁止事項3）。

### ★テストを先に書く★

`lib/format/__tests__/` に置く（`robot-model.test.ts` と同じ場所）。★最低限この6ケース★:

| # | 入力 | 期待値 |
|---|---|---|
| 1 | `"EmergencyStop"` | `"EmergencyStop"`（そのまま） |
| 2 | `""` | 代替文言 |
| 3 | `"   "` | 代替文言 |
| 4 | `null` | 代替文言 |
| 5 | `undefined` | 代替文言 |
| 6 | ★`"  EmergencyStop  "`（前後に空白）★ | ★**トリムして返すのか、そのまま返すのかを決めて固定すること**★ |

★ケース6は「決めて固定する」ことが目的です★。どちらでもよいが、★テストで固定し、段階4 で記録すること★。
（`lib/format/robot-model.ts` は比較前に `trim()` している＝前例はトリムする側）

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

1. `lib/format/` に新規ファイルがあり、★DB アクセスを含まない純粋関数★であること
2. 代替文言が ★1箇所の定数★ になっていること
3. テストが上記6ケースを含み、全パスすること
4. `npx tsc --noEmit` エラー0 / `npm run lint` 0
5. ★`npm test` のテスト数が着手前の実測値から増えていること★
6. コミットする

---

## 段階3: ラベルに適用する

### やること

- `components/features/reports/ReportsScreen.tsx:440` の
  `<div className={styles.errorLabel}>{item.label}</div>` を、★段階2 の関数を通す形にする★
- ★i18n★: 代替文言を `lib/i18n.ts` の `EN` に足すこと。
  ★キーは日本語原文。完全一致でしか引けない★（`lib/i18n.ts:1-6` のコメント参照）。
  例: `"(値なし)": "(no value)"`

### ★★適用範囲は「エラー種別の内訳のラベル」だけにすること★★

★申し送りとして記録すること（段階4 の `docs/decisions.md` に書く）★:

> ★同じ問題は `status_1h` / `status_30m` / `robot_status` にも起こりうる★（空値だと空のバッジが出る）。
> ★ただし fixtures に空値が無く、実データでも空になるか未確認なので今回は適用しない★（禁止事項4）。

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

1. `ReportsScreen.tsx:440` が段階2 の関数を通している
2. ★`lib/i18n.ts` に対訳が入っている★
3. ★`ERROR_TONE` に差分が無いこと★（禁止事項5）。★`item.type` はそのまま `ERROR_TONE` の
   キーとして使われ続ける — 代替文言は**表示だけ**の話で、色分けのキーには影響させない★
4. `npx tsc --noEmit` エラー0 / `npm run lint` 0 / `npm test` 全パス
5. コミットする

---

## 段階4: 動作確認と記録

### ★ブラウザでの確認★

- ★`npm run dev` を Bash で起動しないこと。Browser pane の `preview_start` を使う★
- ★Browser pane が黒画面 / 0×0 になる既知の問題★: `preview_start` に `url` を渡して開き直す

★確認すること★:

| # | 確認項目 |
|---|---|
| 1 | ★`/reports` の「エラー種別の内訳」で `RobotHardwareErrorToAdmin` が **2行に折り返り、バーに重なっていない**こと★ |
| 2 | ★空文字の行に `(値なし)`（選んだ代替文言）が出ること★ |
| 3 | ★行の高さが伸びても、件数・パーセント・バーの縦位置がずれていないこと★ |
| 4 | ★ダーク / ライト両方★ |
| 5 | ★JP / EN 両方★（EN で代替文言が英訳されること） |
| 6 | ★`read_console_messages` でエラー0件★ |
| 7 | ★回帰: `/dashboard` が「9 / 10 完了」であること★ |

★ブラウザが使えなかった場合は、代替手段（curl + API JSON 等）で何を確認し、
何が確認できなかったかを実行状態表に明記すること★（指示書N 段階5 の前例）。

### `docs/decisions.md` に ★D-089★ を追記

★記録すること★:

1. ★生値方針（D-077 / D-088）にしたことで表面化した**表示**の問題であること★（仕様変更ではない）
2. ★クランプせず折り返しにした理由（値を隠さない）★
3. ★代替文言に何を選んだか、およびその理由★
4. ★`(値なし)` は「値が空である」という**事実**の表示であり、D-077 の「推測でマッピングしない」に反しないこと★
5. ★前後に空白がある値をトリムするか／しないかの決定★（段階2 ケース6）
6. ★`width: 110px` を変えたなら、その値と理由★
7. ★申し送り: `status_1h` / `status_30m` / `robot_status` にも同じ問題が起こりうる（今回は未適用）★

### `docs/handoff.md` に追記

★最新は 追記24（`docs/handoff.md:2000`）★。次は ★追記25★ の見込み。★着手直前に自分で確認すること★。

### push

- ★`git add` は必ずファイルパスを明示。`git add -A` は禁止★（禁止事項6）
- `git push origin develop`（★`main` へは push しない★）

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

1. 上記7項目の確認を実施した（★できなかったものは実行状態表に明記した★）
2. `docs/decisions.md` に D-089 を追記した（★7項目すべて記録★）
3. `docs/handoff.md` に追記した
4. ★`origin/develop` へ push 済み★
5. ★文書末尾の「実行状態」表を更新した★

---

## 完了条件（全体）

1. ★**着手前確認は「なし」**★（2件とも回答済み。段階1 から進めた）
2. 段階1〜4 をすべて実施したこと（★途中で止めた場合は、どこで止めたかを実行状態表に書くこと★）
3. `npx tsc --noEmit` エラー0 / `npm run lint` 0
4. `npm test` 全パス。★**テスト数が着手前の実測値から増えていること**★（作成時点の基準: 30ファイル / 603テスト）
5. ★**`.errorLabel` に折り返し指定があり、クランプ指定が無いこと**★
6. ★**代替文言が `lib/format/` の定数1箇所にあり、`ReportsScreen.tsx` に直書きされていないこと**★
7. ★**`grep -n "ERROR_TONE" components/features/reports/ReportsScreen.tsx` の結果が着手前と同じであること**★
8. ★**`lib/fixtures/reports.ts` に差分が無いこと**★（★`:473` の空文字は境界ケースなので消さない★）
9. ★**`d-watch-api.yaml` / `prisma/schema.prisma` / `lib/db/` に差分が無いこと**★
10. ★**`lib/i18n.ts` に対訳が入っていること**★
11. `docs/decisions.md` / `docs/handoff.md` の更新
12. ★**`origin/develop` へ push 済み**★
13. **文書末尾の「実行状態」表を更新した**

---

## 実行状態

| 段階 | 状態 | コミット |
|---|---|---|
| ★**着手前の確認**★ | ★**なし（2026-09-09 に回答済み）**★ | — |
| 段階1 ラベルを折り返す（CSS） | 完了 | `beec8e3` |
| 段階2 空値の表示を決める純粋関数 | 完了 | `d53cc5d` |
| 段階3 ラベルに適用する | 完了 | `2f25d19` |
| 段階4 動作確認と記録 | 完了 | 本コミット |

**実行日**: 2026-09-09
**取得した決定番号**: D-089
**着手前の実測値**: 30ファイル / 603テスト / `tsc` 0 / `lint` 0（2026-09-09 実測。指示書記載と一致）
**純粋関数の置き場所**: `lib/format/raw-label.ts`（`formatRawLabel()`。DBアクセスなしの純粋関数。
`lib/format/robot-model.ts` と同じ分け方＝設定値は `lib/config`、判定は `lib/format`。
今回は静的定数のみで `lib/config` は使っていない）
**選んだ代替文言**: `(値なし)`（指示書の推奨案どおり。理由はD-089参照）
**前後の空白の扱い**: トリムする（`lib/format/robot-model.ts` の前例を踏襲。
`lib/format/__tests__/raw-label.test.ts` ケース6で固定）
**`width: 110px` を変えたか**: 変えていない（指示書の既定推奨どおり）
**完了後の実測値**: 31ファイル / 610テスト 全パス / `tsc` 0 / `lint` 0
**詳細**: `docs/decisions.md` D-089 / `docs/handoff.md` 追記25 を参照。
段階4のブラウザ確認はCursor Browser paneが無い環境のため `npm run dev` + `curl` で代替し、
折り返し・空値表示・ダーク/ライト・JP/EN・コンソールエラーはCSR構成のため目視確認できていない
（D-089に限界を明記）。
