Newer
Older
client_dwatch / docs / handoff / 指示書T_ローカルDBで動かす第5歩_レポートサマリ.md

指示書T — ローカルDBで動かす第5歩(レポートサマリ3本を実DB経由にする)

項目 内容
実行環境 ★Cursor 内の Claude が実装します★(.claude/commands/d-watch.md の役割分担表を参照)
実行状態 ★完了(D-108。2026-09-11)★
実行順 ★指示書S(第4歩・prechecks)の続き★。★指示書M / P / R / S が作った scripts/seed-local-db.ts / lib/db/prisma.ts / lib/db/local-guard.ts / lib/db/use-fixtures.ts / lib/db/ds-status.ts / lib/db/clean-log-mapping.ts / lib/format/date.ts の上に積む作業★です。★prismaSource の既存6メソッド(facilities / schedules / dsDevices / dsAlerts / cleanLogs / prechecks)は1文字も変えません★。★触るコードが重なる未実行の指示書は、着手直前に自分で確認すること★(docs/decisions.md / docs/handoff.md を共通で触る指示書と同時に走らせないこと)
対象メソッド ★3本★: prismaSource.reportKpis() / facilityPerformance() / errorBreakdown()。★continuousProblems は含めない★(冒頭参照)
取得する決定番号 ★**着手直前に `grep -nE "^### D-(0\ 1)[0-9][0-9]" docs/decisions.md \ tail -3で最終番号を実測すること**★。★★^### D-0だけでは3桁台(D-100 以降)を拾えない★★。★2026-09-07 にD-068` の二重採番が実際に起きています★
★D-102 について★ ★D-102「レポートのサマリはサマリテーブルを読む。履歴からの再計算はしない」= docs/decisions.md:5139★(★本指示書の執筆中に記録された。作成開始時点では未記録だったので、★行番号は着手時に自分で取り直すこと★★)。★内訳: 項2(決定と責任分界)= :5163 / ★項4(どのサマリを読むか。D-090 項11 の解決)= :5181★ / ★項5(当期は詳細から算出)= :5198★ / 項7(帰結。第5歩は3本、continuousProblems は第6歩)= :5212★
前提 ★D-090 項11(docs/decisions.md:3397。customer / facility どちらを読むか未決)★ / ★項12(:3398。率が整数 → ★D-094 で解消済み★)★ / ★項13(:3399。ContinuousProblem.summary の生成規則が未定義)★ / ★項14(:3400。当期集計の置き場所が未決)★ / ★項15(:3406。DataSource は引数を取らない)★ / ★項16(:3407。judge を prismaSource で呼ぶのは妥当)★ / ★D-094(:3822。率が double(4,1) に)★ / ★D-071(:763。月跨ぎ週)★ / ★D-072 項4(:921。week=0 が月次)★ / D-096(:4117。schema 追従)/ D-099(:4442。指示書R)/ D-101(:4784。認証)/ D-077(:1847。エラー値を変換しない)/ D-042(:226。書き込み4カラム)/ D-050(:308)/ D-052(:325)/ D-056(:353)
段階数 ★7段階★(段階0〜段階6)
★着手前確認★ ★★なし(回答済み。2026-09-10)★★ — ★consecutive_problem_days は ★A案★(computeConsecutiveProblemDays()(lib/db/clean-log-mapping.ts:112)を流用)で確定★。★テーブル定義書の全29テーブルに該当カラムは存在しない(実測。ヒット0件)★。★根拠と3案の比較は後述の「着手前確認」節★ / ★記録は docs/decisions.md の D-103(:5222)★。★★本書に未回答の確認事項は1件も無い★★(★D-104 項5 で確認済み★)。★★ユーザーの回答を待たずに段階0 から通しで実行してよい★★
着手前の基準 ★着手直前に自分で npm test / npx tsc --noEmit / npm run lint を実行し、その実測値を基準にすること★(D-099 の完了値は「34ファイル / 658テスト / tsc 0 / lint 0」だが、★鵜呑みにしない★。指示書S がこの上に足している可能性がある)
作成 2026-09-10(作成時点の決定事項最終番号 D-101 / docs/handoff.md の最新は 追記30(docs/handoff.md:2361 付近。★着手時に実測すること★))

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

★この指示書を読んでいるあなたへ★ — .claude/commands/d-watch.md の「実装は Cursor 側」は 2つの環境の役割分担です。Cursor の中で動いている Claude は実装担当です。 ★調査や改訂ではなく実装を行ってください。★ ★★うまくいかなければ段階の切れ目(コミット単位)で止めて構いません★★ — ★本書は API契約(d-watch-api.yaml)も型(lib/types/index.ts)も変えないので、 USE_FIXTURES=true に戻せば画面は元に戻ります★。


★★冒頭に読むべきこと — なぜ3本で、continuousProblems を含めないのか★★

★本指示書の範囲は reportKpis / facilityPerformance / errorBreakdown の3本です。★ ★★continuousProblems には手を付けません。★★ 理由は2つ:

# 理由
★1★ ★★対応するサマリテーブルが存在しない★★ — customer_summaries / facility_summaries / customer_error_summaries / facility_error_summaries の4本(prisma/schema.prisma:667 / :701 / :735 / :755。★実測して確認すること★)のどれも「連続問題日数」を持たない。★詳細テーブル(clean_timetables × clean_status_histories)から算出するしかない★ので、★他3本の「サマリをそのまま読む」という構造とまったく違う★
★2★ ★★ContinuousProblem.summary(自由文)の生成規則が未定義★★(★D-090 項13。docs/decisions.md:3399)。★「完了率が5日連続で30%未満。マップ再作成またはオンサイト訪問を検討」のような文(lib/fixtures/reports.ts:340-346)を DB からどう組み立てるかが決まっていない★。★推測で実装しない★(CLAUDE.md §7)

★→ continuousProblems は 第6歩(別の指示書)で扱います★。 ★/reports 画面の「連続問題施設」セクションは、本書の作業後も USE_FIXTURES=false では notImplemented で 500 になります★(lib/db/index.ts:497-499)。★これは想定内★。 ★段階5 の動作確認では、この1本だけが落ちることを確認して記録すること★。


