# 指示書G — D-Secログ「備考」の入力欄と、通信状態の2状態化

## 実行状態

| 項目 | 値 |
|---|---|
| **状態** | ★**完了（2026-09-08）**★。段階0〜7 すべて実施。ブラウザでの目視確認も実施済み |
| ★**実行順**★ | ★**`docs/handoff/指示書J_JSONストアの共通化と安全化.md` を先に実行すること**★（2026-09-07 追加）。J が作る `lib/settings/json-store.ts` を段階2-1 で使います。**J を飛ばすと、この指示書は `advice.ts` の3つの欠陥をコピーします** → **完了済み（2026-09-08）** |
| 作成 | 2026-09-07（作成時点の決定事項最終番号 **D-067**） |
| 実行環境 | **Cursor の中で動く Claude が実装する。★この指示書を渡されたあなたが実装者です。実装してください★** → **実際にはユーザーの直接指示によりClaude Code（Sonnet 5。監督役＋opusサブエージェント委譲）が実行した（2026-09-08）** |
| 実行日 | 2026-09-08 |
| 実行コミット | `b275392`（段階0）/ `5f23723`（段階1）/ `22a58b3`（段階2）/ `9efba0c`（段階6）/
段階3・4は並行していた別セッションのコミット `b564574` に巻き込まれた（内容は正常。詳細はD-076参照）/ `52f93e1`（段階5） |
| 取得した決定番号 | **D-076**（D-075 まで使用済みであることを着手直前に確認済み） |
| ★**前提**★ | ★**指示書J は完了済み**★（2026-09-08。`lib/settings/json-store.ts` が存在します）。**第5回の回答待ちには依存しません。今すぐ着手できます** → **満たされていたため着手した** |

> 実行後はこの表を更新してください。

> ★**この指示書を読んでいるあなたへ**★
> `.claude/commands/d-watch.md` に「実装は Cursor 側、Claude Code は指示書作成まで」とありますが、
> これは**2つの環境の役割分担**です。**Cursor の中で動いている Claude は実装担当**です。
> この指示書を渡されたなら、**調査や改訂ではなく実装を行ってください。**

---

## この指示書の位置づけ

TeamWorks 第2回回答（D-065）で、**F-06 D-Secログに2つの変更**が必要になりました。

| # | 変更 | 性質 |
|---|---|---|
| **A** | 通信状態を **3状態（正常 / 通信断 / 異常）→ 2状態（正常 / 異常）** に | **機械的**。型変更が起点で、コンパイルエラーが漏れを教えてくれる |
| **B** | 「備考」を**表示専用から、運用担当が入力できる欄に** | **設計判断が多い**。独自ストア・保存API・入力UI が要る |

**D-065 は B を「独立した作業として扱うこと」としています。**
本指示書では **A と B を段階で分け、別コミットにします**が、**1つの指示書にまとめました**。
理由は、**両者が同じ4ファイルを触る**ためです。

```
components/features/ds-logs/DsLogScreen.tsx
lib/types/index.ts
lib/fixtures/ds-devices.ts
d-watch-api.yaml
```

別々に走らせると確実に衝突します。**段階0（A）を先に完了させてから、段階1以降（B）に進んでください。**

> ★**指示書F（F-04 レポート）とは触るファイルが重なりません。並行して進められます。**★
> ただし `docs/decisions.md` の決定番号は取り合うので、**番号を取る直前に最終番号を確認**すること。

---

## ★先に決まっていること（読み飛ばさないこと）★

**次の10点は判断済みです。実装時に蒸し返さないでください。**

### 1. 保存先は **JSON ファイル**（★2026-09-07 決定★）

`data/settings/ds-device-notes.json` に `Record<serial_number, string>` の形で保存します。

- **却下した案: SQLite**（点検記録と同じストア）。同時書き込みに強く件数にも耐えますが、
  Prisma のデータソースをもう1つ持つ必要があり、マイグレーション・バックアップの対象が増えます
- **JSON を選んだ根拠（実データ）**: 制御盤は **20施設弱**（TeamWorks 回答）＝ 20件程度。
  1件あたり短いテキスト1本。運用は「翌朝 DFA 担当が確認する」体制で**同時編集はまず起きません**
