| 項目 | 値 |
|---|---|
| 状態 | ★完了(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点は判断済みです。実装時に蒸し返さないでください。
data/settings/ds-device-notes.json に Record<serial_number, string> の形で保存します。
lib/settings/advice.ts が path.join(process.cwd(), "data", "settings", "advice.json") の形で実装済み.gitignore に /data があるのでコミットされません(確認済み)。 裏返すとgit のバックアップ対象外なので、その旨をファイル冒頭コメントに書くことlib/settings/ds-device-notes.ts に閉じ込めてあるので そのファイルだけ差し替えれば済みます。呼び出し側がファイル形式を知らない形にすることCLAUDE.md §7 の禁止事項。更新可は robot_error_reports / control_error_histories の 4カラムのみ(D-042)。備考はそこに含まれません。 lib/settings/advice.ts の冒頭コメントと同じ趣旨の注記を、新しいファイルにも書くこと。
control_error_histories.comment は使わないあれはアラート1件ごとに運用担当が対応時に書き込む欄で、デバイス単位の備考とは粒度が違います。 第3回の共有文書でも TeamWorks へその認識を伝えています。
serial_numberDsDevice に 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 からの参照もシリアル文字列PATCH /ds-devices/{serial_number}/note/clean-logs/{id}/advice、/clean-logs/{id}/areas/settings/ds-device-notes にボディで serial を入れる(advice と同じ方式)。 advice は「設定」ですが備考はデバイスという資源に属する値なので、パスに出すほうが自然ですapp/api/settings/advice/route.ts:50)に合わせますdb.dsDevices() に存在しない serial_number は 404 を返します。
これは2つの問題を同時に潰すためです。
serial_number は任意文字列なので、 __proto__ や constructor が JSON のキーとして飛んできます。 実在チェックで弾けば、そもそも到達しませんあわせて、JSON を読み込んだオブジェクトは Object.create(null) を土台にするか、 キーの取り出しに Object.prototype.hasOwnProperty.call() を使うこと。 advice は「閉じた union のキー」だったのでこの問題がありませんでした。前例が使えない箇所です。
nullnote は string | null。"" を返さないことlib/fixtures/ds-devices.ts のファイル冒頭コメントに 「備考あり / なし(空文字ではなく null で持つ)」と明記されています短いメモの想定です。超えたら 422 validation_failed、 details に { note: ["200文字以内で入力してください"] } を載せること。 上限値は lib/config/ に定数として置く(ハードコード禁止。CLAUDE.md §7)。
lib/settings/advice.ts には次の弱点があります。備考では直してください。
| advice の作り | 備考ではどうするか |
|---|---|
fs.writeFile を直接呼ぶ(非アトミック)。書き込み中にクラッシュするとファイルが壊れる |
一時ファイルに書いてから fs.rename でアトミックに差し替える |
JSON が壊れていると catch で {} を返し、次の書き込みで丸ごと上書き消滅。ログも出ない |
.bak に退避してから {} で継続し、logEvent("warn", ...) を出す。 備考は運用担当が書いた文章なので、黙って消してはいけません |
テストが1本もない(lib/settings/__tests__/ が存在しない) |
テストを書く(下記 段階2) |
同時書き込みのロックは入れません。 20件・実質1人の運用で、rename によるアトミック差し替えで 「壊れたファイルが残る」ことは防げるためです。理由をコメントに残すこと。
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 のように追記位置が 時系列順でないことがあるため)。
D-065 の決定です。controls.status は 0(正常)/ 1(異常)の2値で、 かつ「異常=生死報告が来ていない」なので、通信断と異常は同じものでした。
型を先に変えてください。 CONNECTION_STATUS_ORDER が Record<DsDevice["connection_status"], number> なので、型を変えると必ずコンパイルエラーになり、 直す場所を機械が教えてくれます。
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 を含みません。
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" を一括置換すると壊れます。
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" だけに |
components/features/ds-logs/DsLogScreen.tsx| 行 | 変更 |
|---|---|
:23-27 |
connectionStatusLabel() から disconnected → "通信断" の分岐を削除 |
:29-33 |
connectionStatusColor() から disconnected → c.wn(黄)の分岐を削除 |
:126 |
フィルタボタンのラベル {t("異常・通信断")} → {t("異常")} |
lib/i18n.ts の対訳も追随させること(「異常・通信断」を消し、「異常」があるか確認)。
lib/fixtures/ds-devices.ts:46(DS-005)と :96(DS-010)の connection_status: "disconnected" を "error" に:4-15)の「3状態すべて(正常 / 通信断 / 異常)」「異常 → 通信断 → 正常」を修正lib/ds-devices/__tests__/query.test.tsdisconnected を使っているケースを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状態に合わせて更新すること。
npx tsc --noEmit エラー0npm test 全パス(テスト数は減らないこと。ケースの中身は変わるが件数は維持)DsDevice.note の description を書く現状は note: { type: string, nullable: true, example: "セキュリティ復帰失敗" } で説明がありません。 次の趣旨を書くこと。
運用担当がビューアーから入力するメモ。DB由来ではなく DFA 側の独自ストアに保存される。 検知側が生成する値ではない。
example も差し替えること。 "セキュリティ復帰失敗" は検知側の文言に見えるため誤解を招きます (D-065 でこれが誤りだと判明した経緯があります)。"定期点検実施済み" のような運用メモらしい例に。
PATCH /ds-devices/{serial_number}/note を追加 /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": ...
422 は既存の components/responses/ValidationError(:920-924)を $ref するlib/settings/ds-device-notes.ts を作る★lib/settings/json-store.ts(指示書J で作成)を使ってください。★ アトミック書き込み・.bak 退避・プロトタイプ汚染対策は共通部品側で済んでいます。 このファイルは createJsonStore を呼んで薄く包むだけにしてください。 ★advice.ts を写して自前で fs.writeFile を書かないこと★(advice.ts も指示書J で同じ部品に載せ替え済みです)。
/** 保存先。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>;
ファイル冒頭のコメントに必ず書くこと:
CLAUDE.md §7 / D-042。advice.ts の書き方に倣う).gitignore の /data によりコミットされない = git のバックアップ対象外であること実装の要点:
{}(正常系)。JSON が壊れていたら .bak に退避してから {}、 かつ logEvent("warn", ...) を出す。★黙って捨てないこと★ds-device-notes.json.tmp 等)に書いてから fs.rename。 fs.mkdir(recursive: true) は advice と同じObject.create(null) に hasOwnProperty で確認しながら移し替えるlib/settings/__tests__/ds-device-notes.test.ts(★新規★)lib/settings/ には現在テストが1本もありません。 ここで作ります。 一時ディレクトリを使い、実際のファイル読み書きを検証すること。
必ず含めるケース:
{} が返る(例外にならない)null.bak が作られ、{} で継続する__proto__ をキーにしても Object.prototype が汚染されないapp/api/ds-devices/[serial_number]/note/route.tsapp/api/settings/advice/route.ts の PATCH(:50-110)が唯一の前例です。 骨格を踏襲すること。
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 } を返す
});
}
★守ること★
errorPayload は1つのオブジェクトを作り、logApiError と NextResponse.json の 両方に同じものを渡す(lib/logger/api.ts:4-6 の設計意図。ログとレスポンスがズレるのを防ぐ)withRequestId で全体を包む(lib/logger から import)typeof による手動チェック(zod は未導入。前例に合わせる)code は not_found (lib/types の ErrorCode に既にあるか確認し、無ければ追加してから YAML と合わせる)Next.js のルーティング: 動的セグメントは [serial_number] です。 app/api/clean-logs/[id]/advice/route.ts が同じ構造なので、params の受け取り方はそれを見ること (このバージョンの Next.js では params が Promise の可能性があります。必ず既存ルートに合わせる)。
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 は施設名とシリアルのみ = 現状維持)。
components/features/ds-logs/DsLogScreen.tsx と DsLogScreen.module.css。
: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);セル内インライン編集にします。
-)<textarea> に変わるApiFetchError.serverMessage を使う。lib/api/client.ts)却下した案: モーダル(20件の一覧で1つずつ開くのは手数が多い)/保存ボタンを常時表示 (列が広がり、他の列が潰れる)。
★fetch を直書きしないこと。必ず apiFetch を通す★(lib/api/client.ts の冒頭コメント)。 第2引数に RequestInit を渡せます(lib/api/__tests__/client.test.ts:52 で透過がテスト済み)。
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 を必ず使うこと。
★全件再取得(fetchDevices())はしないこと。★ fetchDevices は setLoading(true) を伴う(:67-86)ため、表がローディング表示に切り替わり、 編集していた位置を見失います。
保存レスポンスの値で setItems を部分更新してください。
setItems(prev => prev.map(d => d.serial_number === serial ? { ...d, note: saved.note } : d));
DsLogScreen.module.css に入力欄用のクラスを新規追加します。 既存の .searchInput(:36-44)が入力系スタイルの参考になります。
色は必ず CSS 変数を使うこと(--tx / --tx-m / --bd / --sf-h / --ng)。 このファイルは全て CSS 変数で書かれています。THEME_TOKENS を混ぜないこと。
lib/i18n.ts に対訳を追加すること(「備考を入力」「保存に失敗しました」など、新しく出す文言)。
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 で確定 |
origin/develop へ push する★段階0〜6 がすべて緑でコミット済みになってから行ってください。
git push origin develop
main へは push しないこと★。CLAUDE.md §6-5 のとおり main へのマージは PR 経由で、もう1人のレビューを通してから行います--force は使わないことgit credential fill は禁止)push 後に確認すること:
git log --oneline -1 origin/develop # ローカルの HEAD と同じコミットか git status -sb # 1行目に ahead/behind が無いか
★この2つを段階6の記録に実測値として書き足すこと。★
npx tsc --noEmit がエラー0npm test のテスト数が減っていないこと。 ★基準値は 2026-09-08 実測で「21ファイル / 447テスト」★(指示書H・指示書J 完了後)。 文中に残っている 267 / 389 はどちらも古い値です。 ★着手直前に npm test を実行して実測値を基準にすること★。 段階2 の lib/settings/__tests__/ds-device-notes.test.ts の分が増えるのが期待値USE_FIXTURES=true で実際に画面から備考を入力・保存し、リロード後も残ることを確認する (npm run dev → /ds-logs)。data/settings/ds-device-notes.json が生成されることも確認docs/decisions.md に記録する。 ★D-065 の「保存先は未決」は指示書J(D-075)で解消済みです★。二重に直さないことorigin/develop へ push 済みで、git status -sb に ahead/behind が無いこと★(段階7)