★★方針 — 確定期間のレポートはサマリテーブルをそのまま読む(D-102)★★

★★履歴(詳細テーブル)から率を再計算しないこと。★★ 理由は ★責任分界★ です。

CLAUDE.md §2「責任分界 — 何を作り、何を作らないか」(★CLAUDE.md:49★)の表:

行 記述(★実測★)
★CLAUDE.md:55★ ` サマリ集計バッチ(顧客/店舗/障害サマリ) TeamWorks `
★CLAUDE.md:58★ ` 当期(進行中の週/月/年)のレポート集計 DFA(内製) `

★つまり「確定した期間の集計は TeamWorks の成果物であり、DFA はそれを読むだけ」「進行中の期間だけ DFA がその場で集計する」と、責任の線が既に引かれている★。★当方が確定期間を再計算すると、 TeamWorks のバッチ結果と当方の表示が食い違ったときに「どちらが正か」が決められなくなる★。

メソッド 読むテーブル Prisma モデル
★reportKpis★ ★customer_summaries★ prisma.customerSummary(prisma/schema.prisma:667)
★facilityPerformance★ ★facility_summaries★ prisma.facilitySummary(:701)
★errorBreakdown★ ★facility_error_summaries★ prisma.facilityErrorSummary(:755)

★errorBreakdown は facility 版か customer 版か — 実装時に最終確認すること★

★D-090 項11(docs/decisions.md:3397)が「customer 版と facility 版のどちらを読むか未決」と していた★。★★D-102 項4(docs/decisions.md:5181-5196)が「facility_error_summaries を読む。customer_error_summaries は P1 では読まない」と決めたうえで、なお「facility 版にするか customer 版にするかは実装時に画面の要求と突き合わせて最終確認すること」と留保している★★。

  • ★customer_error_summaries(prisma/schema.prisma:735)と facility_error_summaries(:755)は ★customer_id / facility_id 以外まったく同じ構造★(実測して確認すること)
  • ★画面(components/features/reports/ReportsScreen.tsx)が求めているのが 全社の内訳なら customer 版、施設別に足し上げた内訳なら facility 版★。 ★P1 の顧客はドン・キホーテ1社のみ(CLAUDE.md §1)なのでどちらでも同じ値になるはず★ — ★★だからこそ「どちらを読んだか」を決めて記録しないと後から追えなくなる★★
  • ★推奨: facility_error_summaries★(★facilityPerformance と粒度が揃い、将来「施設で絞った エラー内訳」が要求されてもクエリを変えずに済む★)。★ただし画面を読んでから最終判断すること★

★★着手前確認 — ★★なし(回答済み。2026-09-10)★★★★

★以下は解決済みの記録です。★★確認を待つ必要はありません★★(★記録: D-103。D-104 項5 でも確認済み★)。

★FacilityPerformance.consecutive_problem_days をどうするか★ → ★★A案で確定★★

★★ユーザーの問い(2026-09-10。原文)★★:

Tはテーブル定義書を読んで該当しそうなテーブルの候補などないですか

★★指摘のとおり調べるべきだった。調べた結果、該当するテーブル・カラムは存在しない★★:

  • ★テーブル定義書(docs/handoff/analysis/table-definitions-01-master.md / -02-clean-error.md / -03-schedule-summary.md。★全29テーブル★)を ★「連続」「consecutive」「continuous」「継続」★で全文検索した結果、★★ヒット0件★★ (2026-09-10 実測。★着手時に自分でも走らせて確認すること★)
  • ★サマリ系4本(customer_summaries / facility_summaries / customer_error_summaries / facility_error_summaries)にも無い★ — ★持っているのは task_count / launch_count / error_count / task_area / clean_area と 率4本(operation_ratio / launch_ratio / clean_ratio / error_ratio)だけ★ (docs/handoff/analysis/table-definitions-03-schedule-summary.md:106-237 を実測)
  • ★★これは当然でもある★★ — ★D-026(docs/decisions.md:122)が 「連続はカレンダー日ではなくスケジュール存在日の連続」と定めており、 ★これは DFA 側の判定仕様★。★TeamWorks がこの値を持つ理由が無い★

★確定内容 — A案★:

  • ★computeConsecutiveProblemDays()(lib/db/clean-log-mapping.ts:112)を流用する★
  • ★同関数は既に cleanLogs() から呼ばれている★(lib/db/index.ts:299)。 ★同じ関数を使えば /detail-log と /reports で連続日数が食い違わない★
  • ★記録は docs/decisions.md の D-103★

選択肢(★当初の提示をそのまま残す。採用案に ✅★):

案 内容 帰結 採否
★A ✅★ ★cleanLogs() と同じ全件走査を facilityPerformance() の中でも行い、施設ごとに「その期間の最終日時点の連続日数」を入れる★ ★値の意味が cleanLogs の consecutive_problem_days と完全に一致する★。★/reports の表と /detail-log の表で同じ施設の連続日数が食い違わない★。★ただし clean_timetables / clean_status_histories の全件取得がもう1回走る(D-092 項3-3 の性能懸念が2倍になる)★ ★★採用★★ — ★DB に無い以上、当方で算出するしかない★。★算出関数は既にありテストもある(lib/db/__tests__/clean-log-mapping.test.ts:118 以降)★
B ★常に 0 を返す★ ★実装は最小。性能劣化も無い★。★しかし画面の「連続問題日数」列が全施設0になり、★★列の意味が消える★★。★fixtures との突き合わせ(段階5)で全行に差が出る★ ★不採用★ — ★「まだ作れない」ではなく「作る手段が既にある」ので、0 で逃げる理由が無い★。★段階5 の突き合わせが機能しなくなる★
C ★FacilityPerformance.consecutive_problem_days を `number \ nullにし、DB 経由ではnull`(未算出)を返す★ ★「0日」と「算出していない」を区別できる★。★ただし API契約(d-watch-api.yaml)と画面の変更を伴い、★★本書が「契約を変えない」前提で組み立てられていることが崩れる★★ ★不採用★ — ★区別する必要が無い★。★A案なら常に算出できるので「未算出」という状態が発生しない★。★契約変更のコストだけが残る★