- **前例をそのまま使えます**: `lib/settings/advice.ts` が
  `path.join(process.cwd(), "data", "settings", "advice.json")` の形で実装済み
- **`.gitignore` に `/data` があるのでコミットされません**（確認済み）。
  裏返すと**git のバックアップ対象外**なので、その旨をファイル冒頭コメントに書くこと
- 将来 SQLite に移すときは、**読み書きを `lib/settings/ds-device-notes.ts` に閉じ込めてある**ので
  そのファイルだけ差し替えれば済みます。**呼び出し側がファイル形式を知らない形にすること**

### 2. メインDBには絶対に書かない

`CLAUDE.md` §7 の禁止事項。更新可は `robot_error_reports` / `control_error_histories` の
4カラムのみ（D-042）。**備考はそこに含まれません。**
`lib/settings/advice.ts` の冒頭コメントと同じ趣旨の注記を、新しいファイルにも書くこと。

### 3. `control_error_histories.comment` は使わない

あれは**アラート1件ごとに運用担当が対応時に書き込む欄**で、**デバイス単位の備考とは粒度が違います**。
第3回の共有文書でも TeamWorks へその認識を伝えています。

### 4. キーは `serial_number`

`DsDevice` に `id` はありません（`lib/types/index.ts:190-197`）。
`serial_number` は既に一意キーとして使われています。

- 画面の行キー: `DsLogScreen.tsx:156` の `<tr key={item.serial_number}>`
- ソートの安定化キー: `lib/ds-devices/query.ts:106-110`（D-055）
- `DsAlert.device_serial` からの参照もシリアル文字列

### 5. API は `PATCH /ds-devices/{serial_number}/note`

- **パス方式**にします。前例: `/clean-logs/{id}/advice`、`/clean-logs/{id}/areas`
- **却下した案**: `/settings/ds-device-notes` にボディで serial を入れる（advice と同じ方式）。
  advice は「設定」ですが備考は**デバイスという資源に属する値**なので、パスに出すほうが自然です
- **メソッドは PATCH**。リポジトリで唯一の書き込み前例（`app/api/settings/advice/route.ts:50`）に合わせます

### 6. ★実在するデバイスかを必ず検証する★

**`db.dsDevices()` に存在しない `serial_number` は 404 を返します。**

これは2つの問題を同時に潰すためです。

1. 存在しないデバイスの備考がストアに溜まる（孤児レコード）
2. ★**プロトタイプ汚染**★ — `serial_number` は任意文字列なので、
   `__proto__` や `constructor` が JSON のキーとして飛んできます。
   実在チェックで弾けば、そもそも到達しません

**あわせて、JSON を読み込んだオブジェクトは `Object.create(null)` を土台にするか、
キーの取り出しに `Object.prototype.hasOwnProperty.call()` を使うこと。**
advice は「閉じた union のキー」だったのでこの問題がありませんでした。**前例が使えない箇所です。**

### 7. 空文字は削除、レスポンスは `null`

- 空文字（またはトリム後に空）が来たら**キーごと削除**します
- API が返す `note` は **`string | null`**。`""` を返さないこと
- 根拠: `lib/fixtures/ds-devices.ts` のファイル冒頭コメントに
  **「備考あり / なし（空文字ではなく null で持つ）」**と明記されています

### 8. 文字数上限は 200 文字

短いメモの想定です。超えたら **422 `validation_failed`**、
`details` に `{ note: ["200文字以内で入力してください"] }` を載せること。
上限値は **`lib/config/` に定数として置く**（ハードコード禁止。`CLAUDE.md` §7）。

### 9. 書き込みはアトミックにする（★advice の作りを真似しないこと★)

`lib/settings/advice.ts` には次の弱点があります。**備考では直してください。**

| advice の作り | 備考ではどうするか |
|---|---|
| `fs.writeFile` を直接呼ぶ（非アトミック）。書き込み中にクラッシュするとファイルが壊れる | **一時ファイルに書いてから `fs.rename`** でアトミックに差し替える |
| JSON が壊れていると `catch` で `{}` を返し、**次の書き込みで丸ごと上書き消滅**。ログも出ない | **`.bak` に退避してから `{}` で継続し、`logEvent("warn", ...)` を出す。** 備考は運用担当が書いた文章なので、黙って消してはいけません |
| テストが1本もない（`lib/settings/__tests__/` が存在しない） | **テストを書く**（下記 段階2） |

