Newer
Older
client_dwatch / docs / handoff / 指示書M_ローカルDBで動かす第1歩.md

# 指示書M — ローカルDBで動かす第1歩(facilities / schedules を実DB経由にする)

項目 内容
実行環境 ★Cursor 内の Claude が実装します★(.claude/commands/d-watch.md の役割分担表を参照)
実行状態 ★未着手★(★着手前確認2件はD-085で回答済み。段階0から進めてよい★)
実行順 ★指示書L(D-084)の続き★。★指示書L が作った app/api/facilities/route.ts / app/api/schedules/route.ts / lib/facilities/query.ts / lib/schedules/query.ts を一切変更せずに、その下のデータ層だけを差し替える作業★です。★触るコードのファイルが重なる指示書は現時点でありません★(未実行は ★指示書I(lib/auth/ / proxy.ts)/指示書J(lib/settings/)/指示書K(lib/theme-status.ts / lib/judge/advice.ts / lib/config/robot-status-match.ts)★ の3本 — 実測)が、★docs/decisions.md / docs/handoff.md は3本とも触ります★。★同時に走らせないこと★。★注意★: ★指示書I / J / K はいずれも禁止事項に「lib/db/ の prismaSource に手を付けないこと」と書いていますが、★それは各指示書の範囲内での禁止であって、本指示書M はまさにそこを実装するための作業です★。矛盾ではありません★
取得する決定番号 ★着手直前に grep -n "^### D-0" docs/decisions.md で最終番号を確認すること★。★作成時点の最終は D-085(docs/decisions.md:2782)なので、最初の空き番号は D-086★。★2026-09-07 に D-068 の二重採番が実際に起きています。必ず自分で確認してください★
前提 ★D-080★(docs/decisions.md:2202。contract は10桁文字列) / ★D-060★(:403。end_time は整形済み文字列) / ★D-056★(:353。db pull 後は schema.prisma を手で書き換えない) / ★D-059★(:392。実DDL を正とする) / ★D-057★(:364。makers テーブルは提供されない) / D-044(:230。fixtures で開発する) / D-050(:308。通ってからコミット) / D-042(:226。書き込みは4カラムのみ) / D-010(:47。customers.contract を参照しない) / D-043(:243。実在の住所・氏名を使わない)
段階数 ★7段階★(段階0〜段階6)
着手前の基準 npm test が ★24ファイル / 530テスト 全パス★、npx tsc --noEmit エラー0、npm run lint 0(★2026-09-09 実測。Vitest 4.1.11 / Node v24.14.0★)。着手直前に自分で実行して実測値を基準にすること
作成 2026-09-09(作成時点の決定事項最終番号 D-084)

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

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

★★これは「第1歩」です。うまくいかなければ途中で止めて構いません。★★ ★段階3 か段階4 で行き詰まったら、.env.local の USE_FIXTURES を true に戻すだけで 完全に元の状態(fixtures で全画面が動く状態)に戻ります★。 ★DB に書いたデータも dwatch_local の中にしかなく、アプリの動作には一切影響しません。★ ★「途中まで進めて止めた」ことを段階6 の記録に残せば、それで十分な成果です。★


★★着手前にユーザーへ確認すること(2件)★★ — ★★2026-09-08 回答済み(D-085)★★

★どちらも「DB に何を書くか」を決める話で、先に決めないと段階2 のシード内容が変わります。★

★★回答: 2件ともこの指示書の推奨案で確定しました。★★ ★確認1 は「1=PUDU / 2=Keenon」、 確認2 は「施設12(id33)をシードから除外する」で進めてください。★詳細は docs/decisions.md D-085 / docs/handoff.md 追記21★。★以下の各節はそのまま推奨案として読んで構いません(変更なし)★。

★確認1 — devices.maker_id の採番★

# 事実
1 ★API の RobotSummary.maker(lib/types/index.ts:336-341)は "PUDU" / "Keenon" という文字列★。fixtures もその2つだけを使っている(lib/fixtures/facilities.ts)
2 ★DB 側は devices.maker_id(tinyint unsigned。prisma/schema.prisma:56)の数値★
3 ★makers テーブルは提供されない★ — ★D-057 の TeamWorks 回答 A-1★(prisma/schema.prisma:53-55 のコメント。「メーカー情報は設定ファイルで代替する想定」「maker_id は 0〜255 の範囲で運用する」)
4 ★したがって「どの番号がどのメーカーか」は TeamWorks からは降ってきません。DFA 側で決めるしかありません★

★当方の推奨(この指示書はこれで書いています)★

項目 内容
採番 ★1 = PUDU / 2 = Keenon★
置き場所 ★lib/config/index.ts★(★ハードコード禁止。CLAUDE.md §7 の趣旨、および D-057 A-1 の「設定ファイルで代替する」に合致★)
未知の番号 ★String(maker_id) をそのまま返す★(例: maker_id=7 → "7")。★"不明" や空文字に潰さない★ — ★画面に見慣れない値が出れば「対応表に足す必要がある」と気づけるが、"不明" に潰すと誰も気づかない★(D-077 の「推測で寄せない」と同じ考え方)
位置づけ ★DFA 側の暫定決定★。★実データの maker_id を受領したら差し替える★。★段階6 で決定として記録すること★

★この採番でよいかを着手前に確認してください。★ ★もし「実データを見るまで決めたくない」なら、代案は「段階2 のシードを PUDU の1メーカーだけにして Keenon を落とす」ですが、 ★fixtures の「メーカー混在」ケース(lib/fixtures/facilities.ts:85 の施設7)が DB 側で再現できなくなります★。★当方は推奨しません★。

★確認2 — 施設12(ロボット0台)のスケジュールを「除外する」方針でよいか★

# 事実
1 ★lib/fixtures/facilities.ts:200-213 の施設12「フレッシュタウン湖畔店」は robots: [](ロボット0台)★
2 ★なのに lib/fixtures/schedules.ts:208-219(id33)が、同じ施設にシリアル 8111R5628060166 のスケジュールを持っている★
3 ★同じシリアルが lib/fixtures/clean-logs.ts:90 と lib/fixtures/reports.ts:238 にも出てくる★
4 ★DB には「存在しないロボットのスケジュール」は入れられません★ — clean_schedules.section_id が sections への外部キーで、その section に紐づくロボットが無いと serial_number を結合で取れないためです

★当方の推奨(この指示書はこれで書いています)★: ★施設12 のスケジュール(id33)をシードから除外する★

# 内容
1 ★除外の判定はハードコードしない★ — ★「シリアルが facilities fixtures のロボット一覧に存在しない行はスキップする」という構造的な規則で落とす★(facility_id: 12 を名指ししない)。★そうしておけば、fixtures が直った日に自動で入るようになります★
2 ★スキップした件数と理由を必ず標準出力に出すこと★。★1件だけスキップされるのが期待値★。★2件以上スキップされたら fixtures 側で新たな不整合が生まれているので、そこで止めて報告すること★
3 結果: ★clean_schedules の行数は 13 → 12★ / ★inactive=0(= active: true 相当)の行は 12 → 11★ / ★ダッシュボードの日次 planned 相当は 10 → 9★
4 ★これは第1歩では問題になりません★ — ★ダッシュボードは第2歩の範囲で、USE_FIXTURES=false では今回そもそも 500 になります★(段階5 の確認4)

★この方針でよいかを着手前に確認してください。★ ★代案は「lib/fixtures/facilities.ts の施設12 にロボットを1台足して辻褄を合わせる」ですが、 ★それは fixtures の変更であり、lib/fixtures/__tests__/schedules.test.ts の不変条件(有効スケジュールの曜日該当件数 = 当日の清掃ログ行数)に影響しうる★ため、★第1歩の範囲を超えます★。★当方は推奨しません★。


この指示書の位置づけ

★発端 — ユーザーの問題意識★

モックなので適当な店舗などをベタ打ちしているのでこのようなことがおきていると思っています
いまローカルに仮データのDBなどを設置して、それを読む形式で確認できないですか

★この指摘は正しいです。★

lib/fixtures/ は ★画面ごとに別ファイルで手書きされている★ため、★ファイル間で辻褄が合わなくなります★。 ★型チェックもテストも通るので、誰も気づきません。★ 実際に2件見つかっています(★実測★):

# 不整合 現物
1 ★施設12「フレッシュタウン湖畔店」は robots: [](ロボット0台)なのに、スケジュールと清掃ログがシリアル 8111R5628060166 を割り当てている★ lib/fixtures/facilities.ts:200-213 / lib/fixtures/schedules.ts:208-219 / lib/fixtures/clean-logs.ts:90 / lib/fixtures/reports.ts:238
2 ★「メガバリューマート緑野第二店」が施設マスタ12件に存在しない★(マスタにあるのは「緑野店」) lib/fixtures/ds-alerts.ts:154 / lib/fixtures/ds-devices.ts:111