★★懸念への手当て★★:

  • ★全件走査が2回になる点(cleanLogs() と facilityPerformance())は、 ★実データで遅ければ後から手を打てばよい★(結果を共有する / 期間で絞れるようにする 等)★
  • ★★まず正しい値を出すことを優先する★★。★段階5 で体感が遅ければ、その事実を段階6 で記録すること★

★D-090 との関係★:

  • ★D-090 項15(docs/decisions.md:3406)が挙げた「lib/db/index.ts:59-63 の方針コメントと consecutive_problem_days(全期間走査)の衝突」は、★A案の採用(=判定も集計も prismaSource 内でやる)で 方向が決まった★。★方針コメントの書き換えを伴うので、段階3 で実施し段階6 で記録すること★
  • ★D-090 項13(:3399。ContinuousProblem.summary の生成規則)は★第6歩の話★なので本書では扱わない★

★★回答済みなので、段階3 に進んでよい★★(★段階1・2 はもともと先に進めてよい★)。

★上記以外で実装者が判断できないものはなし★ — ★errorBreakdown の facility / customer は 画面を読めば決まる★し、★当期集計の置き場所は下記「調査済みの事実」5 で推奨を示している★。


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

1. ★サマリテーブルの構造(2026-09-10 実測。★自分で読み直すこと★)★

★facility_summaries(prisma/schema.prisma:701-733)★:

列 型(Prisma) 備考
id BigInt @id ★Number() が必要★
facility_id BigInt ★FK あり(:726)→ include: { facility: true } が使える★
★year / month / week★ Int(UnsignedSmallInt / UnsignedTinyInt ×2) ★期間キー。下記3参照★
★task_count / launch_count / error_count★ Int @db.UnsignedSmallInt ★分子・分母。検算用★
★task_area / clean_area★ ★Float★(実DDL double(14,6)。D-094) 同上
★operation_ratio / launch_ratio / clean_ratio / error_ratio★ ★Float★(実DDL ★double(4,1)★。D-094) ★★そのまま KpiValue.value に入れてよい★★
created_at / updated_at DateTime ★API に出さない★
  • ★customer_summaries(:667-699)は facility_id が customer_id になるだけで同一構造★ (★実測して確認すること★)
  • ★facility_error_summaries(:755-773)は構造が違う★: id / facility_id / year / month / week / ★error_level(VarChar(20))★ / ★error_type(VarChar(100))★ / ★error_detail(Text)★ / ★error_count★ / created_at。 ★★updated_at が無い★★(:765 で終わり。★実測★)。★operation_ratio 等の率カラムも無い★
  • ★4本すべて deleted_at を持たない★ → ★論理削除の絞り込みを書かない★(書くと型エラー=安全側)

2. ★★重要: 率がもう小数になっている(D-094)★★

  • ★実DDL の operation_ratio / launch_ratio / clean_ratio / error_ratio は ★tinyint unsigned(整数)→ double(4,1)(小数第一位)★に変更済み★ (★D-094 項1-A。docs/decisions.md:3851-3856 の表。customer_summaries:402-405 / facility_summaries:564-567 はDDLファイル内の行番号であってリポジトリの行ではない★)
  • ★prisma/schema.prisma:686-689(customer)/ :720-723(facility)のコメントが 「Prisma型は Int → Float」と明記している★(★実測★)
  • ★★帰結: D-090 項5・項12 は解消済み★★。★「サマリの率が整数なので面積から再計算する必要がある」 という以前の見立てはもう成り立たない★。★★サマリの値をそのまま KpiValue.value に入れてよい★★
  • ★Decimal → number の変換も不要★(D-096 で Float に追従済み。★.toNumber() を書かないこと★)

3. ★期間キーの扱い(D-072 項4 / D-071)★

  • ★★week = 0 が月次、month = 0 AND week = 0 が年次★★(★D-072 項4。docs/decisions.md:921 / 同 :927-931 の表★)。★年次は P1 では読まない★
  • ★ReportPeriodKey.week は monthly のとき 0★(★lib/types/index.ts:241-255。特に :246★)
  • ★API の week は 1以上のみ受け付ける★(lib/reports/query.ts:48 WEEK_MIN = 1 / :46-47)。★週次は 1〜6★(:49 WEEK_MAX = 6。★D-071 の下では第6週が発生する★)
  • ★帰属ルールを知っているのは lib/format/period.ts の weekOfMonth()(:126)だけ★(D-071)。 ★★prismaSource の中で「1日から数えて何本目」のような計算を書き直さないこと★★
  • ★サマリ行の year / month / week はそのまま ReportPeriodKey に写せる★ (★DB 側の規約と内部表現を揃えてあるのがこの設計の狙い。lib/types/index.ts:248-251★)。 ★period は week === 0 ? "monthly" : "weekly" で決まる★

4. ★prev(前期値)の作り方★

  • ★★DB に前期値は無い★★。KpiValue は { value, prev, diff }(lib/types/index.ts:167-180)で、 ★diff は toKpiValue() が計算する★(★lib/judge/kpi.ts:58-64★)
  • ★手順★: ★1つ前の期間の行を引き、toKpiValue(今期の値, 前期の値) を呼ぶ★。★前期の行が 無ければ prev = null(新規施設のケース。toKpiValue() が diff も null にする)★
  • ★★1つ前の期間は shiftPeriod()(lib/format/period.ts:240)で求める★★ — ★★自分で week - 1 を書かないこと★★。★D-071 の下では週の長さが1〜7日で変わり、 第1週の前は「前月の最終週」になる★(:257 のコメント)。 ★shiftPeriod("monthly", y, m, 0, -1) は week: 0 を返す★(:247-255)
  • ★fixtures が同じことをしている★: lib/fixtures/reports.ts:49-70(todayJst() → weekOfMonth() → shiftPeriod())。★この並びをそのまま流用すること★
  • ★実装のコツ★: ★全サマリ行を1回で取り、`${period}/${year}/${month}/${week}` をキーにした Map を作ってから、各行について前期キーを引く★(★N+1 クエリにしない★)

5. ★★当期(進行中の週・月)の扱い — ここが最大の論点★★

事実(★すべて実測★):