**同時書き込みのロックは入れません。** 20件・実質1人の運用で、rename によるアトミック差し替えで
「壊れたファイルが残る」ことは防げるためです。**理由をコメントに残すこと。**

### 10. 備考のマージは API ルート側で行う

`lib/db/index.ts:44-46` が
**「フィルタや集計はこの層では行わず、APIルート側で組み立てます」**と定めています。
`db.dsDevices()` は fixtures / Prisma の値をそのまま返し、
**`app/api/ds-devices/route.ts` で備考ストアの値を上書き**してください。

---

## 着手前の確認

```
git status                 # クリーンであること
npm test                   # 全パス（★267は古い基準値。着手直前に実行して実測値を基準にすること★）
npx tsc --noEmit           # エラー0
```

★**基準値267・決定番号D-067はどちらも作成時点(2026-09-07)のものです。古くなっています**★
（指示書Jの段階3完了時点で実測 **21ファイル/447テスト**。指示書Jの段階4でさらに決定番号が
1件以上進みます。指示書G自身の実行時にはこれよりさらに進んでいる可能性があります）。
**`npm test` と `grep -n "^### D-0" docs/decisions.md` を着手直前に必ず実行し、その実測値を基準にすること**
（`grep` で確認するのはファイル末尾ではなく決定番号そのもの。D-070 のように追記位置が
時系列順でないことがあるため）。

---

# 段階0. 通信状態を2状態にする（★先にこれを完了させてコミット★）

D-065 の決定です。**`controls.status` は 0（正常）/ 1（異常）の2値**で、
かつ「異常＝生死報告が来ていない」なので、**通信断と異常は同じもの**でした。

**型を先に変えてください。** `CONNECTION_STATUS_ORDER` が
`Record<DsDevice["connection_status"], number>` なので、**型を変えると必ずコンパイルエラーになり、
直す場所を機械が教えてくれます。**

### 0-1. `d-watch-api.yaml`（★D-040。YAML が先★）

| 行 | 現状 | 変更 |
|---|---|---|
| `:596` | 並び順は 異常→通信断→正常 | 異常→正常 |
| `:609` | `description: error=異常・通信断 / ok=正常` | `error=異常 / ok=正常` |
| `:1125-1127` | `enum: [ok, disconnected, error]` / `description: 正常 / 通信断 / 異常` | `enum: [ok, error]` / `正常 / 異常` |
| `:1140` | `error: { description: 異常+通信断の合計 }` | `異常の件数` |

**`status` クエリパラメータの enum（`[all, error, ok]`）は変更不要**です。元々 `disconnected` を含みません。

### 0-2. `lib/types/index.ts`

- `:186` `DsConnectionStatus = "ok" | "disconnected" | "error"` → **`"ok" | "error"`**
- `:189` のコメント「error = "error" + "disconnected"」を修正
- `DsDeviceStatusFilter`（`:189`）の**値は変更不要**

★**`lib/types/index.ts:200` の `DsAlertType = "security_off" | "disconnected"` は別物です。触らないこと。**★
F-07 D-Secアラートの種別で、`DsConnectionStatus` とは無関係です。
**`"disconnected"` を一括置換すると壊れます。**

### 0-3. `lib/ds-devices/query.ts`

| 行 | 変更 |
|---|---|
| `:70` | コメント「error = "error" または "disconnected"」を修正 |
| `:75-88` | `filterDsDevicesByStatus` の `case "error"` を `=== "error"` だけに |
| `:90-94` | `CONNECTION_STATUS_ORDER` を `{ error: 0, ok: 1 }` に |
| `:96-99` | ソートのコメント「異常→通信断→正常の順（D-055）」を修正 |
| `:129-137` | `summarizeDsDevices` の `error` の数え方を `=== "error"` だけに |

### 0-4. `components/features/ds-logs/DsLogScreen.tsx`

| 行 | 変更 |
|---|---|
| `:23-27` | `connectionStatusLabel()` から `disconnected → "通信断"` の分岐を削除 |
| `:29-33` | `connectionStatusColor()` から `disconnected → c.wn`（黄）の分岐を削除 |
| `:126` | フィルタボタンのラベル `{t("異常・通信断")}` → `{t("異常")}` |

