作成: 2026-09-16 / ブランチ: develop / 前提: 指示書AK 完了(D-138)
D-Watch の実装をお願いします。ブランチは develop です。
■ 最初に読むもの(この順で、全部)
1. CLAUDE.md(全文。特に §4.2 KPI計算式 / §4.7 集計期間 / §7 禁止事項 / §9 実装規約)
2. docs/handoff/指示書AL_当期KPIの絞り込みとレポート画面の粒度.md(全文)
3. docs/decisions.md の D-138(grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md で
位置を出してから読む。★番号順に並んでいません★)
4. lib/db/index.ts の reportKpis()(確定期間と当期の2本立てを、先に読んで理解すること)
■ やること
指示書AL の 段階1 から 段階5 までを、★番号順に★実行してください。
■ 段階ごとに必ずやること
1. npx tsc --noEmit → 0 であること
2. npm run lint → 0 であること
3. npm test → 全パス
4. コミットする(★段階ごとに1コミット★。まとめない)
■ コミットの決まり
・git add -A は使わないでください。必ずファイルパスを明示してください
・コミットメッセージは日本語。「なぜそうしたか」を1〜2行入れてください
・push はしないでください
■ 絶対に触らないファイル
README.md / docs/code-map.md /
docs/handoff/管理画面_設計検討_20260910.md / .claude/worktrees/** /
d-watch-mock.jsx / lib/db/prisma.ts
※ lib/judge/ は ★段階4 でだけ★ 触ります(grade.ts の比較演算子)。
★段階1〜3 では触らないでください★。lib/judge/advice.ts は段階4 でも触りません
■ 絶対にやらないこと
・生SQL / メインDBのスキーマ変更
・★閾値の「数値」を変えること★(段階4 で変えるのは★比較演算子だけ★。90 / 70 / 10 はそのまま)
・閾値のハードコード(lib/config から読むこと)
・新しい CSS に font-family を書くこと
・★確定期間の経路(customer_summaries を読むところ)を変えること★
— 段階6(D-138)で査読を通った実装です。今回は当期だけを直します
・仕様が不明なところを推測で実装すること
■ 動作確認
★ローカルDB(dwatch_local)に 2グループ・3顧客・11施設 が入っています★
(前回の検証で再シード済み。npm run seed:local 相当で作り直せます)。
★テストDB(133.125.238.144)には書き込まないでください★。
期待される結果は指示書AL の §7 に書いてあります。
■ 終わったら
・段階ごとのコミットハッシュ
・tsc / lint / test の結果(テスト本数も)
・★§7 の動作確認を実際に行った結果★
・指示書どおりにできなかったところと、その理由
・判断に迷って自分で決めたところ(あれば全部)
★指示書AK の実装は、査読3本とも合格でした。★ ★落とし穴14個すべて回避、テスト 86ファイル・2,013件 全パス、tsc / lint は 0。★
★★直すのは、私(監督)が指示書AK で漏らした1箇所と、その巻き添えで起きている1件です。★★
| # | 何を漏らしたか |
|---|---|
| ★1★ | ★段階4 で絞り込みを効かせるルートを列挙したとき、/reports/kpi を入れ忘れた★。段階6 で確定期間だけを扱い、★当期を誰にも割り当てなかった★ |
| ★2★ | ★段階6 で「顧客が定まらなければ422」と決めたが、★★画面がその422をどう扱うか★★を指定しなかった★ |
★★実装者に落ち度はありません。★★ ★むしろ①は実装者が自分で見つけて、D-138 §2・lib/types/index.ts・docs/handoff.md の 3箇所に残課題として記録しています。★ ★隠していません。★
★ローカルDB(3顧客)での実測★です。
| 顧客1 | 顧客2 | 顧客3 | ★存在しないID★ | |
|---|---|---|---|---|
| ★確定週の完了率★ | 87.1 | 78.4 | 69.7 | ★空(正しい)★ |
| ★★当期の完了率★★ | ★★55.3★★ | ★★55.3★★ | ★★55.3★★ | ★★55.3(空にならない)★★ |
★どの顧客を選んでも同じ数字★です。★全顧客の合算★になっています。
SQLでの検算:
customer_id=1 → 57.613889 customer_id=2 → 52.407143 customer_id=3 → 46.44 ★全社 → 55.272727★ ← API が返している値
当期 = 全社の数字 ← 絞れていない 前期 = 選んだ1社の数字 ← 絞れている 前期比 = この2つの差分 ← ★意味のない値★
KPIカード 全社の数字 下の施設一覧 選んだ1社だけ ← facility-performance は当期も絞れている
★例外もテスト失敗も出ません。★
USE_FIXTURES=true 当期・customer_id=2 → ★200・空★(正しい) Prisma 経路 当期・customer_id=2 → ★全社の数字★
★CLAUDE.md §9.5 の「切替で挙動が変わらない」が、当期だけ崩れています。★ ★fixtures で開発している限り、この不具合は一生見つかりません。★
| ファイル | 何を |
|---|---|
lib/db/index.ts の reportKpis() 当期ブランチ |
★cleanTimetable.findMany の where に施設の条件を足す★ |
lib/reports/query.ts の toReportKpiDbQuery() |
★facility_ids を null 固定にしない★ |
app/api/reports/kpi/route.ts |
★施設IDを解決して渡す★ |
★★resolveFacilityIdsByMasters() を使うこと★★ — ★他の3本(facility-performance / continuous-problems / error-breakdown)が既に使っている、同じ1本★です。 ★新しい解決関数を作らないこと。★
★段階6(D-138)で査読を通った実装です。★ ★customer_summaries を customer_id で絞る部分は、1行も変えないこと。★
★今回足すのは「当期の詳細集計を施設で絞る」だけ★です。
★これが今回いちばん大事な点です。★
確定期間 → customer_summaries を customer_id で絞る (顧客粒度) 当期 → clean_timetables を facility_id で絞る (施設粒度)
★別の経路ですが、同じ顧客を指しているはずです。★
★★target.customerId から施設IDを解決すること★★ — ★リクエストの customer_id / group_id から別々に解決すると、ずれる余地が残ります。★
具体的には:
1) resolveTargetCustomer() で対象顧客を1社に決める(既存。変えない) 2) ★その顧客ID★ から施設IDを解決する 3) 確定期間はその顧客IDで、当期はその施設IDで絞る
★★group_id から直接施設を引かないこと★★ — ★傘下2社なら既に422で止まっている★ので、 ★段階1 に到達した時点で顧客は1社に定まっています。★
app/api/reports/__tests__/route.test.ts の「#2 当期」は、 ★customer_id を付けずに施設1〜3を合算する前提★で書かれています。
★★いまの誤った挙動をテストが固定しているわけではありません★★ — ★customer_id 未指定で顧客1社なら、全施設が対象になるのが正しい★からです。
★ただし、絞り込みが入ると通らなくなる可能性があります。★ ★落ちたら、期待値を作り直すこと。★ ★テストを消して逃げないこと。★
★ローカルDB(2グループ・3顧客・11施設)を前提★とします。
| # | 入力 | 期待 |
|---|---|---|
| ★1-1★ | 当期・customer_id=1 |
★★顧客1の施設だけの数字★★(★全社の数字にならない★) |
| ★1-2★ | 当期・customer_id=2 |
★1-1 と違う数字★ |
| ★1-3★ | 当期・customer_id=9999 |
★★空(value が null)★★(★★数字が出たら不具合★★) |
| 1-4 | 当期・group_id=2(傘下1社) |
その顧客の数字 |
| 1-5 | 当期・指定なし・顧客1社 | 全施設(=その顧客の全施設) |
| 1-6 | 当期・指定なし・顧客2社以上 | ★422★(段階6 で既に止まる) |
| ★1-7★ | ★確定期間と当期の両方を含む期間★ | ★★両方が同じ顧客の数字であること★★ |
| ★1-8★ | ★前期比★ | ★当期・前期とも同じ顧客の数字から計算されていること★ |
★★1-3 が今回の要です★★ — ★存在しない顧客を指定して数字が出るなら、絞れていません。★
★1-7 と 1-8 は、実データ(ローカルDB)で確かめること★。★テストだけで済ませないこと。★
★fixtures 経路は既に正しく動いています★(customer_id=2 で空)。 ★壊さないこと。★ ★Prisma 側と挙動が揃うことを確かめること。★
components/features/reports/ReportsScreen.tsx は4本を Promise.all で取り、 ★1本でも失敗すると画面全体をエラー表示にします★。
/reports/kpi ← 顧客が定まらないと422 /reports/facility-performance ← 施設単位。合算に問題なし /reports/continuous-problems ← 同上 /reports/error-breakdown ← 同上
★KPIだけが422になっても、残り3本は問題なく出せます。★ ★いまは画面ごと「顧客またはグループを指定してください」の1行になります。★
★fixtures が4顧客になった★ため、★USE_FIXTURES=true だと分類を選ぶまで レポート画面が何も見られません★。
★実運用(顧客1社)では発火しません★が、★開発中の人が最初に踏みます★。
1本ずつ、成功したものは出す。失敗したブロックだけにメッセージを出す。
★Promise.all を Promise.allSettled に変える★のが素直です。
★★ただし、次の2つを守ること★★:
★APIは候補を返しています★(details.customer_ids)。★画面がそれを捨てています。★
KPIのブロックだけに: 「顧客を選んでください」 + ★選択肢が3つあることが分かる形★(件数を出す、屋号セレクトを強調する、など)
★具体的な見せ方は実装者判断★です。★ただし「文言を読ませるだけ」で終わらせないこと。★ ★details.customer_ids を使うこと。★
| # | 状況 | 期待 |
|---|---|---|
| 2-1 | 4本すべて成功 | ★今までどおり4ブロック★ |
| ★2-2★ | ★KPIだけ422★ | ★★残り3ブロックが出る★★+KPIのブロックに誘導 |
| 2-3 | KPIだけ500 | ★残り3ブロックが出る★+KPIのブロックにエラー |
| ★2-4★ | ★4本とも失敗★ | ★★画面全体のエラー(今までどおり)★★ |
| 2-5 | 施設別だけ失敗 | 残り3ブロックが出る |
★★2-2 と 2-4 の両方をテストに入れること。★★
★画面のテストは走りません★(vitest.config.ts の include は lib/** と app/**)。 ★「どのブロックを出すか」を決める部分を lib/ 側の純粋関数に切り出すこと★ — ★指示書AJ で lib/dashboard/rescue-visibility.ts を切り出したのと同じ形★です。
★どれも小さいですが、放っておくと次の人が誤読します。★
d-watch-api.yaml の GroupId の description 末尾:
絞り込みの適用はエンドポイントごとに順次有効化中(指示書AK 段階4)。
★段階4 は完了しました。★ ★この一文は今は正しくありません。★ ★消すか、現状に合わせること。★
docs/handoff.md の欠け指示書AK §12-2 の4項目のうち、★「点検記録の絞り込みは、画面が復活したときに足すこと」★が 残課題リストに入っていません(★本文に「対象外」と書かれているだけ★)。
★残課題として足すこと。★ ★画面が復活したときに思い出せる形にすること。★
app/api/reports/kpi/route.ts は、★customer_id が明示されていても顧客マスタを読んでいます★。
★resolveTargetCustomer() は customerId !== null のとき顧客リストを見ません★ので、 ★この読み込みは不要★です。★毎リクエスト1クエリ増えています。★
★customer_id が明示されているときは読まないようにすること。★
customers() に並び順が無いlib/db/index.ts の customers() に orderBy がありません。 ★422 の details.customer_ids の並びがDB任せ★になっています。
★id の昇順にすること★(/api/customers の並びと揃える)。 ★テストはスタブ順で通ってしまうので、実DBで確かめること。★
◯、×は90%以上のように境はふくめてください
確認したところ:
10%未満を×としてください
★確定:★
◎ 完了率 90%以上 ○ 完了率 70%以上 △ 完了率 10%以上 × 完了率 10%未満、または未実施
★★すべての境界が「上のランクに入る」で揃います。★★
| 完了率 | 今 | ★変更後★ |
|---|---|---|
| 90.1 | ◎ | ◎ |
| ★90.0★ | ★○★ | ★★◎★★ |
| 89.9 | ○ | ○ |
| 70.1 | ○ | ○ |
| ★70.0★ | ★△★ | ★★○★★ |
| 69.9 | △ | △ |
| 10.1 | △ | △ |
| 10.0 | △ | ★△(変わらない)★ |
| 9.9 | × | × |
| null(未実施) | × | × |
★★変わるのは 90.0 と 70.0 の2つだけです。★★
docs/decisions.md の ★D-020b★ が、★逆の規則★を定めています。
- 決定: 完了率 90.0 は ○、70.0 は △、10.0 は △(×ではない)
- 理由: ◎は「90%超」、○は「70%超」と、★境界値そのものは下のランクに落ちる規則で統一されている★
★★D-020b の見出しに [破棄] の印と行き先を書くこと★★ (D-024 / D-067 / D-053 / D-133 / D-135 と同じ作法)。
★★CLAUDE.md §4.1 は「確定仕様」であり、その本文を書き換えます。★★ ★これは軽い変更ではありません。★ ★ユーザーの明示指示(2026-09-16)に基づくことを、 記録に原文つきで残すこと。★
★ここが今回いちばん危ないところです。★
lib/db/clean-log-mapping.ts:282
return completionRate === null || completionRate <= GRADE_THRESHOLDS.good;
★これは「△以下=問題あり」を完了率から直接判定しています。★ ★70.0 が △ だったので <= が正しかった★のですが、 ★★70.0 が ○ になるので < に変えないと、○ の日が「問題あり」のままになります。★★
★この関数が効いている先★:
連続問題日数 → L1 / L2 / L3 → R3 / R4 のアドバイス
→ 要レスキュー店舗の R-2(連続問題レベル3)
→ レポートの「連続問題施設」
★★直し忘れても例外は出ません。★★ ★完了率ちょうど70.0の日だけが、静かに問題日として数えられ続けます。★
★lib/judge/grade.ts の docstring にも、この関係が明記されています★:
あちらは完了率(生値)を入力に取り
rate === null || rate <= GRADE_THRESHOLDS.goodで判定する。 ★判定結果は本関数と常に一致する★(judgeGrade()の「○ = 70%超」より、△× ⟺nullまたは 70 以下)。 ★入力が違うだけの同じ規則★なので、★★片方の閾値だけを動かさないこと★★。
★★この注記を書き換えること。★★ ★「70 以下」→「70 未満」、「○ = 70%超」→「○ = 70%以上」。★
| ファイル | 何を |
|---|---|
★lib/judge/grade.ts★ |
rate > excellent → ★>=★ / rate > good → ★>=★ / ★rate < poor はそのまま★ |
| 同上 | ★docstring の表と、isProblemGrade() の長い注記★ |
★lib/db/clean-log-mapping.ts★ |
★<= GRADE_THRESHOLDS.good → <★(§5-4) |
lib/config/index.ts |
★GRADE_THRESHOLDS のコメント★(「この値を超えると ◎」→「以上で ◎」)。★数値は変えない★ |
★CLAUDE.md §4.1★ |
判定基準の表 |
★CLAUDE.md §9.4★ |
★境界値のリスト★(「90.4 → 表示90だが判定は◎、89.5 → 表示90だが判定は○」はそのまま正しい) |
docs/fixtures.md |
★境界値表★ |
docs/decisions.md |
★D-020b に [破棄] の印★ |
d-watch-api.yaml |
★grade の説明に「超」「以下」が書かれていないか確かめること★ |
★★GRADE_THRESHOLDS の数値(90 / 70 / 10)は変えないこと。★★ ★比較演算子だけです。★
lib/fixtures/clean-logs.ts:226
rate === null || rate < GRADE_THRESHOLDS.good ? 1 : 0,
★これは fixtures のデータ生成で、既に < です。★ ★「問題あり」と同じ概念なのか、別の概念なのかを確かめてください。★
< が正しいので、★そのままでよい★★どちらか判断できなければ、手を止めて報告すること。★ ★推測で変えないこと。★
lib/judge/advice.ts:66
completionRate < GRADE_THRESHOLDS.poor
★R2「完了率×(10%未満)」の判定です。★ ★× の境界は変わらないので、< のままで正しい★です。 ★触らないこと。★
★CLAUDE.md §9.4 が「境界値を必ず含める」と定めています。★ ★10個すべてテストに入れること。★
| # | 入力 | 期待 |
|---|---|---|
| 4-1 | 90.1 | ◎ |
| ★4-2★ | ★90.0★ | ★★◎★★(★今は○。ここが変わる★) |
| 4-3 | 89.9 | ○ |
| 4-4 | 70.1 | ○ |
| ★4-5★ | ★70.0★ | ★★○★★(★今は△。ここが変わる★) |
| 4-6 | 69.9 | △ |
| 4-7 | 10.1 | △ |
| 4-8 | ★10.0★ | ★△(変わらない)★ |
| 4-9 | 9.9 | × |
| 4-10 | null | × |
★「問題あり」の期待値表★(§5-4):
| # | 完了率 | 期待 |
|---|---|---|
| ★4-11★ | ★70.0★ | ★★問題なし★★(★今は問題あり★) |
| 4-12 | 69.9 | 問題あり |
| 4-13 | 70.1 | 問題なし |
| 4-14 | null | 問題あり |
★★4-11 を必ず入れること。★★ ★ここが漏れると、連続日数が静かに1日多く数えられます。★
★境界値を固定しているテストが複数あります。★ ★期待値を新しい規則に直すこと。★ ★★テストを消して逃げないこと。★★
★落ちたテストの一覧を報告に含めること★ — ★想定外の場所が落ちたら、それは連鎖の見落としです。★
docs/decisions.md★★採番の前に必ず実測すること★★:
grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md | tail -3
★番号順に並んでいません。★
書くこと:
target.customerId から解決した理由★★docs/handoff.md に追記USE_FIXTURES の true / false で、レポート画面の見え方が揃ったこと★group_id = 0 の顧客が、ローカルDBに投入できないままであること★(第9回 Q7-4 待ち)★ローカルDB(dwatch_local)に 2グループ・3顧客・11施設 が入っています。★ ★作り直すなら scripts/seed-local-db.ts。★ ★★テストDB(133.125.238.144)には書き込まないこと。★★
★.env.local はテストDBを向いています。★ ★書き換えないこと★ — ★環境変数を上書きして別に起動するか、ルートハンドラを直接呼ぶこと。★
/api/reports/kpi?period=monthly&year=2026&month=9&customer_id=1 /api/reports/kpi?period=monthly&year=2026&month=9&customer_id=2 /api/reports/kpi?period=monthly&year=2026&month=9&customer_id=9999
| # | 期待 |
|---|---|
| ★6-1★ | ★★当期の数字が、顧客1 と 顧客2 で違うこと★★ |
| ★6-2★ | ★★customer_id=9999 で当期が空になること★★(★数字が出たら直っていない★) |
| 6-3 | 確定期間の数字が、指示書AK のときと変わっていないこと |
| ★6-4★ | ★前期比が、当期・前期とも同じ顧客から計算されていること★ |
| 6-5 | 指定なし・3顧客 → ★422(今までどおり)★ |
★★自分でSQLを書いて検算すること。★★ ★APIの出力をそのまま信じないこと。★
★ログインが要るので、画面の目視は難しいかもしれません。★ ★その場合は、切り出した純粋関数のテストと、API の応答で確かめること。★ ★★「目視できなかった」と正直に報告してよい。★★ ★推測で「動いているはず」と書かないこと。★
| # | 落とし穴 | どうなるか |
|---|---|---|
| ★1★ | ★当期を group_id から直接絞る★ |
★確定期間(顧客粒度)と母集団がずれる★。★段階1 に来た時点で顧客は1社に定まっている★ |
| ★2★ | ★確定期間の経路も一緒に直す★ | ★査読を通った実装を壊す。今回は当期だけ★ |
| ★3★ | ★facility_ids を ?.length で分岐する★ |
★「0件ヒット」が「全件」に化ける。例外もテスト失敗も出ない★ |
| ★4★ | ★落ちたテストを消して逃げる★ | ★期待値を作り直すこと★ |
| ★5★ | ★Promise.allSettled にしたが、どれが失敗したか分からない★ |
欠けているブロックに気づけない |
| ★6★ | ★全部失敗しても空の枠を4つ並べる★ | 1行で「読み込めませんでした」のほうが分かる |
| ★7★ | ★details.customer_ids を捨てる★ |
★APIが用意した候補が活きない★ |
| ★8★ | ★D-138 の残課題に印を付けない★ | ★次の人が「まだ直っていない」と読む★ |
| ★9★ | ★画面に判定を書く★ | ★画面のテストは走らない★。lib/ に切り出すこと |
| ★10★ | ★テストDBに書き込む★ | ★★読み取り専用★★ |
| ★★11★★ | ★★judgeGrade() だけ直して isProblemCompletion() を直し忘れる★★ |
★★完了率ちょうど70.0の日が「問題あり」のまま数えられ続ける★★。★連続日数 → L1〜L3 → R3/R4 → 要レスキューまで波及する★。★例外もテスト失敗も出ない★ |
| ★12★ | ★rate < GRADE_THRESHOLDS.poor も <= にする★ |
★★× の境界は変えません★★(「10%未満が×」でユーザー確定)。★10.0 が × に落ちる★ |
| ★13★ | ★GRADE_THRESHOLDS の数値を変える★ |
★変えるのは比較演算子だけ★。90 / 70 / 10 はそのまま |
| ★14★ | ★lib/judge/advice.ts を直す★ |
★R2 は「×(10%未満)」で、× の境界は変わらない★。★触らないこと★ |
| ★15★ | ★D-020b と CLAUDE.md §4.1 を直さない★ |
★★コードと確定仕様が食い違う★★。次の人がどちらを信じるか分からなくなる |
| ★16★ | ★lib/fixtures/clean-logs.ts を確かめずに変える★ |
★同じ概念か別概念かを先に判断すること★。★判断できなければ手を止めて報告★ |
| 項目 | なぜ |
|---|---|
| ★4画面の draft / 確定の分離★ | ★モックに検索ボタンが無いための判断で、記録済み★。★変えるなら別の判断★ |
| ★総清掃面積の新画面★ | ★次の指示書★ |
| ★複数顧客の合算★ | ★4指標のうち2つを合算する材料がDBに無い★(第9回 6章で照会中) |
★group_id = 0 の投入★ |
★DDLの話。DFAは直せない★(第9回 Q7-4) |
| ★200件超の実行計画対策★ | 測れる見積もりが無い |
| ★点検記録の絞り込み★ | 画面自体が未実装 |