# 事実
★1★ ★CLAUDE.md:193★: 「進行中の期間(当週・当月・当年)はサマリテーブルに存在しない。 詳細テーブルからその場集計すること」
★2★ ★DataSource は引数を取らず全行を返す契約★(★lib/db/index.ts:70-75。特に :73「フィルタや集計はこの層では行わず、APIルート側で組み立てます」★ と ★:87-95 の「レポートの4メソッド」コメント(:89「★引数を取らせない★」)★)
★3★ ★つまり prismaSource が「確定期間=サマリ由来の行」と「当期=その場集計した行」を1つの配列に混ぜて返すことになる★
★4★ ★fixtures がまさにその形★ — lib/fixtures/reports.ts:162-167 の reportKpis は ★今週(kpiCurrentPeriod)/ 先週(kpiWeekly)/ 今月(kpiCurrentPeriod)/ 先月(kpiMonthly)の4行★。★:145-152 のコメントが「当期はサマリテーブルに存在しない前提で、DFA側APIが詳細テーブルから集計する(D-023)」と明記★
★5★ ★selectKpiRow()(lib/reports/query.ts:325-340)は当期を判定するが「器だけ」★ — ★:319-323 のコメント★: 「★当期かどうかで集計方法を変える処理はここに書かないこと★ …DataSource は引数を取らず全行を返す契約(lib/db/index.ts)なので、行の選び方は確定期間と当期で同じにしてある。当期判定は『実DB接続時にその場集計へ差し替える分岐点』を示す器としてだけ置いている」。★:332-335 は isCurrentPeriod() が true でも null を返すだけ★
★6★ ★D-090 項14(docs/decisions.md:3400)が「当期その場集計を prismaSource でやるか lib/reports/query.ts でやるか未決」、★項15(:3406)が「DataSource は引数を取らない契約なので API ルート側では期間を渡せない」★としている★

★★これを段階2 の中で決め、記録すること(段階6)。★★

★推奨: prismaSource の中でやる★。理由:

  • ★DataSource は引数を取らないので、API ルート側では「どの期間を集計すべきか」を データ層に伝えられない★(D-090 項15)
  • ★★指示書R で cleanLogs() が既に同じことをしている前例がある★★ — ★lib/db/index.ts:299(computeConsecutiveProblemDays())と :307・:309 (judgeGrade() / judgeEscalationLevel())★。★実測して確認すること★。 ★D-090 項16(docs/decisions.md:3407)が「judge を prismaSource の中で呼ぶこと自体は妥当」と結論済み★
  • ★lib/reports/query.ts:319-323 が「実DB接続時は、この分岐を『詳細テーブルからその場集計』に 差し替える」と書いている★。★★その実体を prismaSource に置くと決めれば、 lib/reports/query.ts は1文字も変えずに済む★★(selectKpiRow() は「行があれば返す」だけになる)
  • ★決めたら lib/db/index.ts:70-75 の設計方針コメントに例外を明記すること★ (★D-090 項15 が「方針コメントの書き換えを伴う」と指摘★。★cleanLogs() と同じ扱いで1行足せばよい★)

★★当期の集計元は詳細テーブル★★ — ★clean_timetables × clean_status_histories × robot_error_reports(cleanLogs() が既に読んでいるもの)を期間で絞り、lib/judge/kpi.ts の operationRate()(:26)/ launchRate()(:31)/ completionRate()(:36)/ errorRate()(:44)に通す★。 ★★期間の範囲は periodRange()(lib/format/period.ts:157)で求めること★★(自分で日付計算をしない)。 ★completionRate() は面積ベース(CLAUDE.md §4.2 / lib/judge/kpi.ts:35)★。

6. ★落とし穴(★現物で確認すること★)★

# 落とし穴
★1★ ★率は Float(double)。Decimal → number の変換は不要★(D-096 で追従済み)。★.toNumber() を書くと型エラーになる★
★2★ ★BigInt → Number()★ を忘れると JSON.stringify が例外を投げる(NextResponse.json が 500。★tsc は捕まえない★)。★id / facility_id / customer_id が該当★
★3★ ★サマリ4本に deleted_at は無い★ → ★論理削除の絞り込みを書かない★
★4★ ★task_count / launch_count / error_count は Int @db.UnsignedSmallInt★ → ★Prisma 上は number なので Number() は不要★。★ただし型を自分で確認すること★
★5★ ★★サマリが1行も無い期間がある★★(当期)。★selectKpiRow() が null を返し、ルート側が emptyKpi() を組み立てる設計(app/api/reports/kpi/route.ts:32-40 / :70)を壊さないこと★
★6★ ★facility_summaries は FK があるので include: { facility: true } が使える★(prisma/schema.prisma:726)。★cleanLogs() の手動 join(FK が無い3テーブルの話)はここでは不要★
★7★ ★★TeamWorks のバッチの計算式が資料に無い★★(★D-090 項12。docs/decisions.md:3398 末尾「テーブル定義書に計算式が書かれておらず、DFA の再計算値と TeamWorks のバッチ値が一致する保証がない」★)。★当方はサマリの値をそのまま使うので当面は問題にならない★。★ただし分子・分母(task_count / launch_count / error_count / task_area / clean_area)から検算すると食い違う可能性がある★。★★検算するなら「食い違いを見つけて記録するため」であって「当方の計算値で上書きするため」ではない★★
★8★ ★error_ratio の分母は「清掃実施回数」(CLAUDE.md §4.2 / lib/judge/kpi.ts:43)★。★「予定回数」ではない★。★当期集計を書くときに間違えやすい★

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