**`lib/i18n.ts` の対訳も追随させること**（「異常・通信断」を消し、「異常」があるか確認）。

### 0-5. `lib/fixtures/ds-devices.ts`

- `:46`（DS-005）と `:96`（DS-010）の `connection_status: "disconnected"` を **`"error"`** に
- ファイル冒頭コメント（`:4-15`）の「3状態すべて（正常 / 通信断 / 異常）」「異常 → 通信断 → 正常」を修正

### 0-6. `lib/ds-devices/__tests__/query.test.ts`

`disconnected` を使っているケースを2状態に直します。

| 行 | 内容 |
|---|---|
| `:119-121` | フィルタのテスト用3件（ok / error / disconnected） |
| `:144-148` | ソートの期待値配列 |
| `:157-159` / `:172-175` | サマリのテスト |
| `:197-199` | summary 独立性テストの3件 |

★`:187-193` に**重要なコメント**があります。
「`DsConnectionStatus` は閉じた union で、4つ目の値を持つ `DsDevice` は any キャスト無しでは作れない。
**状態が増える時は、型を先に拡張してからこのテストにケースを足すこと**」。
**今回は逆に減らすので、このコメントも2状態に合わせて更新すること。**

### 段階0の完了条件

- `npx tsc --noEmit` エラー0
- `npm test` 全パス（**テスト数は減らないこと**。ケースの中身は変わるが件数は維持）
- **ここで一度コミットする**（D-050）

---

# 段階1. YAML に備考の保存エンドポイントを足す（★D-040★）

### 1-1. `DsDevice.note` の description を書く

現状は `note: { type: string, nullable: true, example: "セキュリティ復帰失敗" }` で**説明がありません**。
次の趣旨を書くこと。

> 運用担当がビューアーから入力するメモ。**DB由来ではなく DFA 側の独自ストアに保存される。**
> 検知側が生成する値ではない。

**`example` も差し替えること。** `"セキュリティ復帰失敗"` は**検知側の文言に見えるため誤解を招きます**
（D-065 でこれが誤りだと判明した経緯があります）。`"定期点検実施済み"` のような運用メモらしい例に。

### 1-2. `PATCH /ds-devices/{serial_number}/note` を追加

```yaml
  /ds-devices/{serial_number}/note:
    patch:
      tags: [ds-devices]
      summary: D-S制御盤の備考を保存する
      description: |
        運用担当が入力するメモ。DFA側の独自ストアに保存し、メインDBには書き込まない。
        空文字を送ると備考を削除する（レスポンスの note は null になる）。
      parameters:
        - name: serial_number
          in: path
          required: true
          schema: { type: string }
      requestBody: ...  # { note: string }
      responses:
        "200": ...      # { serial_number, note: string | null }
        "404": ...      # 存在しない serial_number
        "422": ...      # $ref: "#/components/responses/ValidationError"
        "500": ...
```

- **200 のレスポンスは保存後の値をエコーバック**する（advice の PATCH と同じ形）
- **404 を必ず定義すること**（実在チェックで返します）
- `422` は既存の `components/responses/ValidationError`（`:920-924`）を `$ref` する

---

# 段階2. 独自ストア `lib/settings/ds-device-notes.ts` を作る

### 2-1. 実装

★**`lib/settings/json-store.ts`（指示書J で作成）を使ってください。**★
アトミック書き込み・`.bak` 退避・プロトタイプ汚染対策は**共通部品側で済んでいます**。
このファイルは `createJsonStore` を呼んで**薄く包むだけ**にしてください。
★**`advice.ts` を写して自前で `fs.writeFile` を書かないこと**★（`advice.ts` も指示書J で同じ部品に載せ替え済みです）。

```ts
/** 保存先。lib/settings/advice.ts と同じ決め方（process.cwd() 起点） */
const NOTES_PATH = path.join(process.cwd(), "data", "settings", "ds-device-notes.json");

/** serial_number → 備考。空文字は保存しない（キーごと削除する） */
type DeviceNotes = Record<string, string>;

export async function readDeviceNotes(): Promise<DeviceNotes>;
/** note が空文字なら削除。戻り値は保存後の値（削除なら null） */
export async function saveDeviceNote(serial: string, note: string): Promise<string | null>;
```