★なぜ DB 経由にすると直るのか★

★施設名もシリアル番号も「結合して取ってくる」ものになるからです。★

fixtures DB
各ファイルが facility_name: "..." と serial_number: "..." をそれぞれ手書きしている ★clean_schedules は facility_id / section_id しか持たず、施設名は facilities から、シリアルは robots から結合して取る★
★書き間違えても誰も止めない★ ★存在しない施設を参照した瞬間に外部キーが INSERT を拒否する★(実測。dwatch_local に外部キーが50本ある)

★この種の矛盾が「構造的に起きなくなる」ことが、この作業の本当の目的です。★ ★画面を実DBで動かすこと自体は副産物です。★

★どこまでやるか — 12メソッド中2つだけ★

lib/db/index.ts:140-177 の prismaSource は ★12メソッドすべてが notImplemented()★ です(実測)。 本指示書で実装するのは ★facilities()(:153-155)と schedules()(:156-158)の2つだけ★。 ★残り10メソッドは第2歩に回します★(「やらないこと」の表を参照)。

★この2つを選んだ理由★:

# 理由
1 ★マスタなので依存が少ない★(清掃ログのように「サマリテーブルにある期間」「当期はその場集計」といった分岐が無い)
2 ★指示書L で API ルート・クエリ純粋関数・画面・テストが全部そろっている★。★データ層を差し替えるだけで画面まで通る★
3 ★変換の難所(contract の桁位置 / inactive の反転 / HHMM の24〜29時 / weekdays_label のサーバ生成)が全部この2つに詰まっている★。★ここを越えれば残り10メソッドは作業量の問題になります★

★調査済みの事実(★現物で裏を取ってから使うこと★)★

★本節はすべて 2026-09-09 の実測です。★ ★それでも、着手時にもう一度自分で確認してください★(環境は動きます)。

1. ローカル環境

項目 状態
MySQL ★8.4.9 が稼働中★(SELECT VERSION() で実測)。Windows サービス名 ★MySQL84★(Running)、ポート 3306
★CLI が PATH に無い★ ★C:\Program Files\MySQL\MySQL Server 8.4\bin\mysql.exe をフルパスで呼ぶこと★
DATABASE_URL .env.local に設定済み。★ローカルの dwatch_local を向いている★(★接続情報は .env.local にしかありません。この文書にも他の git 管理下の文書にも書かないこと★。CLAUDE.md §9.3)
★dwatch_local★ ★28テーブル / 外部キー50本 / 全テーブル0行★(実測。groups customers devices facilities sections robots clean_schedules すべて COUNT(*) = 0)。★本作業の書き込み先はここだけ★
★dwatch_ddl★ ★39テーブル。TeamWorks から受領した実DDL を流し込んだもの★(Laravel 基盤の users / sessions / jobs / migrations などを含む)。★参照専用。1行も書き込まないこと★
Prisma prisma / @prisma/client ともに ★7.10.0★
★Prisma クライアント★ ★未生成★(lib/generated/prisma はディレクトリごと存在しない。出力先は prisma/schema.prisma:3 の output)
★.gitignore★ ★/lib/generated/prisma は .gitignore:44 に入っている★(★生成物はコミットされない。段階1 で足す必要はありません★)
シード ★仕組みが無い★(package.json に prisma セクション無し、prisma/seed.* 無し、scripts/ ディレクトリ無し)
Node ★v24.14.0★(実測)。★.ts ファイルを node で直接実行できる★(型ストリップが既定で有効)

2. ★★重大な発見 — dwatch_local は現行の prisma/schema.prisma と一致していません★★

★これは調査で新たに判明した事実です。読み飛ばさないでください。★

テーブル dwatch_local の実測 prisma/schema.prisma
robots ★task_id varchar(20) NULL / version varchar(20) NULL がある★。device_id の既定値 ★0★ / battery の既定値 ★100★ ★task_id / version が無い★(:200-227)。device_id に既定値なし / battery は @default(0)
clean_schedules ★started_at datetime NULL / ended_at datetime NULL がある★ ★両方とも無い★(:500-524)
facilities 一致 :96-140

★理由★: 現行の prisma/schema.prisma は ★2026-09-03 に dwatch_ddl に対して db pull した生成物★です(同ファイル :13-15 のコメント。D-056 / D-059)。 ★一方 dwatch_local は、それより前の「テーブル定義書ベースの手書きスキーマ」から作られたもの★と読めます。

★この食い違いから来る帰結(★守らないと事故ります★)★:

# 帰結
1 ★★dwatch_local に対して prisma db pull を実行しないこと★★ — ★prisma/schema.prisma が書き換わり、task_id / version / started_at / ended_at が生えます★。★それは実DDL(dwatch_ddl)の姿ではないので、D-059 に反する差分になります★
2 ★★prisma db push / prisma migrate を実行しないこと★★ — ★dwatch_local の余分な4列を DROP しようとします★
3 ★INSERT には影響しません★ — ★余分な4列はすべて NULL 許容または既定値付き★なので、Prisma が知らなくても行は入ります(★段階2 で実際に確かめること★)
4 ★この食い違い自体を段階6 で記録すること★。★後任が dwatch_local を「schema.prisma と同じもの」と思い込むと、上の1と2を踏みます★

3. ★dwatch_ddl から裏が取れた事実(★記録する価値があるもの★)★

# 事実 実測方法
1 ★customers に contract 列は存在しない★(dwatch_local / dwatch_ddl の両方で0件) information_schema.columns を table_name='customers' AND column_name='contract' で検索 → 0行
2 → ★D-010「customers.contract は削除依頼済み」が実際に反映されている★。★禁止事項ではなく「そもそも存在しない」ことが確認できました★ 同上
3 ★dwatch_ddl.robot_error_reports に PRIMARY KEY が無い★(制約は UNIQUE 1本 + 外部キー3本のみ) information_schema.table_constraints
4 → ★D-066 の A-1(PRIMARY KEY の付与)が未反映であることの裏取り★ 同上
5 ★ついでの発見★: その UNIQUE のインデックス名は ★robot_error_reports_report_id_unique★ なのに、★対象列は error_id★(名前と列が食い違っている) SHOW INDEX FROM dwatch_ddl.robot_error_reports

4. ★挿入順序(外部キーの依存。実測)★

groups → customers → devices → facilities → sections → robots → clean_schedules
テーブル 外部キー
customers.group_id → groups
facilities.customer_id → customers
sections.facility_id → facilities
★robots.device_id★ ★→ devices。既定値 0 だが devices に対応行が無いと INSERT が失敗する★
robots.facility_id / robots.section_id → facilities / sections
clean_schedules.facility_id / section_id → facilities / sections

★削除は必ずこの逆順★(clean_schedules → robots → sections → facilities → devices → customers → groups)。

5. ★列定義(dwatch_local の実測。★NOT NULL なのに fixtures に無いものに注意★)★

テーブル 列(★注意すべきもの★)
groups id / group_name varchar(100) ★NOT NULL★ / inactive tinyint NOT NULL 既定0 / timestamps / deleted_at
customers id / group_id NOT NULL 既定0 / customer_name varchar(400) ★NOT NULL★ / ★zip_cd varchar(8) NOT NULL★ / ★prefecture_cd tinyint NOT NULL★ / prefecture_name varchar(16) NOT NULL / addr1 varchar(800) NOT NULL / addr2 NULL / phone NULL / inactive / timestamps / deleted_at。★contract 列は無い★
devices id / ★maker_id tinyint unsigned NOT NULL★ / ★model_name varchar(100) NOT NULL★ / model_number NULL / inactive / timestamps / deleted_at
facilities id / customer_id NOT NULL / ★shop_id varchar(20) NOT NULL 既定 "0"★ / facility_name varchar(400) NOT NULL / ★zip_cd varchar(8) NOT NULL★ / ★prefecture_cd tinyint unsigned NOT NULL★ / prefecture_name varchar(16) NOT NULL / addr1 varchar(800) NOT NULL / addr2 NULL / phone NULL / email NULL / manager_name NULL / charger_name NULL / ★contract varchar(10) NOT NULL 既定 "0000000000"★ / inactive NOT NULL 既定0 / timestamps / deleted_at
sections id / facility_id NOT NULL / section_name varchar(100) ★NOT NULL★ / floor_name NULL / layout NULL / inactive / timestamps / deleted_at
robots id / facility_id NOT NULL / section_id NOT NULL / device_id NOT NULL 既定0 / serial_number varchar(20) ★NOT NULL★ / task_id NULL / version NULL / robot_name NULL / status smallint NOT NULL 既定0 / clean_status NOT NULL 既定0 / clean_ratio NOT NULL 既定0 / battery NOT NULL 既定100 / inactive / timestamps / deleted_at
clean_schedules id / facility_id NOT NULL / section_id NOT NULL / ★weeks varchar(7) NOT NULL★ / ★start_time varchar(4) NOT NULL★ / ★end_time varchar(4) NOT NULL★ / started_at NULL / ended_at NULL / contract varchar(10) NOT NULL 既定 "0000000000" / ★inactive tinyint NOT NULL 既定0★ / timestamps / deleted_at