# ★してはいけないこと★ 理由
★1★ ★★確定期間の率を履歴(詳細テーブル)から再計算すること★★ ★D-102 / CLAUDE.md:55。★サマリ集計バッチは TeamWorks の担当領域★。★率はもう double(4,1) で小数のまま入っている(D-094)ので、再計算する技術的理由も消えた★
★2★ ★★continuousProblems に手を付けること★★ ★第6歩の範囲★。★対応テーブルが無く、summary の生成規則も未定義(D-090 項13)★
★3★ ★★エラー種別を日本語に変換すること / 7分類に寄せること★★ ★D-077(docs/decisions.md:1847)/ D-088★。★error_type の生値を type と label の両方に入れる★
★4★ ★★当期(今週・今月)の行をサマリテーブルにシードすること★★ ★実データでは当期の行は存在しない(CLAUDE.md:193)★。★入れてしまうと当期集計の実装を検証できない★
★5★ ★閾値(90/70/10・75・猶予30分・L2=2日/L3=3日)をハードコードすること★ CLAUDE.md §7。★lib/config/ から読む(FACILITY_STATUS_THRESHOLDS は lib/config/index.ts:84-89)★
★6★ ★★メインDB(TeamWorks 側)に接続・書き込みすること / dwatch_ddl に書き込むこと★★ CLAUDE.md §2 / §7 / D-042。★作業は dwatch_local の中だけ★。★接続先ホスト名は未受領★
★7★ ★prisma migrate / db push / db pull を使うこと / prisma/schema.prisma を手で書き換えること★ CLAUDE.md §7 / D-056 / D-059。★テーブルは既にある★
★8★ ★prismaSource の既存6メソッド(facilities / schedules / dsDevices / dsAlerts / cleanLogs / prechecks)を変更すること★ ★指示書M / P / R / S の成果物★。★本作業は「その隣に3本足す」だけ★
★9★ ★lib/db/prisma.ts / local-guard.ts / use-fixtures.ts / ds-status.ts / clean-log-mapping.ts を変更すること★ ★完成しており、そのまま効く★。★clean-log-mapping.ts は読んで流用するだけ★
★10★ ★lib/reports/query.ts / API ルート4本 / 画面(components/features/reports/)を変更すること★ ★本作業はデータ層の差し替え★。★これらが1文字も変わらないまま画面が実DBで動くことが、変換が正しいことの証明★。★例外は着手前確認でC案を選んだ場合のみ★
★11★ ★lib/fixtures/ を削除・改変すること★ CLAUDE.md §9.5。★段階1 は fixtures を読んでシードに変換するだけ★
★12★ ★書き込み許可4カラム以外に書き込むこと★ ★D-042★。★サマリ4本はすべて読み取り専用(4カラムに含まれない)★。★本作業は読むだけ(シードを除く)★
★13★ ★テストを DB 依存にすること★ ★遅く・不安定になり、DB が空の環境で落ちる★。★テストは fixtures と純粋関数だけ★
★14★ ★npm run dev を Bash で起動すること★ ★Browser pane の preview_start を使う★(段階5)
★15★ ★git add -A を使うこと★ CLAUDE.md §7。★必ずファイルパスを明示★
★16★ ★.env.local をコミットすること / DB の認証情報を文書やコードに書くこと★ CLAUDE.md §9.3。★確認後は USE_FIXTURES=true に戻す★

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

段階 内容 コミット
段階0 前提の確認(★読むだけ★) コミットなし
段階1 ★サマリ3テーブルのシードを足す★ 段階1で1コミット
段階2 prismaSource.reportKpis() 段階2で1コミット
段階3 prismaSource.facilityPerformance() 段階3で1コミット
段階4 prismaSource.errorBreakdown() 段階4で1コミット
段階5 ★動作確認★ コミットなし
段階6 記録と push 段階6で1コミット + push

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


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

読むもの

# 対象 見る点
★1★ ★D-102(docs/decisions.md:5139。★行番号を実測すること★)★ ★項2 / 項4 / 項5 / 項7★。★方針・読むテーブル・当期の扱い・第5歩の範囲がすべてここにある★
★2★ ★CLAUDE.md:49-58 / :193★ ★責任分界の2行と「進行中の期間はサマリに存在しない」★
★3★ ★D-090 項11 / 項12 / 項13 / 項14 / 項15 / 項16★(docs/decisions.md:3397-3407) ★未決の原文★
★4★ ★D-094 項1-A★(:3822 以降) / ★D-072 項4★(:921) / ★D-071★(:763) ★率の小数化 / week=0 / 月跨ぎ週★
★5★ ★★prismaSource.cleanLogs()(lib/db/index.ts:186-320)★★ ★これが雛形★。★遅延 import(:187-188)/ Number() / toJstIso() / judge の呼び出し(:299 / :307・:309)/ docstring の書き方をそのまま流用する★
★6★ ★lib/reports/query.ts(全340行。特に :280-287 :295-314 :316-340)★ ★当期の扱い(「器だけ」)と matchesPeriod() の4項目比較★
★7★ ★lib/fixtures/reports.ts:41-127★ ★4期間ぶんの行をどう作っているか★(todayJst() → weekOfMonth() → shiftPeriod() → PERIOD_KEYS(:111)→ forEachPeriod()(:119))
★8★ ★lib/format/period.ts:126(weekOfMonth)/ :157(periodRange)/ :197(isCurrentPeriod)/ :240(shiftPeriod)★ / ★lib/judge/kpi.ts(全82行)★ ★期間計算は全部ここにある。自分で書き直さない★ / ★toKpiValue()(:58)/ 4指標(:26 :31 :36 :44)/ judgeFacilityStatus()(:75)★
★9★ ★scripts/seed-local-db.ts(全784行。特に :242-260 :392-405 :762-774)★ ★deleteMany の逆順・件数出力の作法★ / ★prisma/schema.prisma:667-773(サマリ4本の実際の構造)★

接続先の確認とこの段階の完了条件

  • ★.env.local の DATABASE_URL が dwatch_local を指していることを確認する★。 ★値そのものを画面・ログ・文書に出さないこと★(CLAUDE.md §9.3)
  • ★.env.local に ALLOW_DIRECT_ACCESS=true があることを確認する★(★作成時点では キーの存在を実測済み。値は確認していない★。D-101 項2。★無いと proxy.ts が全ルートを止める★)
  • ★着手前の基準値(npm test のファイル数・テスト数 / tsc / lint)を自分で実測して控えた★
  • ★★着手前確認(consecutive_problem_days)の回答を得た★★(段階3 の前までに) / ★D-102 の存在を確認した(無ければ方針をユーザーに確認した)★

段階1 — ★サマリ3テーブルのシードを足す★

1-1. 作るもの