**ファイル冒頭のコメントに必ず書くこと:**

1. **なぜメインDBに書かないか**（`CLAUDE.md` §7 / D-042。advice.ts の書き方に倣う）
2. **`.gitignore` の `/data` によりコミットされない = git のバックアップ対象外**であること
3. **将来 SQLite へ移すときはこのファイルだけ差し替えれば済む**設計であること

**実装の要点:**

- **読み込み**: ファイル不在は `{}`（正常系）。**JSON が壊れていたら `.bak` に退避してから `{}`**、
  かつ `logEvent("warn", ...)` を出す。★黙って捨てないこと★
- **書き込み**: 一時ファイル（`ds-device-notes.json.tmp` 等）に書いてから **`fs.rename`**。
  `fs.mkdir(recursive: true)` は advice と同じ
- **プロトタイプ汚染対策**: 読み込んだ JSON をそのまま使わず、
  `Object.create(null)` に `hasOwnProperty` で確認しながら移し替える
- **同時書き込みのロックは入れない。理由をコメントに書く**（★先に決まっていること 9★）

### 2-2. テスト `lib/settings/__tests__/ds-device-notes.test.ts`（★新規★）

**`lib/settings/` には現在テストが1本もありません。** ここで作ります。
一時ディレクトリを使い、**実際のファイル読み書きを検証**すること。

必ず含めるケース:

- ファイル不在 → `{}` が返る（例外にならない）
- 保存 → 読み込みで同じ値が返る
- **空文字で保存 → キーが消え、戻り値が `null`**
- **前後の空白のみの文字列も削除扱いになる**
- 既存の別デバイスの備考が消えないこと（部分更新）
- **JSON が壊れている → `.bak` が作られ、`{}` で継続する**
- **`__proto__` をキーにしても `Object.prototype` が汚染されない**

---

# 段階3. API ルート `app/api/ds-devices/[serial_number]/note/route.ts`

**`app/api/settings/advice/route.ts` の PATCH（`:50-110`）が唯一の前例です。** 骨格を踏襲すること。

```ts
const ROUTE = "/api/ds-devices/[serial_number]/note";

export async function PATCH(request: Request, { params }: { params: ... }) {
  return withRequestId(request, async () => {
    // 1) serial_number をパスから取得
    // 2) body = await request.json() を try/catch → 失敗は 422「JSONの形式が不正です」
    // 3) typeof body.note === "string" を検査 → 違えば 422
    // 4) 文字数上限（lib/config の定数）を超えたら 422 + details
    // 5) ★db.dsDevices() に存在するか確認 → 無ければ 404★
    // 6) try { saveDeviceNote(...) } catch → 500（errorPayload を logApiError と両方に渡す）
    // 7) 200 で { serial_number, note } を返す
  });
}
```

**★守ること★**

- **500 の `errorPayload` は1つのオブジェクトを作り、`logApiError` と `NextResponse.json` の
  両方に同じものを渡す**（`lib/logger/api.ts:4-6` の設計意図。ログとレスポンスがズレるのを防ぐ）
- `withRequestId` で全体を包む（`lib/logger` から import）
- バリデーションは `typeof` による手動チェック（zod は未導入。前例に合わせる）
- **404 のメッセージは「デバイスが見つかりません」**。`code` は `not_found`
  （`lib/types` の `ErrorCode` に既にあるか確認し、無ければ追加してから YAML と合わせる）

**Next.js のルーティング**: 動的セグメントは `[serial_number]` です。
`app/api/clean-logs/[id]/advice/route.ts` が同じ構造なので、**params の受け取り方はそれを見ること**
（このバージョンの Next.js では `params` が Promise の可能性があります。**必ず既存ルートに合わせる**）。

---

# 段階4. 一覧に備考をマージする

`app/api/ds-devices/route.ts` を変更します。**`lib/db/index.ts` は触りません**（★先に決まっていること 10★）。

```
rows = await db.dsDevices()          ← try の中はこれだけ（既存の形を維持）
notes = await readDeviceNotes()      ← 同じ try の中で取得してよい
rows = rows.map(d => ({ ...d, note: notes[d.serial_number] ?? d.note }))
```

**マージの規則**: **独自ストアに値があればそちらを優先**し、無ければ元の値（fixtures の `note`）。
理由をコメントに残すこと。