6. ★★変換表 — これが prismaSource の中身そのものです★★

★段階3・段階4 はこの表を実装するだけです。表から外れたことをしないでください。★

API / TS 型 DB 変換
★すべての id★ ★BigInt★(prisma/schema.prisma の全モデルが BigInt @id) ★★Number(row.id) で変換すること★★。★JSON.stringify は BigInt を渡されると例外を投げます★(NextResponse.json が 500 になる)。★TS 側の型は number なので tsc が大半は捕まえますが、...row のような spread では素通りします★
Facility.address addr1 + addr2 ★連結★(addr2 が null / 空文字なら addr1 だけ。★純粋関数にしてテストを書くこと★)
Facility.prefecture prefecture_name そのまま。★prefecture_cd は NOT NULL なので、シード側で JIS の都道府県コードを入れる必要がある★
★Facility.monitoring_enabled★ ★contract の2桁目が "1"★ ★D-080★。★数値として比較しないこと★ — Number("0000000000") は例外を出さずに 0 になり、★全施設が「契約なし」になってもエラーもテスト失敗も出ません★
★Facility.d_security_enabled★ ★contract の1桁目が "1"★ 同上
Facility.robots[].maker devices.maker_id(★数値★) ★確認1 の対応表★。lib/config/ から読む
Facility.robots[].model devices.model_name そのまま
Facility.robots[].serial_number robots.serial_number そのまま
Schedule.weekdays ★weeks★ ★カラム名が違う★。単純な spread では移らない
Schedule.weekdays_label ★DB に無い★ ★サーバ生成★(1111111 → 「毎日」など。★純粋関数にしてテストを書くこと★)
Schedule.start_time ★HHMM の4桁文字列★ "0200" → "02:00"
★Schedule.end_time★ ★HHMM の4桁文字列。時が 24〜29 を取りうる★(prisma/schema.prisma:507-508 のコメント。D-057 B-5) ★D-060★。"0300" → "03:00" / ★"2830" → "翌04:30"★。★素朴な HH:MM パースは壊れます★
★Schedule.active★ ★inactive(0 = 有効)★ ★★論理が反転している★★。★active: Boolean(row.inactive) と書くと全件逆になります★。正しくは active: row.inactive === 0(lib/schedules/query.ts:15-19)
Schedule.serial_number robots.serial_number ★section 経由で結合して取る★
Schedule.facility_name facilities.facility_name ★結合して取る★
Schedule.facility_id facility_id Number()
★全テーブル共通★ deleted_at ★★論理削除。deleted_at IS NULL で絞ること★★
★全テーブル共通★ inactive ★絞らない★(段階3 の 3-4 を読むこと)

★禁止事項(この指示書の全段階に効きます)★

# 禁止 理由
1 ★★メインDB(TeamWorks 側)に接続・書き込みすること★★ CLAUDE.md §2 / §7 / D-042。★作業は dwatch_local の中だけで完結します★。★接続先ホスト名は未受領なので、そもそも繋がりません(D-078 項1)★。★間違って向けないよう、段階2 でガードを実装すること★
2 ★dwatch_ddl に書き込むこと★ ★参照専用★。受領した実DDL の姿を保つためのものです
3 ★prisma migrate を使うこと★ ★マイグレーションファイルがリポジトリに残ると、「メインDBへマイグレーションを流す意図」と誤解されます★。★CLAUDE.md §7 が禁止している領域に見える★。★テーブルは dwatch_local に既にあるので db push も不要★
4 ★prisma db push を使うこと★ 上記に加え、★dwatch_local の余分な4列(robots.task_id / version、clean_schedules.started_at / ended_at)を DROP しようとします★(「調査済みの事実」2)
5 ★dwatch_local に対して prisma db pull を実行すること★ ★prisma/schema.prisma が実DDL の姿から乖離します★(D-056 / D-059。「調査済みの事実」2)
6 ★prisma/schema.prisma を手で書き換えること★ ★D-056★。★db pull の生成物が正★
7 ★lib/fixtures/ を削除・改変すること★ ★CLAUDE.md §9.5★(DB接続後も残す)。★特に lib/fixtures/schedules.ts は不変条件の番人テストが付いています★(lib/fixtures/__tests__/schedules.test.ts)
8 ★テストを DB 依存にすること★ ★遅く・不安定になり、DB が空の環境で落ちます★。★テストは fixtures と純粋関数だけで書く★
9 ★シードに実在の住所・電話番号・社名・人名を入れること★ CLAUDE.md §7 / D-043。★fixtures の架空値をそのまま使えば安全★
10 ★contract を数値として比較すること★ ★D-080★。★contract === 3 のような比較は絶対に真になりませんが、例外もテスト失敗も出ません★
11 ★contract の桁位置をハードコードすること★ ★D-080 / CLAUDE.md §4.6★。★lib/format/ か lib/config/ の純粋関数1箇所に閉じ込める★
12 ★閾値をハードコードすること★ CLAUDE.md §7
13 ★customers.contract を参照すること★ CLAUDE.md §7 / D-010。★そもそも列が存在しません(実測)★
14 ★lib/facilities/query.ts / lib/schedules/query.ts / API ルート2本 / 画面2枚を変更すること★ ★本作業はデータ層の差し替えです★。★これらが1文字も変わらないまま画面が実DBで動くことが、変換が正しいことの証明になります★
15 ★lib/db/ にフィルタ・ページングを持ち込むこと★ ★lib/db/index.ts:50-54 の設計方針★(「フィルタや集計はこの層では行わず、APIルート側で組み立てます」)
16 ★git add -A を使うこと★ 過去に別作業の途中ファイルを巻き込む事故が起きています。★必ずファイルパスを明示★
17 ★.env.local をコミットすること★ CLAUDE.md §9.3。★.gitignore:34 の .env* で追跡外になっているが、git add でパスを明示すれば入ってしまいます★
18 ★DB のユーザー名・パスワードを git 管理下の文書やコードに書くこと★ CLAUDE.md §9.3。★.env.local にのみ置く★

作業の順序(★崩さないこと★)

段階 内容 コミット
段階0 前提の確認(★読むだけ★) コミットなし
段階1 Prisma クライアントを生成する ★コミットなし★(生成物は .gitignore:44 で追跡外)
段階2 シードスクリプトを作る 段階2で1コミット
段階3 prismaSource.facilities() を実装する 段階3で1コミット
段階4 prismaSource.schedules() を実装する 段階4で1コミット
段階5 ★動作確認(ここが本題)★ コミットなし
段階6 記録と push 段階6で1コミット + push

★段階ごとに npx tsc --noEmit / npm run lint / npm test を実行し、通ってからコミットすること★(D-050。docs/decisions.md:308)。


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

読むもの

資料 何を読むか
★lib/db/index.ts 全文(180行)★ ★:48 の USE_FIXTURES("false" という文字列以外はすべて fixtures)/:50-54 の設計方針/:55-82 の DataSource/:86-123 の fixtureSource/:134-138 の notImplemented/:140-177 の prismaSource★
★lib/facilities/query.ts★ ★変更しない★。★:79 の filterFacilitiesByQ が robots[] を横断していること(:74-78 のコメント)★を確認する
★lib/schedules/query.ts:8-34★ ★実DB実装時の注意4点がすでに書き残されています★。★本作業はこの4点に答える作業です★
app/api/facilities/route.ts / app/api/schedules/route.ts ★変更しない★。db.facilities() / db.schedules() を呼ぶだけの形になっていることを確認する
lib/types/index.ts:336-341(RobotSummary)/ :343-366(Facility)/ :368-379(Schedule) ★変換の到達点★。Facility.monitoring_enabled のコメント(:352-359)と d_security_enabled のコメント(:360-364)に D-080 が書かれている
prisma/schema.prisma:38(Group)/ :50(Device)/ :68(Customer)/ :96(Facility)/ :142(Section)/ :200(Robot)/ :500(CleanSchedule) ★変換の出発点★。★:113 が Facility.contract、:504 が weeks、:505-509 が HHMM の注意、:512 が CleanSchedule.inactive★
docs/decisions.md:2202-2245(D-080) ★contract は10桁文字列。数値比較禁止★
docs/decisions.md:403-412(D-060) ★end_time は「翌04:30」の整形済み文字列★
docs/decisions.md:353(D-056)/ :392(D-059) ★schema.prisma は db pull の生成物が正。手で書き換えない★
docs/decisions.md:364(D-057) ★A-1: makers テーブルは提供されない(確認1 の根拠)★ / ★B-5: end_time の時は 24〜29★
docs/handoff/analysis/prismaSource実装前チェックリスト.md ★実DB接続時の全体手順と落とし穴の一覧★。★本作業はその「ローカル版・第1歩」★
CLAUDE.md §2 / §4.6 / §7 / §9.3 / §9.5 責任分界 / 契約フラグ / 禁止事項 / コーディング規約 / fixtures の扱い