★scripts/seed-local-db.ts に3テーブルの投入を足す★。★★挿入順(FK依存)★★:

  1. ★customer_summaries★ ← 親は ★customers(シードは1件のみ。scripts/seed-local-db.ts:411-415。★customer_id = 1★)★
  2. ★facility_summaries★ ← 親は ★facilities★
  3. ★facility_error_summaries★ ← 親は ★facilities★

★customer_error_summaries は投入しない★(★errorBreakdown を customer 版にすると決めた場合のみ代わりに投入する★)。

1-2. ★lib/fixtures/reports.ts から変換して INSERT する★

★第1〜4歩と同じ方針(fixtures を DB の形に落とす)★:

シード先 変換元
customer_summaries ★kpiWeekly(lib/fixtures/reports.ts:130)/ kpiMonthly(:138)★。★*.value を *_ratio に入れる★
facility_summaries ★facilityPerformanceItems(:177 付近から12件)★。★facility_id が13以上の行は施設マスタに無いので構造的に弾くこと★(continuousProblemItems の話だが、念のため確認する)
facility_error_summaries ★errorCounts(:463-477。9件)★。★error_type をそのまま、error_count をそのまま★。★error_level / error_detail は既存のダミー定数(scripts/seed-local-db.ts:229-231 の ERROR_LEVEL_PLACEHOLDER / ERROR_DETAIL_PLACEHOLDER)を使う★
  • ★*_count / *_area は「率と辻褄が合う値」を作ること★(★検算できるようにするため。 例: task_count=100, launch_count=94 なら launch_ratio=94.0★)。 ★★規則をコメントに書くこと★★
  • ★★率は小数第一位に丸めて入れること★★(double(4,1)。★91.43 は入らない★)
  • ★実在の住所・電話番号・社名・人名を入れないこと★(CLAUDE.md §7 / D-043)

1-3. ★★当期(今週・今月)はサマリに入れないこと★★

  • ★fixtures は4期間(今週 / 先週 / 今月 / 先月)を持つ★(lib/fixtures/reports.ts:111-116 の PERIOD_KEYS。★実測して確認すること★)
  • ★★シードに入れるのは「先週」と「先月」の2期間だけ★★。★今週・今月は入れない★ — 理由: ★実データでは当期の行が存在しない(CLAUDE.md:193)のでそれを再現する★。 ★★入れてしまうと段階2 の当期集計が一度も走らず、実装を検証できない★★
  • ★期間キーは実行日から相対で作ること★(★todayJst() → weekOfMonth() → shiftPeriod(..., -1)。 lib/fixtures/reports.ts:49-70 と同じ★)。★★日付を固定すると時間が経って「先週」が 当期になったり2期間前になったりする★★
  • ★week は月次なら 0、週次なら 1〜6★(D-072 項4)

1-4. ★冪等にすること・検算★

  • ★deleteMany を挿入の逆順で先頭に足す★(scripts/seed-local-db.ts:392-405 の並びを崩さない。 ★facilityErrorSummary → facilitySummary → customerSummary の順で、 facility.deleteMany() / customer.deleteMany() より前★)
  • ★件数を console.log に出す★(:762-774 の完了ログに足す)/ ★2回続けて実行して結果が同じであること★

この段階の完了条件

  • ★npm run seed:local が通り、3テーブルの件数が想定どおり★ (★customer_summaries = 2(先週・先月)/ facility_summaries = 施設数 × 2 / facility_error_summaries = 9 × 2 の見込み。★自分で数えること★)
  • ★2回実行して同じ結果になること★ / ★★当期(今週・今月)の行が0件であること(SQL で確認)★★
  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス / ★ここで1コミット★

段階2 — prismaSource.reportKpis() を実装する

2-1. 作るもの

★lib/db/index.ts:491-493 の notImplemented("reportKpis") を実装に置き換える★。 ★cleanLogs()(:186)と同じ形にすること★(遅延 import → クエリ → 変換 → map)。

2-2. ★確定期間: customer_summaries を読む★

  1. ★prisma.customerSummary.findMany()(★全件。DataSource は引数を取らない★)★
  2. ★`${period}/${year}/${month}/${week}` をキーにした Map を作る★ (period は ★week === 0 ? "monthly" : "weekly"★)
  3. ★各行について shiftPeriod() で1つ前の期間キーを求め、Map から前期行を引く★
  4. ★toKpiValue(row.operation_ratio, prev?.operation_ratio ?? null) を4指標ぶん★
  5. ★★month === 0 && week === 0(年次)の行は除外する★★(P1 では読まない。D-072 項4)

2-3. ★★当期: 詳細テーブルからその場集計する★★

★「調査済みの事実」5 の推奨(prismaSource の中でやる)に従う場合★:

  1. ★todayJst()(lib/format/date.ts:19)から当週・当月の期間キーを作る★ (★weekOfMonth() / lib/fixtures/reports.ts:49-70 と同じ手順★)
  2. ★periodRange()(lib/format/period.ts:157)で [from, to] を求める★
  3. ★clean_timetables × clean_status_histories × robot_error_reports を読み、 その範囲の本清掃行を集計する★(★cleanLogs() の読み方(lib/db/index.ts:190-236)を流用★)
  4. ★lib/judge/kpi.ts の4関数に通す★(★completionRate() は面積ベース★)
  5. ★prev は「1つ前の期間」=確定期間のサマリ行から取る★(★当期の前は先週/先月なので サマリに存在するはず★)
  6. ★確定期間の行と当期の行を1つの配列にして返す★

★★この設計判断(prismaSource で当期集計を行うこと)をコードコメントに残すこと★★ — ★lib/db/index.ts:70-75 の設計方針コメントに例外として1行足す★(★cleanLogs() の 連続日数・judge と同じ扱い。D-090 項14 / 項15 の解決★)。

2-4. ★純粋関数に切り出してテストを書く★

  • ★置き場所★: ★lib/db/report-mapping.ts を新設することを推奨★(★clean-log-mapping.ts と混ざらない★)
  • ★切り出す候補★: ★サマリ行 → ReportPeriodKey(week===0 → monthly)★ / ★期間キー → Map のキー文字列★ / ★サマリ行 + 前期行 → ReportKpi★
  • ★★期待値表を先に作ること★★(CLAUDE.md §9.4)。★境界値: week=0(月次)/ week=1(第1週。 前期は前月の最終週)/ week=6 / month=0 AND week=0(年次。除外される)/ 前期行が無い(prev=null)/ 率が 0.0★。★DB 依存のテストは書かない★(禁止事項13)