**フィルタ・ソート・サマリより前にマージすること**（将来 `q` の部分一致に備考を含める可能性があるため。
ただし**今回は備考を検索対象にしません**。`q` は施設名とシリアルのみ ＝ 現状維持）。

---

# 段階5. 画面に入力欄を作る

`components/features/ds-logs/DsLogScreen.tsx` と `DsLogScreen.module.css`。

### 5-1. 現状

- `:151` に `<th>{t("備考")}</th>`、`:166` に `<td className={styles.tdMuted}>{item.note ?? "-"}</td>`
- **表示専用**。画面全体で `<input>` は検索ボックス（`:107`）の1つだけ
- `.tdMuted`（CSS `:119-123`）は `font-size: 12px; color: var(--tx-m);`

### 5-2. 入力の形

**セル内インライン編集**にします。

- 通常は今までどおりテキスト表示（未入力は `-`）
- **クリックすると `<textarea>` に変わる**
- **確定はフォーカスを外したとき（blur）**。Enter は改行として使えるようにする
- 保存中は入力を無効化し、失敗したら**セルの下に赤字でメッセージ**を出す
  （`ApiFetchError.serverMessage` を使う。`lib/api/client.ts`）

**却下した案**: モーダル（20件の一覧で1つずつ開くのは手数が多い）／保存ボタンを常時表示
（列が広がり、他の列が潰れる）。

### 5-3. 通信

★**`fetch` を直書きしないこと。必ず `apiFetch` を通す**★（`lib/api/client.ts` の冒頭コメント）。
第2引数に `RequestInit` を渡せます（`lib/api/__tests__/client.test.ts:52` で透過がテスト済み）。

```ts
await apiFetch<{ serial_number: string; note: string | null }>(
  `/api/ds-devices/${encodeURIComponent(serial)}/note`,
  { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ note }) }
);
```

**`encodeURIComponent` を必ず使うこと。**

### 5-4. 保存後の反映

★**全件再取得（`fetchDevices()`）はしないこと。**★
`fetchDevices` は `setLoading(true)` を伴う（`:67-86`）ため、**表がローディング表示に切り替わり、
編集していた位置を見失います。**

**保存レスポンスの値で `setItems` を部分更新**してください。

```ts
setItems(prev => prev.map(d => d.serial_number === serial ? { ...d, note: saved.note } : d));
```

### 5-5. CSS

`DsLogScreen.module.css` に**入力欄用のクラスを新規追加**します。
既存の `.searchInput`（`:36-44`）が入力系スタイルの参考になります。

**色は必ず CSS 変数を使うこと**（`--tx` / `--tx-m` / `--bd` / `--sf-h` / `--ng`）。
このファイルは全て CSS 変数で書かれています。**`THEME_TOKENS` を混ぜないこと。**

### 5-6. i18n

`lib/i18n.ts` に対訳を追加すること（「備考を入力」「保存に失敗しました」など、新しく出す文言）。

---

# 段階6. fixtures の `note` を運用メモらしい文言に直す

**D-065 で「モックの値は検知側の情報ではなく運用担当のメモ」と判明しました。**
ところが `lib/fixtures/ds-devices.ts` には**検知側の文言に見えるものが残っています。**

| 行 | 現状 | 問題 |
|---|---|---|
| `:32`（DS-001）/ `:114`（DS-012） | `"セキュリティ復帰失敗"` | **検知側が出す文言に見える** |
| `:48`（DS-005） | `"6時間以上通信なし"` | 同上。しかも「6時間」は**根拠のない仮の値**（第2回で TeamWorks へその旨を伝えている） |
| `:90`（DS-009） | `"通信不安定"` | 同上 |
| `:98`（DS-010） | `"長時間通信なし"` | 同上 |
| `:64` / `:106` | `"定期点検実施済み"` | ★これだけが運用メモらしい★ |

**運用担当が書きそうな文言に差し替えてください**（例:「現地確認済み。電源要交換」「9/5 に再起動対応」など）。
**★実在の店舗名・住所・電話番号を書かないこと★**（`CLAUDE.md` §7）。

`null` の件数（6件）と非 null の件数（6件）の**比率は維持**すること。
ファイル冒頭コメントの「備考あり / なし（空文字ではなく null で持つ）」はそのまま残します。