★接続先の確認(★これを飛ばさないこと★)★

# ★値そのものを画面に出さず、ローカルを向いているかだけを確認する★
grep -c "dwatch_local" .env.local     # 1 以上であること
grep -c "localhost\|127.0.0.1" .env.local

★.env.local には TeamWorks の参照用アカウントもコメントとして書かれています★(★ホスト名は未受領で <HOST> のまま。D-078 項1★)。 ★間違ってそちらを有効化しないこと。★ ★本作業で触ってよい DB は dwatch_local だけです。★

段階0の完了条件

  • 上の資料を読んだこと
  • git status がクリーンであること
  • ★着手前にユーザーへ確認すること(2件)の回答を得たこと★
  • ★.env.local の DATABASE_URL がローカルの dwatch_local を向いていることを確認したこと★
  • npm test / npx tsc --noEmit / npm run lint を実行し、★実測値を控えたこと★ (作成時点の基準は 24ファイル / 530テスト / tsc 0 / lint 0)
  • ★コミットしない★

段階1 — Prisma クライアントを生成する

1-1. 実行するコマンドは1本だけ

npx prisma generate

★migrate も db push も db pull も実行しないこと★(禁止事項3・4・5)。 ★理由: テーブルは dwatch_local に既にあり、スキーマを変える必要がまったくありません。★

★Prisma の設定ファイルは prisma7.config.ts です★(prisma.config.ts ではありません)。 ★実測では npx prisma が「Loaded Prisma config from prisma7.config.ts.」と出して自動で読みました★ので、 ★--config を付ける必要はありません★。★もし読まれていなかったら --config prisma7.config.ts を付けること★。

1-2. ★生成物を自分の目で確認すること★

ls lib/generated/prisma

★出力先は prisma/schema.prisma:3 の output = "../lib/generated/prisma"★。 ★生成されたエントリファイルの名前と拡張子を実測してください★ — ★段階3 の import 文がそれで決まります★。 ★記憶で @prisma/client から import しないこと★(このプロジェクトの generator は provider = "prisma-client" で、 ★出力先ディレクトリから import する形★です。prisma/schema.prisma:1-4)。

1-3. ★.gitignore の確認★

★/lib/generated/prisma は .gitignore:44 にすでに入っています★(実測)。★足す必要はありません★。 ★もし入っていなかったら、この段階では直さずに指摘だけしてください★(段階2 以降のコミットに混ぜないため)。

1-4. ★★生成物に依存させないこと(★段階3 への布石。ここで頭に入れておく★)★★

★lib/db/index.ts の先頭で生成クライアントを静的 import してはいけません。★

# 理由
1 ★テストが lib/db を読み込んでいます★(実測: lib/settings/__tests__/advice.test.ts / lib/settings/__tests__/ds-device-notes.test.ts が @/lib/db を import)
2 ★静的 import にすると、クライアント未生成の環境で npm test が全滅します★
3 ★USE_FIXTURES=true の人(=通常の開発者)まで生成物を要求されることになります★

★対策★: ★lib/db/prisma.ts を新設し、prismaSource の各メソッドの中で await import("./prisma") する★(段階3 の 3-1)。

段階1の完了条件

  • ★lib/generated/prisma が存在すること★
  • ★エントリファイル名と import パスを実測して控えたこと★
  • ★git status に追跡対象の差分が出ていないこと★(生成物は .gitignore:44 で無視される)
  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス(★この段階ではまだ何も変わっていないはず★)
  • ★コミットしない★

段階2 — シードスクリプトを作る

2-1. 作るもの

ファイル 内容
★新規★ scripts/seed-local-db.ts シード本体(★ファイル名・置き場所は 2-2 の結論に合わせること★)
★変更★ lib/config/index.ts ★メーカー対応表(確認1)★
★変更★ prisma7.config.ts または package.json ★実行方法の登録(2-2)★

2-2. ★★実行方法は「Prisma 7 の作法」を確認してから決めること★★

★記憶で package.json の prisma.seed を書かないでください。★ ★Prisma 7 では場所が変わっています。★

★作成時点の実測★(★着手時にもう一度確かめること★):

# 事実 実測方法
1 ★prisma db seed コマンドは存在する★ npx prisma db --help の Commands に seed Seed your database
2 ★シードコマンドの設定場所は migrations.seed(文字列)★ node_modules/@prisma/config/dist/index.d.ts の MigrationsConfigShape に seed?: string(「The command to run to seed the database after schema migrations are applied.」)
3 ★prisma7.config.ts には既に migrations: { path: "prisma/migrations" } がある★ 現物
4 ★Node v24.14.0 は .ts を直接実行できる★(型ストリップ既定有効)。★fixtures を import して動くことを実測済み★ node t.ts で facilities 12 / schedules 13 が出力された

★当方の推奨★: ★package.json の scripts に "seed:local": "node scripts/seed-local-db.ts" を足す★。

理由 内容
1 ★migrations.seed は名前のとおり「マイグレーション後に走らせるもの」★。★このプロジェクトはマイグレーションを禁止しています(禁止事項3)★ので、★そこに置くと「マイグレーションを使う気がある」と読まれます★
2 ★npm run seed:local という名前自体が「ローカル専用」を主張します★
3 ★どちらを採ってもよいが、採った理由を段階6 で記録すること★

★Node で .ts を直接動かすときの地雷(★実測で確認済み★)★:

# 地雷
1 ★@/ のパスエイリアスは解決されません★(tsconfig.json:21-23 の paths は TypeScript と Vitest だけの設定)。★シードからは相対パスで import すること★
2 ★相対 import には拡張子 .ts が必要です★(import { facilities } from "../lib/fixtures/facilities.ts")
3 ★型は必ず import type で読むこと★。★型ストリップは「値の import」を消さないので、型を通常の import で読むと実行時に落ちます★(★fixtures 側は既に import type になっているので、そのまま import できます★ — 実測)
4 ★MODULE_TYPELESS_PACKAGE_JSON の警告が出ますが、動きます★(package.json に "type": "module" が無いため)。★警告を消すために package.json に "type": "module" を足さないこと★ — ★Next.js のビルド全体に影響します★
5 ★生成された Prisma クライアントが node から素直に import できるかは実測すること★。★できなければ node --experimental-transform-types を試す★。★それでも駄目ならそこで止めて報告してください★(★prisma/schema.prisma の generator 設定を変えるのは D-056 違反です★)

2-3. ★★lib/fixtures/ を import して変換すること★★

★シードのデータをゼロから手書きしないでください。★

import { facilities } from "../lib/fixtures/facilities.ts";
import { schedules } from "../lib/fixtures/schedules.ts";
# 理由
1 ★fixtures を単一の出所に保てる★。★手書きすると「3つ目の真実」が生まれ、いま解こうとしている問題を増やすことになります★
2 ★変換ロジックは prismaSource で書くものの裏返し★なので、★二度手間になりません★。★段階3・段階4 で書く純粋関数(contract の組み立て・HH:MM → HHMM)をシードから逆向きに使えます★
3 ★段階5 の確認2(fixtures 版と DB 版のレスポンス一致)が意味を持つのは、出所が同じだからです★

2-4. ★★dwatch_local 以外に接続しないガードを入れること★★

★これは「気をつける」では足りません。コードで止めてください。★