この段階の完了条件

  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス(★新規テストを含む★)
  • ★lib/reports/query.ts / API ルート / 画面に差分が無いこと★
  • ★当期集計をどこでやると決めたかがコードコメントに書かれていること★ / ★ここで1コミット★

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

★lib/db/index.ts:494-496 の notImplemented("facilityPerformance") を置き換える★。

3-1. クエリ

  • ★prisma.facilitySummary.findMany({ include: { facility: true } })★ (★FK があるので include が使える。prisma/schema.prisma:726★)
  • ★serial_number は facility_summaries に無い★ → ★robots を別途引いて facility_id で結ぶ★ (★facilities()(lib/db/index.ts:407)と同じ形★)。★P1 は1清掃エリア=1ロボットなので先頭1台★
  • ★★ロボットが0台の施設(fixtures の施設12)をどう扱うか決めて記録すること★★ (★推奨: serial_number: "" ではなくその施設の行を返さない。cleanLogs() / schedules() は 例外を投げているが(lib/db/index.ts:258-262 / :465-469)、★レポートは「一覧の1行が欠ける」で 済むので例外は重い★)

3-2. 各フィールド

フィールド 取得元
facility_id ★Number(row.facility_id)★
facility_name ★row.facility.facility_name★
serial_number ★robots[0].serial_number★
★operation_rate / launch_rate / completion_rate / error_rate★ ★toKpiValue(row.*_ratio, 前期行の *_ratio ?? null)★(★段階2 と同じ Map の作法★)
★status★ ★★judgeFacilityStatus(operation_rate.value)(lib/judge/kpi.ts:75)を呼ぶ★★。★閾値は FACILITY_STATUS_THRESHOLDS(lib/config/index.ts:84-89)から読まれる。★直書きしないこと★★
★consecutive_problem_days★ ★★着手前確認の回答に従う★★(A案なら computeConsecutiveProblemDays()(lib/db/clean-log-mapping.ts:112)を流用 / B案なら 0 / C案なら型変更)

3-3. 当期

  • ★段階2 と同じ★ — ★当期の施設別行はサマリに無いので、詳細テーブルから施設ごとに集計する★
  • ★★段階2 で作った当期集計を「施設ごとに束ねる」形に一般化できるなら、共通化すること★★ (★reportKpis は全社1行、facilityPerformance は施設ごと1行。分母・分子の作り方は同じ★)

この段階の完了条件

  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス
  • ★status が lib/judge/ 経由で決まっていること(good / warning / bad を直書きしないこと)★
  • ★consecutive_problem_days の扱いがコードコメントに書かれていること★ / ★ここで1コミット★

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

★lib/db/index.ts:500-502 の notImplemented("errorBreakdown") を置き換える★。

4-1. クエリと集計

  • ★「方針」章で決めたテーブル(推奨 facility_error_summaries)を findMany()★
  • ★facility 版を選んだ場合、★★同じ期間・同じ error_type の行を全施設ぶん足し上げる★★ (★施設ごとに1行ずつ入っているため。★足し忘れると同じ種別が複数行出る★)
  • ★rate はサーバ側で計算する★(★ErrorBreakdownItem.rate のコメント。lib/types/index.ts:214-219 「サーバが返す…フロントで再計算しない」★)。 ★★分母はその期間の全エラー件数の合計★★(lib/fixtures/reports.ts:478-483 と同じ作法。 ★合計を定数に置かず reduce で出すこと★)
  • ★★rate は丸めない生値★★(CLAUDE.md §4.3)

4-2. ★error_type は生値のまま★

  • ★★type と label の両方に同じ生値を入れる★★(★D-077 / D-088。 lib/types/index.ts:206-213 と lib/fixtures/reports.ts:455-462 のコメント★)
  • ★7分類への変換をしない★ / ★日本語に変換しない★ / ★空文字も未知の値もそのまま通す★
  • ★★帰結: 画面には EmergencyStop のような英語識別子が出る。それが意図した挙動★★

4-3. 当期

  • ★当期のエラー内訳は robot_error_reports を期間で絞って error_type ごとに数える★
  • ★periodRange() で範囲を求め、errored_at ?? created_at で期間判定すること★ (★指示書S で detected_at に採った規則と揃える。★指示書S の結論を実測して合わせること★)

この段階の完了条件

  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス
  • ★type と label に同じ値が入っていること★ / ★rate の合計が 100 になること(テストで固定)★
  • ★facility / customer どちらを読んだかがコードコメントに書かれていること★ / ★ここで1コミット★

段階5 — ★★動作確認(コミットなし)★★

5-1. 起動 / 5-2. ★USE_FIXTURES の true / false で突き合わせる★

  • ★★npm run dev を Bash で起動しないこと★★ → ★Browser pane の preview_start を使う★
  • ★★認証が入っている(D-101)★★ → ★.env.local の ALLOW_DIRECT_ACCESS=true が要る★。 ★★/login でトークンを入れる必要があるかも確認すること★★(★分からなければ止めて聞く★)

★/api/reports/kpi / /api/reports/facility-performance / /api/reports/error-breakdown / /api/reports/continuous-problems の4本を、period=weekly / monthly の両方で叩いて比べる★。

API 見込み
/api/reports/kpi ★値は違う(シードの数値が fixtures と違うため)★。★4指標すべてが null でないこと★・★prev と diff が入っていること★を見る
/api/reports/facility-performance ★件数が施設数と一致すること★(fixtures は12件)。★status が3種類とも出ること★
/api/reports/error-breakdown ★rate の合計が100になること★・★type に英語識別子が出ること★
★/api/reports/continuous-problems★ ★★USE_FIXTURES=false では 500(notImplemented)★★。★これは想定内。第6歩の範囲★