---

## `docs/decisions.md` への記録

★**着手直前に最終番号を確認してから番号を振ること**★（2026-09-07 時点の最終は **D-067**。
**指示書F と取り合うので必ず再確認**）。

記録すべきもの:

| 内容 | 段階 |
|---|---|
| D-Secログの通信状態を2状態にした（`controls.status` が 0/1 の2値で、通信断と異常は同じもの） | 段階0 |
| **備考の保存先は JSON（`data/settings/ds-device-notes.json`）**。SQLite を却下した理由と、将来移す場合の道筋 | 先に決まっていること 1 |
| API は `PATCH /ds-devices/{serial_number}/note`。実在チェックで 404 を返す（孤児レコードとプロトタイプ汚染を同時に防ぐ） | 先に決まっていること 5・6 |
| 空文字は削除し `null` を返す | 先に決まっていること 7 |
| 備考の文字数上限（200文字） | 先に決まっていること 8 |
| 書き込みは一時ファイル + rename。JSON 破損時は `.bak` へ退避して warn（advice の黙殺は真似しない） | 先に決まっていること 9 |

★**D-065 に「保存先の具体（SQLite か JSON か）は未決」と書いてあります。決まったので
その記述を更新すること。**★

---

## ★やらないこと★

| 対象 | 理由 |
|---|---|
| **`lib/types/index.ts:200` の `DsAlertType` の `"disconnected"`** | ★**F-07 の種別で別物**★。`"disconnected"` の一括置換は禁止 |
| **メインDBへの書き込み** | `CLAUDE.md` §7 / D-042 |
| **`control_error_histories.comment` の利用** | 粒度が違う（★先に決まっていること 3★） |
| **F-07 D-Secアラートの「種別」列・「詳細」列の削除** | D-065 の別項目。**この指示書の範囲外**。別途 |
| **備考を検索（`q`）の対象に含めること** | 現状は施設名とシリアルのみ。変更しない |
| **同時書き込みのロック機構** | ★先に決まっていること 9★ |
| ~~**`lib/settings/advice.ts` のリファクタ**~~ | ★**2026-09-07 撤回。指示書J で実施します**★。理由: この指示書が `advice.ts` を「ひな型にする」と指示しているため、直さないと**同じ欠陥が2つ目のストアにコピーされる**。また `advice.ts` はパスをモジュール先頭で固定していて**テストが書けず**、段階2-2 の「一時ディレクトリで実際のファイル読み書きを検証」を満たすには部品化が必要 |
| **`lib/db/index.ts` の変更** | マージは API ルート側（★先に決まっていること 10★） |
| **SQLite への移行** | JSON で確定 |

---

## 段階7 — ★`origin/develop` へ push する★

**段階0〜6 がすべて緑でコミット済みになってから行ってください。**

```bash
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つを段階6の記録に実測値として書き足すこと。**★

---

## 完了条件

1. 段階0〜6 をすべて実施
2. `npx tsc --noEmit` がエラー0
3. `npm test` のテスト数が**減っていないこと**。
   ★**基準値は 2026-09-08 実測で「21ファイル / 447テスト」**★（指示書H・指示書J 完了後）。
   **文中に残っている 267 / 389 はどちらも古い値です。**
   ★**着手直前に `npm test` を実行して実測値を基準にすること**★。
   段階2 の `lib/settings/__tests__/ds-device-notes.test.ts` の分が増えるのが期待値
4. **`USE_FIXTURES=true` で実際に画面から備考を入力・保存し、リロード後も残ることを確認する**
   （`npm run dev` → `/ds-logs`）。**`data/settings/ds-device-notes.json` が生成されることも確認**
5. **空文字で保存して備考が消えることも実際に確認する**
6. 上の表の決定事項を `docs/decisions.md` に記録する。
   ★**D-065 の「保存先は未決」は指示書J（D-075）で解消済みです**★。**二重に直さないこと**
7. **段階0 と段階1以降は別コミットにする**（段階0 は独立して意味を持つため）。
   検証が通ったら**即コミット**（D-050）
8. 実行後、この指示書の冒頭「実行状態」表を更新する
9. ★**`origin/develop` へ push 済みで、`git status -sb` に ahead/behind が無いこと**★（段階7）