// イメージ(実装は任せます)
const url = process.env.DATABASE_URL ?? "";
// 1) データベース名が dwatch_local であること
// 2) ホストが localhost / 127.0.0.1 であること
// どちらかでも満たさなければ throw して中断する(★エラーメッセージに URL を出さないこと★)
# 内容
1 ★事故で TeamWorks の DB に書き込むことを構造的に防ぐため★(★CLAUDE.md §3 のとおり、参照用アカウントは SELECT のみなので実際には失敗しますが、失敗に頼らない★)
2 ★エラーメッセージに DATABASE_URL の値を出さないこと★ — ★パスワードがログに残ります★(CLAUDE.md §9.3)
3 ★このガードにテストを書くこと★(URL 文字列を渡して真偽を返す純粋関数に切り出せば、DB 無しでテストできます)

2-5. ★冪等にすること★

★何度実行しても同じ結果になること。★ ★実行のたびに行が増える実装にしないでください。★

# 内容
1 ★実行前に対象7テーブルを削除する★(deleteMany)。★削除順序は挿入の逆順★(「調査済みの事実」4)
2 ★id を明示して INSERT すること★。★AUTO_INCREMENT に任せると、2回目以降の実行で facilities.id が 13〜24 になり、★段階5 の確認2(fixtures との一致)が id のズレで壊れます★
3 ★id は fixtures の id をそのまま使う★ — ★施設 = facilities fixtures の id(1〜12)/ロボット = robots[].id(101〜114)/スケジュール = schedules fixtures の id(21〜33 のうち採用した12件)★
4 ★sections.id は対応するロボットの id と同じにする★(★P1 は 1清掃エリア = 1ロボット。CLAUDE.md §8★)。★対応が一目で分かる★

2-6. ★fixtures に無い NOT NULL 項目の埋め方★

項目 埋め方
★zip_cd★ ★ダミー。"0000000" 固定でよい★。★実在の郵便番号を使わないこと★(CLAUDE.md §7 / D-043)。★0000000 は実在しない番号です★
★prefecture_cd★ ★JIS X 0401 の都道府県コード★。fixtures に出てくるのは4つだけ(実測): ★東京都 = 13 / 神奈川県 = 14 / 三重県 = 24 / 奈良県 = 29★。★対応表はシードスクリプト内のローカル定数でよい★(★アプリのロジックではなく、シードのためだけの情報だから★)。★表に無い都道府県が来たら例外を投げて止めること★(黙って 0 を入れない)
shop_id ★既定の "0" のままでよい★(画面が読んでいません)
★devices.maker_id★ ★確認1 の対応表★。★lib/config/index.ts に置き、シードはそれを逆引きして使う★
devices.model_name fixtures の robots[].model(CC1 / Phantas / T5)
★devices の行数★ ★fixtures に出てくる(メーカー, 機種)の組み合わせは3通り★(実測。PUDU/CC1・PUDU/Phantas・Keenon/T5)。★3行作る★
★sections.section_name★ fixtures に無い。★"1F フロア" のような架空の名前でよい★。★ロボットと1対1なので、ロボットごとに一意な名前を付けること★
★groups / customers★ fixtures に無い。★1グループ + 1顧客を作る★。★実在の社名を使わないこと★ — ★推奨は groups.group_name = "テストグループ001" / customers.customer_name = "テスト顧客001"★(★実在しないことが明白な名前にする★)。★住所は fixtures と同じ架空形式("東京都○○区1-2-3")★
★facilities.contract★ ★fixtures の monitoring_enabled / d_security_enabled から10桁文字列を組み立てる★。★桁位置をシードに直書きしないこと★ — ★段階3 で作る純粋関数と同じ桁位置定義を使うこと★(D-080 / CLAUDE.md §4.6)
clean_schedules.contract ★既定値 "0000000000" のままでよい★。★理由をコメントに書くこと★ — 「API はこの列を読まない。施設側の contract と混同しないため、あえて既定値のままにしている」
★clean_schedules.inactive★ ★fixtures の active の反転★(active: true → 0 / active: false → 1)。★id24 だけが 1 になるのが期待値★
robots.status / clean_status / clean_ratio / battery / task_id / version ★既定値のままでよい★(今回の2メソッドはどれも読みません)。★task_id / version は現行 schema.prisma の Robot に存在しないので、Prisma からは触れません★(「調査済みの事実」2)

2-7. ★★不整合を再現しないこと★★

★fixtures にある矛盾を、そのまま DB に持ち込まないでください。★ ★それでは何のために DB にしたのか分かりません。★

# 内容
1 ★施設12(ロボット0台)には clean_schedules を作らない★(確認2)
2 ★判定は構造的な規則で行う★ — ★「そのスケジュールの serial_number が、facilities fixtures のロボット一覧に存在しないならスキップ」★。★facility_id === 12 を名指ししないこと★
3 ★スキップした行を標準出力に出すこと★(例: skip: schedule id=33 serial=8111R5628060166 (施設12 にロボットが存在しない))
4 ★スキップ件数が1件でなければ、そこで止めて報告すること★。★2件以上なら fixtures 側で新しい不整合が生まれています★

2-8. ★第2歩への申し送り(★段階6 で記録すること★)★

★清掃ログをシードするとき(第2歩)は、施設12 の「未実施」1件も同時に除外してください。★

現状(fixtures) 第1歩の後(DB) 第2歩で両方除外した後
当日の有効スケジュール該当 10 対 当日ログ 10 ★スケジュール側だけ 9 になる(ログは第2歩の範囲なので未シード)★ ★9 対 9 で均衡が保たれる★

★該当行は lib/fixtures/clean-logs.ts:90★(facility_id: 12 / serial_number: "8111R5628060166" / completion_rate: null)。

2-9. ★検算コマンド★