5-3. ★画面での確認★

  • ★/reports で週次・月次の切り替えができること★ / ★前期比の増減(▲▼)が出ること★(★prev が入っていないと出ない★)
  • ★★当期(今週・今月)が正しく出ること★★ — ★★サマリに行が無くても詳細テーブルから 算出されて表示されること★★。★期間ナビで「先週 → 今週」と動かして両方に値が出ることを見る★
  • ★エラー種別の内訳に英語識別子が出ること★(生値方針の帰結)
  • ★★連続問題施設のセクションだけが壊れる(または空になる)こと★★を確認し、記録する

5-4. ★検算(任意だが推奨)/ログの確認/復帰★

  • ★サマリの launch_ratio と launch_count / task_count * 100 を突き合わせる★。 ★★食い違ったら記録するだけ。当方の計算値で上書きしない★★(禁止事項1 / 落とし穴7)
  • ★サーバログにエラーが出ていないこと★
  • ★★確認後 USE_FIXTURES=true に戻すこと★★。★戻したあと全画面が従来どおり動くこと★

この段階の完了条件

  • ★5-2〜5-4 をすべて実施して結果を控えた★ / ★USE_FIXTURES=true に戻した★ / ★コミットしない★

段階6 — 記録と push

docs/decisions.md に追記

★★着手直前に最終番号を実測すること★★ — grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md | tail -3。 ★★^### D-1 も grep すること★★(★^### D-0 だけでは3桁台を拾えない★)。

★記録すること★:

# 内容
★1★ ★★当期集計をどこでやると決めたか★★(prismaSource か lib/reports/query.ts か)。★D-090 項14・項15 の解決として明記すること★
★2★ ★errorBreakdown を facility 版 / customer 版のどちらにしたか★(★D-090 項11 の解決★)と理由
★3★ ★facilityPerformance.consecutive_problem_days をどうしたか★(★着手前確認の回答★)
★4★ ★サマリの値と自前計算を検算したか。したなら結果(一致 / 食い違いの内容)★
★5★ ★ロボット0台の施設をどう扱ったか★ / ★continuousProblems を 500 のまま第6歩に残したこと★
★6★ ★段階5 の実測値(件数・見込みとの差・見込みと違った点)★

★docs/handoff.md にも1追記★(★直前の追記番号を実測して採番★)。

push とこの段階の完了条件

  • ★git add はファイルパスを明示★。★★git add -A は禁止★★(CLAUDE.md §7)
  • ★git commit(メッセージは日本語。既存のログの書式に倣う)★ / ★git push origin develop★
  • ★文書末尾の「実行状態」表と冒頭の表を更新した★

★やらないこと(第6歩以降)★

対象 いつ
★continuousProblems★ ★第6歩★(対応テーブルが無く、summary の生成規則が未定義。D-090 項13)
★点検記録2本(inspections / inspectionDetail)★ ★最後★(D-072 項7。docs/decisions.md:1052)
★dsAlerts の security_off 側★ ★D-091 項3 の内部調整の結果待ち★
★年次(month=0 AND week=0)の表示★ ★P1 では読まない(D-072 項4)★
★サマリ集計バッチそのもの★ ★★TeamWorks の担当。当方は永久に作らない★★(CLAUDE.md:55)

完了条件(全体)

  1. ★★着手前確認1件(consecutive_problem_days)の回答を得てから段階3 に入った★★
  2. ★★D-102(docs/decisions.md:5139)を読み、方針・読むテーブル・当期の扱いを確認した★★
  3. 段階0〜6 をすべて実施した(★途中で止めた場合は、どこで止めたかを実行状態表に書くこと★)
  4. npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス
  5. ★USE_FIXTURES=false で /reports が 200 で表示され、週次・月次とも KPI と前期比(▲▼)が出ること★
  6. ★★当期(今週・今月)が、サマリに行が無い状態で正しく算出されて表示されたこと★★
  7. ★エラー種別の内訳に英語識別子が出ること★(生値方針の帰結)/ ★/api/reports/continuous-problems が 500 のままであること(第6歩に残した)を確認した★
  8. ★USE_FIXTURES=true に戻して全画面が従来どおり動くこと★
  9. ★prisma/schema.prisma / lib/fixtures/ / lib/reports/ / API ルート / 画面 / lib/db/prisma.ts / local-guard.ts / use-fixtures.ts / ds-status.ts / clean-log-mapping.ts / prismaSource の既存6メソッド に差分が無いこと★ (★着手前確認で C案 を選んだ場合を除く★)
  10. docs/decisions.md / docs/handoff.md の更新 / ★origin/develop へ push 済み★ / 文書末尾の「実行状態」表を更新した

実行状態

段階 状態 コミット
★D-102 の確認★ ★実施済み★(docs/decisions.md:5139) —
★着手前の確認★ ★★なし(回答済み。2026-09-10)★★ — ★A案★(facilityPerformance.consecutive_problem_days は computeConsecutiveProblemDays()(lib/db/clean-log-mapping.ts:112)を流用)。★記録: docs/decisions.md の D-103(:5222)★。★★本書に未回答の確認事項は無い(D-104 項5)★★ —
段階0 前提の確認 ★完了★ コミットなし
段階1 サマリ3テーブルのシード ★完了★ 67c2b46
段階2 prismaSource.reportKpis() ★完了★ e826b1f
段階3 prismaSource.facilityPerformance() ★完了★ 614a51f
段階4 prismaSource.errorBreakdown() ★完了★ 47de1d4
段階5 動作確認 ★完了★ コミットなし
段階6 記録と push ★完了★ 本コミット

実行日: 2026-09-11 / 取得した決定番号: D-107(着手時点の最終番号)→ 本作業で D-108 を追加 着手前の実測値: tsc 0 / lint 0 / npm test 44ファイル/862テスト 当期集計の置き場所: ★prismaSource の中(決定。D-108項1)★ / errorBreakdown のテーブル: ★facility_error_summaries(決定。D-108項2)★ / consecutive_problem_days: ★★A案で確定・実装済み★★(computeConsecutiveProblemDays() を流用。D-103) / 検算の結果: ★24行中6行で最大0.5ポイントの食い違い(シードの丸め誤差。D-108項4)★ 詳細: docs/decisions.md の D-108 / docs/handoff.md 追記32 を参照