# ★mysql.exe は PATH に無い。フルパスで呼ぶこと★
# ★接続アカウントは `.env.local` の DATABASE_URL に書いてあるものを使う★
#   (★この文書にユーザー名・パスワードを書かないこと★。CLAUDE.md §9.3)
# ★`groups` は MySQL 8 の予約語。バッククォートで囲まないと構文エラーになる★(実測)
"C:\Program Files\MySQL\MySQL Server 8.4\bin\mysql.exe" -u <ユーザー> -D dwatch_local -e "
  SELECT (SELECT COUNT(*) FROM \`groups\`) g,
         (SELECT COUNT(*) FROM customers) c,
         (SELECT COUNT(*) FROM devices) d,
         (SELECT COUNT(*) FROM facilities) f,
         (SELECT COUNT(*) FROM sections) s,
         (SELECT COUNT(*) FROM robots) r,
         (SELECT COUNT(*) FROM clean_schedules) cs;"

★期待値★: g=1 / c=1 / d=3 / f=12 / s=14 / r=14 / cs=12 (★s と r は「fixtures のロボット総数」。着手時に自分で数えて確かめること★ — ★作成時点の実測は14台★)。

段階2の完了条件

  • scripts/seed-local-db.ts(相当)が存在し、★2回続けて実行しても結果が同じであること★
  • ★facilities 12行 / robots が fixtures のロボット総数と同数 / clean_schedules 12行★(2-9 の検算)
  • ★スキップされたスケジュールがちょうど1件で、その内容が出力されていること★
  • ★ガードが効くこと★ — ★DATABASE_URL を一時的に別の値にして、中断することを1回だけ確かめる★(★確かめたら必ず元に戻すこと★)
  • ★prisma/schema.prisma に差分が無いこと★(git diff --stat prisma/ が空)
  • ★lib/fixtures/ に差分が無いこと★(git diff --stat lib/fixtures/ が空)
  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス
  • ★ここで1コミット★

段階3 — prismaSource.facilities() を実装する

3-1. 作るもの

ファイル 内容
★新規★ lib/db/prisma.ts ★Prisma クライアントのシングルトン★(3-2)
★新規★ lib/format/contract.ts ★contract(10桁文字列)↔ boolean の純粋関数★
★新規★ lib/format/__tests__/contract.test.ts ★期待値表(3-5)★
★新規または追記★ lib/format/address.ts(+ テスト) ★addr1 + addr2 の連結★
★変更★ lib/config/index.ts ★桁位置の定義★ + ★メーカー対応表(段階2 で追加済みなら追記不要)★
★変更★ lib/db/index.ts ★prismaSource.facilities()(:153-155)の notImplemented() を置き換える★

★lib/format/__tests__/ は既存です★(period.test.ts がある)。★同じ場所に置くこと★。

3-2. ★Prisma クライアントはシングルトンにし、★遅延 import する★★

# 内容
1 ★lib/db/index.ts の先頭で生成クライアントを静的 import しないこと★(段階1 の 1-4。★テストが落ちます★)
2 ★prismaSource.facilities() の中で const { getPrisma } = await import("./prisma"); する★
3 ★lib/db/prisma.ts は globalThis にキャッシュする形にすること★ — ★next dev は編集のたびにモジュールを作り直すので、素直に new PrismaClient() すると接続が増え続けます★
4 ★なぜこの形にしたのかをファイル冒頭のコメントに書くこと★。★後任が「普通に import すればいいのに」と直してしまうのを防ぐため★

3-3. ★contract の変換は純粋関数1箇所に閉じ込める★

★D-080 / CLAUDE.md §4.6 / §7 の要求です。★

置き場所 内容
★lib/config/index.ts★ ★桁位置の定義★(例: CONTRACT_DIGITS = { dSecurity: 0, dWatch: 1 } as const)。★これが「設定値として外出しする」の実体★
★lib/format/contract.ts★ ★純粋関数★。contract 文字列 → { monitoring_enabled, d_security_enabled }、およびその★逆向き★(段階2 のシードが使う)

★桁位置の数字(0 / 1)を関数の中に直接書かないこと。★ ★Number() / parseInt() を通さないこと。★ ★文字の比較(contract[i] === "1")で判定します★。

3-4. ★★inactive では絞り込まないこと(★仕様が未確定なので、絞らない側に倒す★)★★

# 内容
1 ★deleted_at IS NULL では絞る★ — ★論理削除は「無かったこと」なので、返してはいけません★
2 ★inactive では絞らない★ — ★inactive は「無効化」であって削除ではありません★。★画面が見せ分ける可能性があります★
3 ★前例があります★ — lib/schedules/query.ts:108-112 が「一覧は有効・無効の両方を返す。API 側で無効行を落とすと『登録したはずのスケジュールが消えた』ように見える」と明記しています
4 ★robots.inactive も同じ扱い★(無効なロボットも robots[] に含める)
5 ★これは仕様が未確定な箇所です。判断と理由を段階6 で決定として記録すること★(CLAUDE.md §0-1)

3-5. ★テスト(期待値表を先に作ること。CLAUDE.md §9.4)★

★テストは純粋関数に対してだけ書きます。DB に繋ぐテストを書かないこと★(禁止事項8)。

lib/format/contract.ts:

# 入力 d_security_enabled monitoring_enabled
1 "0000000000" false false
2 "1000000000" ★true★ false
3 "0100000000" false ★true★
4 "1100000000" ★true★ ★true★
5 ★"11"(短い文字列)★ ★true★ ★true★(★桁が足りていれば読める★)
6 ★"1"(1桁)★ ★true★ ★false★(★2桁目が無い = 契約なし★)
7 ★""(空文字)★ false false(★落ちないこと★)
8 ★"0000000000000"(10桁超)★ false false(★落ちないこと★)
9 ★"abcdefghij"(0/1 以外)★ false false(★"1" との完全一致で判定するので自然にこうなる。★テストで固定すること★)
10 ★逆変換★ { dSecurity: true, dWatch: true } → ★"1100000000"★ / 両方 false → "0000000000" —

アドレス連結:

# 入力 期待
1 addr1="東京都○○区1-2-3" / addr2=null "東京都○○区1-2-3"
2 addr2=""(空文字) ★addr1 のまま。★末尾に余計な空白や区切りを付けないこと★★
3 addr2="○○ビル5F" ★連結された文字列★(★区切り文字を何にするか決めて、テストで固定すること★)

3-6. ★★動作確認は段階5 でまとめて行いますが、この段階で1回だけ叩いてよい★★

# ★USE_FIXTURES=false で /api/facilities が JSON を返すことだけ確認する★

★画面まで確認するのは段階5 です。ここでは「500 にならない」ことだけ見れば十分です。★ ★確認したら .env.local を元に戻すこと★(段階4 の作業中に全画面 500 になるのを避けるため)。

段階3の完了条件

  • ★lib/db/index.ts の facilities() が notImplemented() でなくなっていること★
  • ★contract の変換が lib/format/ の純粋関数1箇所にあり、桁位置が lib/config/ から来ていること★
  • ★Number() / parseInt() で contract を扱っている箇所が1つも無いこと★(grep -n "Number(.*contract\|parseInt(.*contract" lib/ が0件)
  • ★期待値表(3-5)のケースがテストに入っていること★
  • ★deleted_at IS NULL で絞っていること★ / ★inactive で絞っていないこと★
  • ★lib/facilities/query.ts / app/api/facilities/route.ts / 画面に差分が無いこと★(git diff --stat で確認)
  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス(★テスト数が増えていること★)
  • ★ここで1コミット★

段階4 — prismaSource.schedules() を実装する

4-1. 作るもの

ファイル 内容
★新規★ lib/format/schedule.ts ★weekdays_label の生成★ と ★HHMM → 表示用文字列の整形★
★新規★ lib/format/__tests__/schedule.test.ts ★期待値表(4-4 / 4-5)★
★変更★ lib/db/index.ts ★prismaSource.schedules()(:156-158)の notImplemented() を置き換える★

★lib/schedules/query.ts:8-34 に、この段階で答えるべき4点がすでに書き残されています。★ ★まずそれを読んでから実装してください。★

4-2. ★結合★

欲しいもの 取り方
facility_name clean_schedules.facility_id → facilities
★serial_number★ ★clean_schedules.section_id → sections → その section に属する robots★

★P1 は 1清掃エリア = 1ロボット★(CLAUDE.md §8)なので、★section に属するロボットは1台★の前提です。 ★0台だった場合にどうするかを決めて、コメントに書くこと★。 ★当方の推奨は「その行を落とさず、serial_number を空文字で返す」ではなく ★そもそもそういう行が作られないよう段階2 で除外している★ ので、 ★実装上は「見つからなければ例外を投げて気づけるようにする」か「ログを出して空文字」かのどちらかを選び、★選んだ理由をコメントに書くこと★★。

4-3. ★★inactive の反転★★

active: row.inactive === 0     // ✅
active: Boolean(row.inactive)  // ❌ 全件逆になる

★lib/schedules/query.ts:15-19 が名指しで警告している箇所です。★ ★逆にしても型エラーもテスト失敗も出ません。画面の「有効」列が全部逆になるだけです。★ ★段階5 の確認3 で、id24(唯一の無効スケジュール)だけが無効表示になっていることを目で確かめてください。★

4-4. ★weekdays_label はサーバで生成する(★純粋関数 + テスト★)★

★DB に無い項目です★(lib/schedules/query.ts:31-33)。★weeks の7桁から作ります★。

入力(weeks) 期待するラベル 出典
"1111111" ★"毎日"★ lib/fixtures/schedules.ts:64
"0111110" ★"平日のみ"★ 同 :114
"1000001" ★"土日のみ"★ 同 :127
"0101010" ★"月水金"★ 同 :89
"1010101" ★"日火木土"★ 同 :178
★"0000000"★ ★決めてテストで固定すること★(推奨: "なし")。★空文字を返さないこと★ — 画面が空欄になり、不具合と区別できません —
★"1000000"★ ★"日"★(1曜日だけのケース) —
★"111"(桁が足りない)★ ★落ちないこと★。★決めてテストで固定すること★ —

★曜日の起点は日曜★(lib/fixtures/schedules.ts:6「weekdays は「日月火水木金土」の7桁」)。 ★特例3つ(毎日 / 平日のみ / 土日のみ)を先に判定し、それ以外は該当曜日を連結する★形にすること。 ★この起点をフロントと2箇所に持たないこと★(CLAUDE.md §9.6 / 指示書L 段階5-3)。

4-5. ★★end_time の 24〜29時(★D-060。テストに必ず含める★)★★

DB の値 API が返す値
"0200" "02:00"
"0300" "03:00"
"2359" "23:59"
★"2430"★ ★"翌00:30"★
★"2830"★ ★"翌04:30"★
★"2900"★ ★"翌05:00"★(★24〜29 の上限★)
★"3000"(範囲外)★ ★決めてテストで固定すること★。★推奨は例外を投げる★(★DB の値域外なので、静かに変な表示になるより止まるほうがよい★)
★start_time★ ★0000〜2359 のみ★(prisma/schema.prisma:505)。★常に HH:MM★

★整形は db.schedules()(データ層)が済ませてから Schedule を返す約束です★(lib/schedules/query.ts:28-29)。 ★クエリ層・画面側では一切パースしません。★

4-6. ★★シードが書く end_time の値について(★段階2 と揃えること★)★★

★fixtures の id27(施設5・港南総本店)は start_time: "23:30" / end_time: "00:30" です★(lib/fixtures/schedules.ts:132-143)。 ★これは日跨ぎ清掃ですが、fixtures は D-060 より前の書き方のまま「00:30」と持っています。★

★当方の推奨★: ★シードは DB の表現に忠実に "2430" を書く★。

# 理由
1 ★DB の end_time は 24〜29時で日跨ぎを表す★(D-057 B-5 / prisma/schema.prisma:507-508)。★"0030" と書くと「同じ日の 0:30 に終わる(= 23:30 開始より前に終わる)」という矛盾したデータになります★
2 ★これが 24〜29時ケースの唯一の実データになり、段階4 の整形が実際に効いているかを画面で確認できます★
3 ★帰結: 段階5 の確認2 で、この1件だけ意図的に不一致になります★ — ★fixtures 版は "00:30"、DB 版は "翌00:30"★。★これは想定どおりで、DB 版のほうが正しい★。★段階6 で記録すること★

★この方針を採らない("0030" を書く)場合も、採らなかった理由を記録すること。★

段階4の完了条件

  • ★lib/db/index.ts の schedules() が notImplemented() でなくなっていること★
  • ★active: row.inactive === 0 になっていること★(grep -n "Boolean(.*inactive" lib/ が0件)
  • ★weekdays_label の生成が純粋関数で、期待値表(4-4)のケースがテストにあること★
  • ★end_time の 24〜29時が純粋関数で整形され、期待値表(4-5)のケースがテストにあること★
  • ★lib/schedules/query.ts / app/api/schedules/route.ts / 画面に差分が無いこと★
  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス(★テスト数が増えていること★)
  • ★ここで1コミット★

段階5 — ★★動作確認(ここが本題)★★

★ここまでの4段階は準備です。この段階で「本当に DB から出ているのか」を確かめます。★

5-1. 起動

  • ★.env.local の USE_FIXTURES を false にする★ (★lib/db/index.ts:48 は process.env.USE_FIXTURES !== "false" なので、★文字列 false 以外はすべて fixtures 扱い★です。FALSE や 0 では切り替わりません)
  • ★npm run dev を Bash で起動しないこと。Browser pane の preview_start を使う★ (★.claude/launch.json に d-watch(npm run dev / port 3000)が既にあります★ — 実測)
  • ★環境変数を変えたら dev サーバを再起動し、反映されていることを確認すること★

5-2. ★★確認1 — 画面が実DBのデータで表示されること★★

# 項目
1 ★/facilities が表示され、★フッタが 全12施設★であること★
2 ★ロボット3台の施設(メガバリューマート緑野店)が3行で出ること★
3 ★ロボット0台の施設(フレッシュタウン湖畔店)で表示が崩れないこと★
4 ★監視 / D-Sec 列が施設ごとに正しく出ていること★ — ★全部 ー になっていたら contract の変換が壊れています★(D-080 の典型的な失敗)
5 ★/schedules が表示され、★12行★であること★(★13行ではない。施設12 を除外したため★)
6 ★23:30 - 翌00:30(id27)が出ていること★ — ★00:30 のままなら段階4 の整形が効いていません★
7 ★無効行(id24。バリューマート南丘駅前店)だけが薄く表示されていること★ — ★全行が薄い / 1行以外が薄いなら inactive の反転を間違えています★

5-3. ★★確認2 — USE_FIXTURES の true と false でレスポンスが一致すること★★

★これが「変換が正しい」ことの一番強い証拠です。★

# 両モードで叩いて保存し、差分を見る(★dev サーバの再起動を挟むこと★)
見るもの 期待
★施設名(12件すべて)★ ★完全一致★
★契約フラグ(monitoring_enabled / d_security_enabled)★ ★完全一致★
★ロボットのシリアル・メーカー・機種★ ★完全一致★
★meta.total★ ★12 で一致★
★施設の id / ロボットの id★ ★段階2 で id を明示していれば完全一致★

★意図的に一致しないもの(★事前に宣言しておきます★)★:

# 不一致 理由
1 ★/api/schedules の件数(fixtures 13 / DB 12)★ ★施設12 のスケジュールを除外したため★(確認2 の方針)
2 ★id27 の end_time(fixtures "00:30" / DB "翌00:30")★ ★段階4 の 4-6。DB 版のほうが正しい★

★上の2件以外に差分が出たら、それは変換のバグです。★ ★潰してから次に進んでください。★

5-4. ★★確認3 — 不整合が消えたこと★★

# 項目
1 ★/schedules に「フレッシュタウン湖畔店」が1行も出てこないこと★(★ロボット0台なのでスケジュールが存在しないのが正しい★)
2 ★/facilities には「フレッシュタウン湖畔店」が出ること★(★施設としては存在する。ロボット列が空になるだけ★)
3 ★この2つが同時に成り立っていることが、「DB にしたら矛盾が消えた」の実物です★。★段階6 で記録すること★

5-5. ★★確認4 — 他の画面が落ちることを「確認して記録する」★★

★これは失敗ではありません。第1歩の想定どおりの状態です。★

USE_FIXTURES=false では、★実装していない10メソッドが notImplemented() のまま★なので、 ★以下の画面は 500 になります★:

画面 呼んでいる未実装メソッド
/dashboard cleanLogs / prechecks / dsAlerts ほか
/detail-log cleanLogs
/alerts prechecks / cleanLogs
/reports reportKpis / facilityPerformance / continuousProblems / errorBreakdown
/ds-logs dsDevices
/ds-alerts dsAlerts
# やること
1 ★実際に開いて、どの画面がどのエラーメッセージで落ちるかを記録すること★
2 ★エラーメッセージが [db] xxx のPrisma実装はまだありません。USE_FIXTURES=true で開発してください。 になっていること★(lib/db/index.ts:134-138)。★これが「黙って空配列を返さない」設計が効いている証拠です★
3 ★第2歩の対象として段階6 に申し送ること★

5-6. ★★確認5 — 終わったら USE_FIXTURES を true に戻すこと★★

★忘れないでください。★ ★.env.local は .gitignore:34 の .env* で追跡外なのでコミットはされませんが、 ★戻さないと、次にこのリポジトリを触る人(あなた自身を含む)が全画面 500 に出くわします★。

5-7. ★ログの確認★

  • ★read_console_messages でブラウザのコンソールエラーを確認する★
  • ★preview_logs でサーバ側のエラーを確認する★
  • ★BigInt のシリアライズ例外(Do not know how to serialize a BigInt)が出ていないこと★(変換表の1行目)

段階5の完了条件

  • ★確認1〜確認5 をすべて実施したこと★
  • ★確認2 の差分が「意図的に一致しない2件」だけであること★
  • ★確認4 の結果(どの画面がどう落ちたか)を控えたこと★
  • ★USE_FIXTURES が true に戻っていること★
  • ★コミットしない★

段階6 — 記録と push

6-1. docs/decisions.md に新しい決定を追記する

★着手直前に grep -n "^### D-0" docs/decisions.md で最終番号を確認すること★ (★作成時点の最終は D-085(:2782)なので、最初の空き番号は D-086★)。

記録すべきもの:

# 内容
1 ★ローカルDBで動かす方針にした理由★ — ★fixtures が画面ごとに手書きされているため、ファイル間で辻褄が合わなくなる★。★実際に見つかった2件(施設12 のロボット0台問題 / 「緑野第二店」がマスタに無い問題)を具体的に書くこと★
2 ★dwatch_local と dwatch_ddl の役割の違い★ — ★前者は書き込み可能な作業用、後者は受領した実DDL の参照専用★
3 ★★dwatch_local は現行の prisma/schema.prisma と一致していないこと★★ — ★robots.task_id / version、clean_schedules.started_at / ended_at が DB 側にだけある★。★帰結として db pull / db push / migrate を dwatch_local に対して実行してはいけない★
4 ★customers に contract 列が無いことを実DDLで確認できた★(D-010 が実際に反映済み)
5 ★dwatch_ddl.robot_error_reports に PRIMARY KEY が無いことを確認できた★(★D-066 の A-1 未反映の裏取り★)。★ついでに UNIQUE のインデックス名(..._report_id_unique)と対象列(error_id)が食い違っていることも書く★
6 ★★変換表★★ — ★後任がこれを見れば第2歩を書ける形にすること★(本指示書の「調査済みの事実」6 をそのまま持っていってよい)
7 ★devices.maker_id の採番(確認1 の結果)★と、★それが DFA 側の暫定決定であること★(D-057 A-1: makers テーブルは提供されない)
8 ★施設12 のスケジュールをシードから除外したこと★と、★構造的な規則で落としていること★、★第2歩で清掃ログ側も除外すれば 9対9 で均衡すること★
9 ★inactive では絞らない(deleted_at IS NULL でのみ絞る)と決めたこと★と、その理由(段階3 の 3-4)。★仕様が未確定であることを明記する★
10 ★id27 の end_time を DB に "2430" で入れ、API が "翌00:30" を返すこと★(★fixtures の "00:30" とは意図的に食い違う。DB 版が正しい★。D-060)
11 ★シードの実行方法(npm run seed:local か prisma db seed か)と、そう決めた理由★(段階2 の 2-2)
12 ★lib/fixtures/ は残すこと★(CLAUDE.md §9.5)。★テストは fixtures と純粋関数のままで、DB 依存にしていないこと★
13 ★★第2歩に残した10メソッドの一覧★★ — cleanLogs / prechecks / dsDevices / dsAlerts / inspections / inspectionDetail / reportKpis / facilityPerformance / continuousProblems / errorBreakdown(★着手時に lib/db/index.ts を読んで実測し直すこと★)
14 ★USE_FIXTURES=false で落ちる画面の一覧(段階5 の確認4 の実測)★
15 ★ds-alerts / ds-devices の「緑野第二店」の不整合が未解決で残っていること★(★第2歩で解消する★)
16 実測値(npm test のファイル数・テスト数 / npx tsc --noEmit / npm run lint)

6-2. docs/handoff.md に追記する

  • ★末尾に追記を1つ追加★(★作成時点の最新は「追記20」(docs/handoff.md:1728)。着手時に確認すること★)
  • ★冒頭の「最新の追記」ポインタも更新すること★
  • ★次にやること(第2歩)を書く★:
    • ★残り10メソッドのシードと実装★
    • ★「緑野第二店」の不整合の解消★
    • ★ダッシュボードの planned 均衡(清掃ログのシードとセット。9対9)★
    • ★実DBへの接続(★接続先ホスト名が未受領★。D-078 項1)★
  • ★docs/handoff/analysis/prismaSource実装前チェックリスト.md にも、★本作業で判明した事実(特に「調査済みの事実」2 と 3)を追記すること★★

6-3. コミットと push

★git add は明示的にファイルパスを指定すること。git add -A は禁止★ (過去に別作業の途中ファイルを巻き込む事故が起きています)。

git status                       # 何が変わったか目で確認する
git add docs/decisions.md docs/handoff.md
git add "docs/handoff/指示書M_ローカルDBで動かす第1歩.md"
git commit -m "docs: 指示書M完了(D-0XX)を記録する"
git push origin develop
  • ★.env.local を git add しないこと★(禁止事項17)
  • ★main へは push しないこと★。CLAUDE.md §6-5 のとおり main へのマージは PR 経由で、もう1人のレビューを通してから行います
  • --force は使わないこと
  • ★403 が出たら、無理に回避せずユーザーへ報告してください★ (認証情報を画面に出すコマンドを実行しないこと。git credential fill は禁止)

push 後に確認すること:

git log --oneline -1 origin/develop   # ローカルの HEAD と同じコミットか
git status -sb                        # 1行目に ahead/behind が無いか

段階6の完了条件

  • docs/decisions.md に新しい決定(★着手直前に確認した番号★)が追記されていること
  • ★記録すべきもの16項目がすべて書かれていること★(★特に #3 / #6 / #13 の3件は後任が必ず読む★)
  • docs/handoff.md の末尾に追記があり、冒頭のポインタも更新されていること
  • ★origin/develop へ push 済みで、git status -sb に ahead/behind が無いこと★
  • 文書末尾の「実行状態」表が埋まっていること

★やらないこと(第2歩に回すもの)★

対象 理由
★残り10メソッドの実装★(cleanLogs / prechecks / dsDevices / dsAlerts / inspections / inspectionDetail / reportKpis / facilityPerformance / continuousProblems / errorBreakdown) ★第1歩の範囲を超えます★。★正確な一覧は lib/db/index.ts:140-177 を読んで実測すること★(作成時点は12メソッド中10メソッドが notImplemented)
★清掃ログ・事前チェック・D-Sec・レポート・点検記録のシード★ 上記とセット。★サマリテーブル(週次/月次/年次)と当期のその場集計という分岐があり、第1歩の3倍の分量になります★
★ds-alerts / ds-devices の「緑野第二店」の不整合★ ★dsDevices() / dsAlerts() の実装とセット★。★第1歩では記録だけする★
★ダッシュボードの planned 均衡(9対9)★ ★清掃ログのシードとセット★。★第1歩では /dashboard はそもそも 500 になります★
★実DB(TeamWorks 側)への接続★ ★接続先ホスト名が未受領★(D-078 項1。.env.local は <HOST> のまま)
★lib/fixtures/ の不整合そのものの修正★ ★fixtures の変更は不変条件テスト(lib/fixtures/__tests__/schedules.test.ts)に波及します★。★DB 側で正しい形にすることが本作業の目的★
★画面・API ルート・クエリ純粋関数の変更★ ★禁止事項14★。★これらが変わらないまま動くことが証明になります★
★prisma/schema.prisma の変更★ D-056 / D-059
★マイグレーション・db push・db pull★ 禁止事項3・4・5
★点検記録(inspections)★ ★D-072 項7★。★写真の置き場所が未決★。★そもそも別ストア(SQLite)で、メインDBの話ではありません★(CLAUDE.md §3)

完了条件(全体)

  1. ★着手前確認2件の回答を得たこと★(maker_id の採番 / 施設12 のスケジュール除外)
  2. 段階0〜6 をすべて実施したこと(★途中で止めた場合は、どこで止めたかを実行状態表に書くこと★)
  3. npx tsc --noEmit エラー0 / npm run lint 0
  4. npm test 全パス。★テスト数が着手前の実測値から増えていること★(作成時点の基準: 24ファイル / 530テスト)
  5. ★lib/db/index.ts の facilities() と schedules() が notImplemented() でなくなっていること★
  6. ★残り10メソッドは notImplemented() のままであること★(★勝手に実装しない★)
  7. ★シードが冪等で、facilities 12行 / clean_schedules 12行 になること★
  8. ★contract の変換が lib/format/ の純粋関数1箇所にあり、桁位置が lib/config/ から来ていること★
  9. ★grep -rn "Number(.*contract\|parseInt(.*contract\|Boolean(.*inactive" lib/ が0件であること★
  10. ★git diff --stat prisma/ lib/fixtures/ lib/facilities/ lib/schedules/ app/api/facilities/ app/api/schedules/ components/features/facilities/ components/features/schedules/ が空であること★
  11. ★USE_FIXTURES の true / false で /api/facilities のレスポンスが一致すること★(★意図的な不一致2件を除く★)
  12. ★/schedules に「フレッシュタウン湖畔店」が出ないこと★
  13. ★USE_FIXTURES が true に戻っていること★
  14. ★prisma/migrations/ にファイルが1つも作られていないこと★
  15. ★.env.local がコミットされていないこと★(git log -1 --stat で確認)
  16. ★シードに実在の住所・電話番号・社名・人名が入っていないこと★
  17. docs/decisions.md / docs/handoff.md の更新
  18. ★origin/develop へ push 済み★(★main へは push しない★)
  19. 文書末尾の「実行状態」表を更新した

実行状態

段階 状態 コミット
★着手前の確認(maker_id の採番 / 施設12 の除外)★ ★済(2026-09-08 / D-085)★ 7153abe
段階0 前提の確認 ★済★ (コミットなし)
段階1 Prisma クライアントを生成する ★済★ (コミットなし。生成物は.gitignoreで追跡外)
段階2 シードスクリプトを作る ★済★ a5502ac
段階3 prismaSource.facilities() ★済★ be01922
段階4 prismaSource.schedules() ★済★ 4999155
段階5 動作確認(★Edge Runtimeバンドル破壊を発見・修正★) ★済★ 553b3cc(修正のみ。段階5自体はコミットなし)
段階6 記録と push ★済★ 本コミット

実行日: 2026-09-09 取得した決定番号: D-086(着手前確認はD-085) 着手前の実測値: 24ファイル / 530テスト / tsc 0 / lint 0(2026-09-09 実測。Vitest 4.1.11 / Node v24.14.0 / MySQL 8.4.9 / Prisma 7.10.0) maker_id の採番: 1=PUDU / 2=Keenon(確認1どおり) 施設12 のスケジュール: シードから除外(id33。構造的な規則でスキップ、実測1件) 完了後の実測値: 28ファイル / 572テスト / tsc 0 / lint 0 実行環境: ユーザーが「このセッションはCursorのターミナル上で動いているClaude」と明示したため、Claude Code側のセッションが段階1〜6を直接実装した(サブエージェント委譲なし。段階2〜4完了後、独立2視点でforkレビューを実施し問題なしを確認) 詳細: docs/decisions.md D-086 / docs/handoff.md 追記22 を参照