作成: 2026-09-15 / ブランチ: develop / 前提: D-134(docs/decisions.md の最終決定番号。★着手時に grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md | tail -3 で実測し直すこと★)
D-Watch の実装をお願いします。ブランチは develop です。
■ 最初に読むもの(この順で、全部)
1. CLAUDE.md(全文。特に §4.1 判定基準 / §4.3 端数処理 / §4.5 エスカレーション /
§7 禁止事項 / §9 実装規約)
2. docs/handoff/指示書AI_要レスキュー店舗と詳細ログの期間上限.md(全文)
3. 指示書AI が名前を挙げている既存ファイル(lib/db/clean-log-mapping.ts の
ConsecutiveInput 周辺、lib/db/consecutive-source.ts、lib/judge/escalation.ts、
lib/judge/kpi.ts、lib/config/index.ts、components/features/dashboard/DashboardScreen.tsx)
■ やること
指示書AI の 段階1 から 段階9 までを、番号順に実行してください。
段階を飛ばさないでください。特に 段階3(判定の純粋関数)は
「期待値表をテストに落としてから実装する」順序を崩さないでください。
■ 段階ごとに必ずやること
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/dashboard/aggregate.ts / lib/judge/advice.ts / lib/db/prisma.ts
■ 絶対にやらないこと
・生SQL($queryRaw / $executeRaw / *Unsafe / Prisma.raw)— ESLint が落とします
・閾値のハードコード — 必ず lib/config/index.ts に置いてください
・メインDBのスキーマ変更・マイグレーション作成
・表示用に丸めた値で判定すること
・新しい .module.css に font-family を書くこと(日本語フォントが落ちて漢字が中華字形になります)
・仕様が不明なところを推測で実装すること
■ 迷ったら
推測で埋めず、手を止めて「ここが決まっていません」と報告してください。
指示書AI の §13(実装者へ — 特に踏みやすい落とし穴)に、
例外もテスト失敗も出ないまま壊れる箇所を10個挙げてあります。着手前に読んでください。
■ 動作確認
dev server は npm run dev で立ててください(.env.local は既にテストDBを向いています)。
★テストDBのデータは 2026-09-13 で終わっています★。今日の日付で開くと全部0件になるので、
/api/dashboard?date=2026-09-13 と ?date=2026-09-12 で確かめてください。
期待される結果は指示書AI の §12 に書いてあります。
■ 終わったら
次を報告してください。
・段階ごとのコミットハッシュ
・tsc / lint / test の結果(テスト本数も)
・指示書どおりにできなかったところと、その理由
・判断に迷って自分で決めたところ(あれば全部)
ユーザーから新要件が4件来ました(2026-09-15)。★そのうち、いま着手できる2件だけ★を扱います。
| 要件 | この指示書で | 理由 |
|---|---|---|
| ★要レスキュー店舗★ | ★やる(3条件)★ | 独立している |
| ★詳細ログの期間上限を2年に★ | ★やる★ | 設定値1本と検証の変更で済む |
| ★CSVの上限と、出せないことの表示★ | ★やる★ | ★上を広げた結果として要る(ユーザー指示 2026-09-15)★ |
| 分類(ホールディングス/屋号/店舗)の復活 | ★やらない★ | ★TeamWorks への確認待ち★(TeamWorks確認事項_第9回_20260915.md の 6章・7章) |
| 総清掃面積の新画面 | ★やらない★ | ★分類の絞り込みに乗るため、上が先★ |
★★着手前に必ず CLAUDE.md を全文読むこと。★★ 特に §4.1(判定基準)/ §4.3(端数処理)/ §4.5(エスカレーション)/ §7(禁止事項)/ §9(実装規約)。
ユーザー原文(2026-09-15):
・ダッシュボードの変更 要レスキュー店舗をダッシュボード上の詳細ログの上に表示 定義は出発失敗3日異常、連続問題レベル3、完了率急落前日比OO%、未実施3日連続 当該日の清掃詳細ログの情報を元に表示
★「出発失敗」と「未実施」は、現在のコードでは同じ判定です★ (notExecutedFromHistory()、lib/db/clean-log-mapping.ts:152-157)。
ユーザーに確認したところ「別物」との回答でしたが、★実データを調べた結果、別物にできませんでした★。
clean_status_histories.started_at は、テーブル定義書に ★「清掃スケジュールの清掃開始日時を格納する」★ と書かれている(= clean_timetables.start_at の写し)TIMESTAMPDIFF(MINUTE, t.start_at, h.started_at) = 0。 ★30分以上の遅延は0件★clean_task_reports は ★0行★robot_instruct_histories / section_robot_statuses も ★0行★clean_statuses.clean_status_trans(1=清掃指示 で止まる=出発失敗を表せる唯一の列)は ★履歴テーブルに存在しない★ため、過去を遡れない★したがって、猶予30分の計算をしようとしても材料がありません。★ ★D-091(「予定+猶予30分を当方で計算しない」)を覆すかどうか以前の問題です。★
★ユーザー決定(2026-09-15): 3条件で先に実装する。★ TeamWorks から実開始時刻の所在について回答が来たら、4つ目の条件として足す (照会済み: TeamWorks確認事項_第9回_20260915.md 11章)。
確定した3条件:
| # | 条件 | 判定 |
|---|---|---|
| ★R-1★ | ★未実施が3日以上連続★(=出発失敗と同義) | completion_rate === null が3スケジュール日以上連続 |
| ★R-2★ | ★連続問題レベル3★ | escalation_level === "L3" |
| ★R-3★ | ★完了率が前日比20%以上の急落★ | 後述 |
ユーザー原文:
数値は一旦20%を閾値にしてください、ただし変更は可能にしてください
差(ポイント)か比(%)かを確認したところ:
比で
★確定:★
(前回の完了率 − 当日の完了率) ÷ 前回の完了率 ≧ 0.20
★= 完了率が前回の8割を切ったら「急落」。★
★20 は設定値。ハードコード禁止(CLAUDE.md §7)。★
ユーザーに個別の確認を取らず、既存の規則との整合から監督が決めた事項です。
| # | 決定 | 根拠 |
|---|---|---|
| ★A★ | ★「前日」はカレンダー前日ではなく、前回のスケジュール存在日★ | CLAUDE.md §4.5 / D-026 が「連続はスケジュール存在日の連続」と定めている。急落だけカレンダー日にすると規則が二本立てになる。★実データに1件だけ差の出る例がある — 施設7が 09-07 → 09-09(09-08 は予定なし)。カレンダー前日だと算出不可、前回スケジュール日だと 94.7% → 72.5% で急落として拾う★ |
| ★B★ | ★前回の完了率が 0% のときは急落としない★ | 0 で割れない。下がりようもない |
| ★C★ | ★前回が未実施(null)のときは急落判定から外す★ | 比べる相手が無い。未実施は R-1 で拾う |
| ★D★ | ★当日が未実施(null)のときも急落判定から外す★ | 同上。★「未実施を 0% とみなして急落にする」ことはしない★ — completionRateFromHistory() が未実施を 0 ではなく null にしているのは意図的(lib/db/clean-log-mapping.ts:168-173) |
| ★E★ | ★完了率が上がったときは拾わない★ | 「急落」なので低下方向のみ。絶対値で見ない |
ユーザー確認済み(「それでOK」):
★面積で加重する。★
施設の1日の完了率 = Σclean_area ÷ Σtask_area × 100
★CLAUDE.md §4.2 が「清掃完了率 = Σ清掃面積 ÷ Σ清掃対象面積」と面積ベースを定めているため。★ ★★単純平均にしないこと★★ — 1000㎡の区画と100㎡の区画を同じ重みで扱うことになる。
★★注意: これは既存の completion_rate(1行=1清掃エリアごとの率)とは別の数です。★★ ★混同を防ぐため、名前を変えること★(後述 §5 の facilityDayRate)。
ユーザー原文:
出す、がダッシュボードの割合計算には含めない
★確定:★
lib/dashboard/aggregate.ts と app/api/dashboard/route.ts の summary に手を入れない★この指定は重要です。★ 実データに例があります:
★施設7「メガバリューマート緑野店」は 2026-09-12 時点で問題4日連続(L3)だが、2026-09-13 には予定が無い★
★「当該日のログだけ」を母集団にすると、一番出すべき施設が消えます。★
ユーザー原文:
・詳細ログは2年分見れるように 31日なのを2年をデフォに、変更可能はそのまま
確認したところ:
上限だけ、未指定で開いた場合や、店舗指定のみの場合は上限無し、絞り込むときの上限を2年
★確定:★
| 状況 | 上限 |
|---|---|
date_from と date_to の両方が指定されている |
★2年★(今の31日を置き換え) |
| 日付が指定されていない | ★上限なし★ |
| 店舗(施設名)だけ指定されている | ★上限なし★(=日付が無いので上記と同じ) |
| 画面を開いたときの既定 | ★今日1日のまま★(変更しない) |
★★「デフォ」を画面の初期表示のことだと解釈しないこと★★ — 現在の画面の既定は DetailLogScreen.tsx:43-44 の useState(todayJst()) で ★今日1日★ です。31日は既定ではなく上限です。
| 対象 | 理由 |
|---|---|
README.md |
★別作業が進行中★ |
docs/code-map.md |
★同上★ |
docs/handoff/管理画面_設計検討_20260910.md |
★同上★ |
.claude/worktrees/** |
★別ブランチの作業コピー★ |
d-watch-mock.jsx |
★モックは原型として保存する★(D-120 / D-121 の前例。§8 参照) |
lib/dashboard/aggregate.ts |
★§1-5。数値カードを変えない★ |
lib/judge/advice.ts |
★R1〜R4 のアドバイス生成。要レスキューとは別物★ |
lib/db/prisma.ts |
★着手時点で未コミットの変更が乗っている★。触らない |
★★禁止★★(CLAUDE.md §7 と、これまでの決定):
git add -A★ — 必ずファイルパスを明示する$queryRaw / $executeRaw / *Unsafe / Prisma.raw)— ESLint が error で落とす(D-122)lib/config/index.ts に置くlib/config/index.tsESCALATION_THRESHOLDS(:20-25)の直後に足す。
/**
* 完了率の「急落」判定(ユーザー指示 2026-09-15 / 指示書AI)
*
* ★比(%)で見る。差(ポイント)ではない★ — ユーザー確定(2026-09-15「比で」)。
* (前回の完了率 − 当日の完了率) ÷ 前回の完了率 ≧ dropRatio
* = 完了率が前回の (1 - dropRatio) 倍を下回ったら急落。
*
* ★差で見ると、元から完了率が低い施設を拾えない★ —
* 前日30% → 当日18% は「12ポイント低下」だが「40%低下」。
* ユーザーは後者を拾う側を選んだ。
*
* ★「前回」はカレンダー前日ではなく、前回のスケジュール存在日★
* (D-026 / `CLAUDE.md` §4.5 の「連続」の数え方に揃えた。指示書AI 決定A)。
*/
export const COMPLETION_DROP = {
/** 前回比でこの割合以上下がったら「急落」(0.2 = 20%) */
dropRatio: 0.2,
} as const;
★★0.2 と書くこと。20 と書いて後で100で割らないこと★★ — 単位が二重になると、次に値を変える人が どちらの単位か分からなくなる。
npx tsc --noEmit
急落判定(R-3)は ★施設×日の面積加重完了率★ を必要とします(§1-4)。 現在の ConsecutiveInput(lib/db/clean-log-mapping.ts:286-301)は completion_rate しか持たず、 ★面積を持っていません★。
★★R-1(未実施の連続)と R-2(L3)は、既存の completion_rate だけで判定できます★★ — completion_rate === null が「未実施=出発失敗」と同値であることは、 lib/db/clean-log-mapping.ts:176-180 の不変条件として既に書かれています。★新しい列は要りません。★
面積が要るのは R-3 だけです。
loadConsecutiveInputs() の docstring(lib/db/consecutive-source.ts の select の直上)には こう書かれています。
★★
selectの列を増やさないこと★★ — ここに列を足すとそのまま本番 (保持2年・約26万行)の転送量になります。
★この指示書は、その禁を意図的に破ります。★ 根拠:
clean_area / task_area の2列(いずれも double、8バイト)★★docstring を書き換えること★★ — 「増やすな」とだけ書いてあると、次の人が 「なぜ2列だけ増えているのか」を判断できません。★増やしてよい条件と、増やした理由を書く★。
(1) lib/db/consecutive-source.ts
ConsecutiveHistoryRow に clean_area: number / task_area: number を足すselect に clean_area: true, task_area: true を足す (★clean_timetables 側の select は増やさない★)consecutiveInputsFromRows() の .map() で ConsecutiveInput に詰める。 ★履歴が無い行は clean_area: 0 / task_area: 0★(?? 0)(2) lib/db/clean-log-mapping.ts
ConsecutiveInput に足す:
/** * 面積の生値(急落判定の面積加重に使う。指示書AI 段階2)。 * * ★履歴が無い行・未実施の行は 0★(実データでは `clean_error = 1` の行が * `task_area = 0` / `clean_area = 0` になっている)。 * ★★`completion_rate` を面積から再計算しないこと★★ — `completion_rate` は * 1行(=1清掃エリア)の率で、未実施は `null`。この2つは別の数です。 */ clean_area: number; task_area: number;
(3) fixtures 側
lib/fixtures/ から ConsecutiveInput を組み立てている箇所があれば同じ2列を足す。 ★無ければ何もしない(探すこと)★。
ConsecutiveInput を組み立てているテストが落ちます。★必須の2列なので全部直す必要があります★。
★★clean_area / task_area を optional(?)にして逃げないこと★★ — optional にすると、詰め忘れた呼び出し側が undefined のまま通り、 ★面積加重が静かに 0/0 になって率が null に化けます(例外もテスト失敗も出ません)★。
npx tsc --noEmit npm run lint npm test
★この段階ではテストが落ちたままでも構いません★(次の段階で期待値を足すため)。 ★ただし「どのテストがなぜ落ちているか」を把握してから次に進むこと。★
★新規ファイル lib/judge/rescue.ts★ と lib/judge/__tests__/rescue.test.ts。
★lib/judge/ に置く理由★: CLAUDE.md §9.1 が「判定ロジック(◎○△×、L1〜L3、KPI計算、丸め)」を lib/judge/ に置くと定めており、要レスキューはその仲間です。 ★DBアクセスを混ぜないこと★(§9.4「判定関数は純粋関数にする」)。
★vitest.config.ts の include は lib/** と app/** だけで、components/** のテストは走りません★ (D-120 項2)。★判定を画面側に書くと、テストが1本も走りません。★
CLAUDE.md §6-2 / §9.4:
判定ロジックには必ずテストを書く。★期待値表を先に作り、それをテストで固定する★
★★実装より先にこの表をテストに落とすこと。★★
「スケジュール存在日」を古い順に並べたときの、当該日時点の連続数。
| # | 入力(★は未実施) | 期待 |
|---|---|---|
| 1-1 | [○] |
★false★(1日) |
| 1-2 | [★] |
★false★(1日) |
| 1-3 | [★, ★] |
★false★(2日) |
| 1-4 | ★[★, ★, ★]★ |
★★true★★(3日) |
| 1-5 | [★, ★, ★, ★] |
★true★(4日以上も拾う。「3日以上」) |
| 1-6 | [★, ○, ★, ★] |
★false★(途中で切れて2日) |
| 1-7 | [★, ★, ★, ○] |
★false★(★当該日が正常なら、過去に3日連続があっても拾わない★) |
| 1-8 | [](行なし) |
★false★ |
| 1-9 | ★予定のない日を挟む★ [★(9/7), ★(9/9), ★(9/10)](9/8 は予定なし) |
★★true★★(★カレンダー日ではなくスケジュール存在日で数える。D-026★) |
| 1-10 | 同一日に2台、片方だけ未実施 | ★その日は「未実施の日」とする(OR)★。D-109 の A案に揃える |
| 1-11 | 同一日に同一ロボットが2回清掃、片方だけ未実施 | ★同上(1日に畳む)★ |
★既存の judgeEscalationLevel()(lib/judge/escalation.ts:52-59)をそのまま使う。★ ★★新しく数え直さないこと★★(D-109 の再発防止「連続日数の算出は1本に集約する」)。
| # | 連続問題日数 | 期待 |
|---|---|---|
| 2-1 | 2 | ★false★(L2) |
| 2-2 | 3 | ★★true★★(L3) |
| 2-3 | 10 | ★true★ |
| 2-4 | 0 | ★false★ |
facilityDayRate)| # | 入力(clean_area / task_area の組) | 期待 |
|---|---|---|
| 3-1 | [(900, 1000)] |
★90★ |
| 3-2 | [(900, 1000), (100, 1000)] |
★50★(= 1000/2000)★単純平均も50なので、これだけでは加重を確かめられない★ |
| 3-3 | ★[(900, 1000), (10, 100)]★ |
★★82.7…★★(= 910/1100)。★単純平均なら 50 になる★ — ★この1本で加重を固定する★ |
| 3-4 | [(0, 1000)] |
★0★(0%で実施した。★null ではない★) |
| 3-5 | [(0, 0)](未実施のみ) |
★★null★★(0除算を捏造しない) |
| 3-6 | [] |
★null★ |
| 3-7 | [(0, 0), (900, 1000)](未実施1台+正常1台) |
★90★(= 900/1000。★未実施行は分母に 0 を足すだけ★) |
| 3-8 | [(500, 0)](分母0だが分子あり) |
★★null★★(★∞ や NaN を返さない★) |
★lib/judge/kpi.ts:36 の completionRate() をそのまま使えば 3-5 / 3-6 / 3-8 は自動的に満たされます★ (内部の rate() が分母0で null を返す)。★自前で割り算を書かないこと。★
dropRatio = 0.2 のとき★| # | 前回 → 当日 | 低下 | 期待 | 根拠 |
|---|---|---|---|---|
| 4-1 | 100 → 80 | ちょうど20% | ★★true★★ | ★境界は「以上」★ |
| 4-2 | 100 → 80.1 | 19.9% | ★false★ | |
| 4-3 | 100 → 79.9 | 20.1% | ★true★ | |
| 4-4 | 30 → 18 | 40% | ★true★ | ★差なら12ポイントで拾えない。比を選んだ意味がここに出る★ |
| 4-5 | 80 → 60 | 25% | ★true★ | |
| 4-6 | 60 → 80 | ★上昇★ | ★★false★★ | ★決定E★ |
| 4-7 | ★0 → 0★ | — | ★★false★★ | ★決定B。0で割らない★ |
| 4-8 | ★0 → 50★ | — | ★false★ | ★決定B★ |
| 4-9 | ★null → 50★(前回が未実施) | — | ★★false★★ | ★決定C★ |
| 4-10 | ★50 → null★(当日が未実施) | — | ★★false★★ | ★決定D。R-1 で拾う★ |
| 4-11 | ★null → null★ | — | ★false★ | |
| 4-12 | 前回が存在しない(初日) | — | ★★false★★ | 比べる相手が無い |
| 4-13 | ★予定のない日を挟む★ 94.7(9/7) → 72.5(9/9) | 23.4% | ★★true★★ | ★決定A。9/8 に予定が無くても 9/7 と比べる★ |
★★4-1 の境界を必ずテストに入れること★★ — CLAUDE.md §9.4 は 「90.0 / 90.1 / 70.0 / 70.1」のように★境界の両側を必ず含める★と定めている。
| # | R-1 | R-2 | R-3 | 期待 |
|---|---|---|---|---|
| 5-1 | false | false | false | ★要レスキューでない★ |
| 5-2 | true | false | false | ★要レスキュー★(理由: 未実施3日以上) |
| 5-3 | false | true | false | ★要レスキュー★(理由: L3) |
| 5-4 | false | false | true | ★要レスキュー★(理由: 急落) |
| 5-5 | ★true★ | ★true★ | ★true★ | ★要レスキュー。★★理由は3つとも返す★★(1つに絞らない)★ |
★★理由を配列で返すこと★★ — 「なぜレスキューが要るのか」が運用担当の判断材料そのものです。 ★1つに丸めると、現場に何を伝えるかが決まりません。★
/** 要レスキューの理由(複数該当したら全部入る) */
export type RescueReason = "not_executed_streak" | "escalation_l3" | "completion_drop";
export interface RescueJudgement {
/** 3条件のいずれかに当たったか */
required: boolean;
/** 当たった理由(★空配列なら required は false★) */
reasons: RescueReason[];
/** 表示用の付帯情報。★判定には使わない★ */
not_executed_days: number;
consecutive_problem_days: number;
/** 面積加重。未実施・データなしは null */
completion_rate: number | null;
previous_completion_rate: number | null;
/** 低下の比(0.25 = 25%低下)。算出できないときは null */
drop_ratio: number | null;
}
★★文言(日本語)をこの層で作らないこと★★ — D-111 の決定です。
連続問題施設の summary は★事実だけ★を組み立てる(★原因も助言も書かない★)。 テンプレート文字列のまま返し、数値はフロントが差し込む(EN切替で和文が残るのを避けるため)
★RescueReason は識別子(enum)で返し、日本語は画面側の t() で作ること。★
lib/db/index.tsloadConsecutiveInputs() から取ること★★★新しい全期間クエリを書かないこと。★★
lib/db/clean-log-mapping.ts の computeConsecutiveProblemDays() の docstring に明記されています:
★入力を作る責務は
loadConsecutiveInputs()(lib/db/consecutive-source.ts)の1本に切り出してある★ — ★★cleanLogs()/facilityPerformance()/continuousProblems()はその1本を呼ぶこと★★ (D-109 / D-103 の再発防止。連続日数を2箇所で別々に計算しない)
★要レスキューも、この1本を通します。★ ★4本目の呼び出し元になります。★
loadConsecutiveInputs() は「表示期間 + 遡りぶん」を読みます。要レスキューは:
★既存の遡り幅(表示上限+1 ×7 カレンダー日、既定なら217日)で足ります。★ ★★遡り幅を要レスキュー用に別途縮めないこと★★ — R-2 が L3 を取りこぼします。
各施設について、「当該日以前で最も新しいスケジュール存在日」を求め、 その日の状態で3条件を判定する。
★★「当該日のスケジュール存在日だけ」で絞らないこと★★ — 施設7の例(09-12 に L3、09-13 は予定なし)が 消えます。
★実装の目安★: loadConsecutiveInputs() の返す ConsecutiveInput[] を施設ごとに分け、 scheduled_at の日付が当該日以下のものだけを残し、その中の最終日を「評価日」とする。
DataSource への追加interface DataSource(lib/db/index.ts:151-262)に足し、 ★fixtureSource と prismaSource の両方に実装すること★。
★★fixtures 側を notImplemented にしないこと★★ — ダッシュボードは USE_FIXTURES=true でも開きます。 ★fixtures で画面が落ちると、実装した本人が最初に踏みます。★
★fixtures 側は lib/fixtures/ の既存データから同じ判定を通すこと★(CLAUDE.md §9.5: 「判定ロジックは切替の外側に置く」)。★fixtures 用に別の判定を書かない。★
★契約に明記すること★(D-052 / D-055 の前例: ソート順は契約に書く)。
lib/config/index.ts に置くこと★★(ハードコード禁止)★★訂正(2026-09-15。指示書AJ 段階1 で撤回)★★
★上の「並び=連続問題日数の降順」「件数=上限5件」の2つは、指示書AJ で撤回しました。★ この組み合わせで ★急落だけで該当する施設が構造的に一覧から落ちていました★ (急落は「昨日まで正常 → 今日落ちた」なので連続日数が1〜2にしかならず、 連続日数の降順で並べると必ず L3 の後ろに来る。実データで7件中2件が黙って消えていた)。 ★これは監督(指示書AI の作成者)の指定の誤りであり、実装者に落ち度はありません。★
現在の正:
- 並び = ★
reasonsの件数の降順 →consecutive_problem_daysの降順 →facility_idの昇順★- 件数 = ★API に上限を設けない★。初期表示5件+「もっと表示」は★画面側の責務★ (
RESCUE_INITIAL_VISIBLE。旧RESCUE_LIST_LIMITの改名)詳細は
docs/handoff/指示書AJ_要レスキューの見せ方とサイドバーのスクロール.md§2 / §4。
CLAUDE.md §6-1:
★API契約を変えたくなったら、先に
d-watch-api.yamlを直す。★ 実装だけ変えてYAMLと乖離させない
★GET /dashboard のレスポンスに rescue_required を足す。★ 新規エンドポイントは作らない。
★理由★: security_banner と同じ「ダッシュボード専用の配列」で、形が揃う。 画面の往復も増えない。
★★注意★★: d-watch-api.yaml:170-172 に
コンパクト版清掃詳細ログは本APIには含めない。
GET /clean-logs?result=problem&date_from=当日を再利用すること
とあります。★これは「既存の一覧APIで足りるものを二重に作るな」という趣旨★であり、 要レスキューは CleanLogItem だけでは組み立てられない(前日比も未実施連続日数も入っていない)ので 抵触しません。★ただし、その旨を description に書き添えること★ — 次に読む人が同じ疑問を持ちます。
rescue_required:
type: array
description: |
要レスキュー店舗。指定日時点で3条件のいずれかに当たる施設。
★指定日に清掃予定が無い施設も含む★ — 各施設の「指定日以前で最も新しい
スケジュール存在日」の状態で判定する(指示書AI)。
★summary の数値カード(success/failed/not_executed/success_rate)には
影響しない★ — 予定の無い施設を分母に入れないため(ユーザー指示 2026-09-15)。
並びは consecutive_problem_days の降順、同値なら facility_id の昇順。
件数は RESCUE_LIST_LIMIT 件まで。
items:
type: object
properties:
facility_id: { type: integer, example: 7 }
facility_name: { type: string, example: "メガバリューマート緑野店" }
evaluated_on: { type: string, format: date, example: "2026-09-12" }
reasons:
type: array
description: |
当たった条件。★複数該当したら全部入る★。
文言はクライアント側で作る(D-111。EN切替で和文が残るのを避けるため)。
items:
type: string
enum: [not_executed_streak, escalation_l3, completion_drop]
not_executed_days: { type: integer, example: 3 }
consecutive_problem_days: { type: integer, example: 4 }
completion_rate: { type: number, nullable: true, example: 8.5 }
previous_completion_rate: { type: number, nullable: true, example: 73.4 }
drop_ratio: { type: number, nullable: true, example: 0.884 }
required: [facility_id, facility_name, evaluated_on, reasons]
★completion_rate は丸めない生値を返す★(CLAUDE.md §9.3「数値の丸めは表示層でのみ行う」)。
app/api/dashboard/route.ts の body(:109-113)に1キー足す。
★★summary / schedule_progress / security_banner の中身を変えないこと。★★
★型★: lib/types/index.ts の DashboardResponse に足す。★YAML のスキーマ名と揃える★(§9.2)。
components/features/dashboard/DashboardScreen.tsx:
:97-125 数値カード4枚のグリッド(</div> が :125)
★★ここに入れる★★
:127 <div className={styles.card}> ← 清掃詳細ログ(コンパクト版)
★:125 の </div> と :127 の <div> の間★。ユーザー指定(「詳細ログの上に表示」)どおり。
実測: d-watch-mock.jsx に「要レスキュー」「レスキュー」のヒット ★0件★。
CLAUDE.md §5 は「モックが画面レイアウト・文言・遷移の正」と定めています。★前例が2件あります★:
| 決定 | 何をしたか |
|---|---|
| ★D-120★ | モックに無いページ送りUIを新規に作った。「本決定は §5 の例外」と明記し、★ユーザーの原文を決定に転記★ |
| ★D-121★ | モックと違う色にした。同上+★コード側にも理由コメントを残した★ |
★同じ作法を踏襲すること:★
d-watch-mock.jsx は1文字も書き換えない★docs/decisions.md に ★「§5 の例外。ユーザー指示(2026-09-15)に基づく」★ と記録し、 ★指示の原文を引用する★DashboardScreen.tsx の該当ブロックの直上に、理由コメントを残す★ — ★★書かないと、次にモックと突き合わせた人が「余計なものがある」と判断して消します★★app/globals.css の CSS変数★を参照する。★JS 側で色を持たない★ (ReportsScreen.module.css:1-5 の作法)。★新しい色を発明しない★font-family を新しい .module.css で宣言しないこと★ — 宣言すると和文フォントが落ちて ★中国語フォントにフォールバックし、漢字が中華字形になります★(CLAUDE.md §0-3)useTheme() の t() を通す。★JP/EN の器を壊さない★(§9.6)reasons から日本語を作るのはここ★。段階3 の純粋関数は識別子だけを返す(D-111)★★段階6 までとは別のコミットにすること。★★ D-120 項6 の教訓: ★2つを同じコミットに入れると、どちらが壊したのか切り分けられない★。
| # | ファイル:行 | 現状 | 変更 |
|---|---|---|---|
| 1 | lib/config/index.ts:266 |
MAX_DATE_RANGE_DAYS = 31 |
★731★(2年。うるう年を含みうるので 730 ではなく 731) |
| 2 | lib/clean-logs/query.ts:86-92 |
超過で422 | ★そのまま。定数を読んでいるので自動的に追従する★ |
| 3 | components/features/detail-log/DetailLogScreen.tsx:129-132 |
画面側の先回り検証 | ★そのまま★ |
| 4 | components/features/detail-log/DetailLogScreen.tsx:43-44 |
useState(todayJst()) |
★★変更しない★★(既定は今日1日のまま。§1-6) |
| 5 | d-watch-api.yaml:253 |
「from-toは最大31日間、超過は422」 | ★文言を2年に★ |
| 6 | d-watch-api.yaml:1251 |
example: "date_from〜date_toは31日以内で指定してください" |
★同上★ |
★★定数名を変えないこと★★(MAX_DATE_RANGE_DAYS)。★値だけ変える。★
lib/clean-logs/query.ts:78-94 の検証は ★date_from と date_to の両方があるときだけ★走ります。 片方でも無ければ範囲の検証をしません。★したがって「未指定なら上限なし」は追加実装が不要です。★
★確認だけすること★ — テストで固定されているかを見て、無ければ足す:
| # | 入力 | 期待 |
|---|---|---|
| 9-1 | 両方指定・731日 | ★200★ |
| 9-2 | 両方指定・732日 | ★422★ |
| 9-3 | ★date_from のみ★ |
★200(上限の検証をしない)★ |
| 9-4 | ★date_to のみ★ |
★200★ |
| 9-5 | ★両方なし・facility_name のみ★ |
★200★ |
MAX_DATE_RANGE_DAYS を直接 import しているテストは追従しますが、 ★31 や "31日以内" を直書きしているテストは落ちます★。全部探して直すこと。
grep -rn "31日\|MAX_DATE_RANGE_DAYS" --include=*.ts --include=*.tsx --include=*.yaml .
★索引が入るまで、2年を選んだときは重くなります。★ 実測に基づく見込み:
| 現状(31日) | 2年(外挿。施設200件) | |
|---|---|---|
| 主クエリが読む行 | 約1万1千行 | ★約25万行★ |
clean_status_histories の走査 |
1塊 | ★254塊 × 全表走査 ≒ 6,430万行★ |
| CSVエクスポート | 55KB / 0.7秒 | ★約28MB / 約6分(下限)★ |
★★CSVは nginx の既定タイムアウト(60秒)を超えるため、2年分は落とせません(504)。★★ ★これは索引だけでは直りません★ — LIMIT がSQLに降りていないこと、 ORDER BY start_at DESC, id ASC の向きが混在していて filesort が消えないことが原因です。
★★この問題は段階8 で「押す前に分かる」形にします。★★
★★依頼済み★★: 索引7件(TeamWorks確認事項_第9回_20260915.md 1章)。 ★★別課題★★: LIMIT の押し下げと並び順の変更(D-131 決定D-4 の前提が崩れたため。 ★同時にやらないこと★ — D-131 申し送り⑤が「ページ番号をURLに持たせる件と同時にやるな」と 書いているのと同じ理由で、切り分けができなくなる)。
★★ユーザー指示(2026-09-15。原文)★★:
あとCSVで出力できる期間の限界が来たらこの期間だとCSV出力できませんとわかるようにしてください
★段階7 で期間の上限を2年に広げた結果、CSVは現実的に落とせない大きさになりえます。★ ★「押してから6分待たされて 504 が返る」のが最悪の形です。★ ★押す前に分かるようにします。★
★★日数で決めないこと。★★ 理由:
| 絞り込み | 2年ぶんの行数(外挿) | 落とせるか |
|---|---|---|
| ★1店舗だけ★ | ★約1,300行★ | ★余裕で落とせる★ |
| 全店舗(施設200件) | ★約25万行★ | ★落とせない★ |
★同じ「2年」でも、絞り込み次第で200倍違います。★ ★日数で切ると、1店舗の2年ぶん(軽い)まで巻き添えで禁止することになります。★
★★ユーザーは「店舗指定のみなら上限なし」と明示しています(§1-6)。★★ ★行数で決めれば、その指定と自然に両立します。★
lib/config/index.ts の MAX_DATE_RANGE_DAYS の直後に足す。
/** * CSVエクスポートの行数上限(ユーザー指示 2026-09-15 / 指示書AI 段階8) * * ★日数ではなく行数で決めている★ — 同じ2年でも、1店舗なら約1,300行、 * 全店舗(施設200件)なら約25万行で、200倍違うため(指示書AI 10-1)。 * * ★★この値は暫定です。実データと索引が入ったら測り直すこと★★ — * 現在の根拠は「31日・502行で0.73秒」という実測1点からの外挿にすぎない。 * ★DB側は塊ごとの全表走査になるので、行数に対して二乗で効く★ * (`clean_status_histories.clean_timetable_id` に索引が無いため。第9回 1-2 で依頼中)。 * 20,000行なら 20塊 × 20,000行 = 40万行の走査で収まる見込み。 * * ★CSVの大きさの目安★: 1行あたり 111 バイト(実測。BOM・CRLF・10列)。 * 20,000行 ≒ 2.2MB。 */ export const MAX_CSV_ROWS = 20_000;
app/api/clean-logs/export/route.ts★絞り込みを適用したあと、CSVを組み立てる前に行数を見る。★
MAX_CSV_ROWS 以下 → これまでどおりCSVを返す{ error: { code, message, details } }。CLAUDE.md §9.3)message: "この期間はCSVで出力できません。対象が{n}件で、上限の{max}件を超えています。期間を短くするか、店舗で絞り込んでください。"
details: { total: n, max: MAX_CSV_ROWS }
★★500 ではなく 422 にすること★★ — 利用者の指定が上限を超えているという話で、 サーバの不具合ではありません。★details に実数を入れること★ — 「あと何件減らせばよいか」が分かります。
★★CSVを組み立ててから長さで判断しないこと★★ — 25万行ぶんの文字列を作ってから捨てるのでは、 ★メモリを使い切る危険が残ったまま★です。判断は組み立ての前。
★d-watch-api.yaml の /clean-logs/export に 422 の宣言を足すこと★(先にYAML。§6-1)。
components/features/detail-log/DetailLogScreen.tsx
★★画面は既に件数を持っています。★★ 一覧を取ったときの meta.total です。 ★CSVのために問い合わせを増やす必要はありません。★
meta.total > MAX_CSV_ROWS のとき:
disabled)★★理由を出さずに disabled だけにしないこと★★ — ★押せないボタンは、理由が書いていないと不具合と区別がつきません★(D-053 がUIを削除した理由と同じ)。
★★文言は t() を通すこと★★(JP/EN の器。§9.6)。★数値は画面側で差し込む★(D-111)。
★★サーバ側の422 を消さないこと★★ — 画面で止めても、APIを直接叩く経路が残ります。 ★画面は「親切」、サーバは「歯止め」で、役割が違います。★
| # | 状況 | サーバ | 画面 |
|---|---|---|---|
| 8-1 | 0件 | ★200(ヘッダ行だけのCSV)★ | ボタン有効 |
| 8-2 | 19,999件 | ★200★ | ボタン有効 |
| 8-3 | ★ちょうど 20,000件★ | ★★200★★ | ★ボタン有効★(★「上限」は含む★) |
| 8-4 | ★20,001件★ | ★★422★★ | ★★ボタン無効+理由の表示★★ |
| 8-5 | 1店舗・2年・1,300件 | ★200★ | ★ボタン有効★(★期間が長くても行数が少なければ出せる★) |
| 8-6 | 日付未指定・全店舗 | ★件数次第★ | ★同上★ |
★★8-3 と 8-4 の両方を必ずテストに入れること★★ — 境界の片側だけだと、 > と >= の取り違えが通ってしまいます(CLAUDE.md §9.4)。
段階6(ダッシュボード)・段階7(期間上限)・段階8(CSV上限)は、★3つとも別のコミット★。
docs/decisions.md★★採番の前に必ず実測すること★★:
grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md | tail -3
★★docs/decisions.md は番号順に並んでいません。★★ 末尾を見て最大値を判断しないこと。
書くこと:
CLAUDE.md §5 の例外★。D-120 / D-121 と同じ扱い)。ユーザー原文を転記★select の列を増やした理由と、そのコスト(約4MB)★MAX_CSV_ROWS = 20_000 が実測1点からの外挿にすぎない暫定値であること★docs/handoff.md に追記MAX_CSV_ROWS を実データで測り直すこと★★ — ★索引が入ると通る行数が変わる★。 ★暫定値のまま放置しない★LIMIT の押し下げと並び順の変更が別課題として残ること★★★段階ごとにコミットすること。★★ ★git add -A を使わず、ファイルパスを明示する。★
npx tsc --noEmit # 0 であること npm run lint # 0 であること npm test # 全パス
★★「テストが通った」を安全の証明にしないこと★★ — この指示書で扱う3条件は、 ★ローカルのデータでは1件も発火しません★(実測: 最終データ日 2026-09-13 を当該日としたとき R-1 は0件。R-2 は4件、R-3 は4件)。 ★テストが通ることと、本番で正しく動くことは別です。★
preview_start で立てること。npm run dev を Bash で叩かない★).env.local は既にテストDB(133.125.238.144:13306 / clean_watch)を向いており、 USE_FIXTURES=false です。
★★テストDBのデータは 2026-09-13 で終わっています。★★ ★今日の日付で開くと全条件が0件になります。★ 確認するときは日付を指定すること:
/api/dashboard?date=2026-09-13 → R-2 が4件、R-3 が4件出るはず /api/dashboard?date=2026-09-12 → R-1 が1件(施設6)、R-2 が3件出るはず
★★訂正(2026-09-15。指示書AJ 段階3)★★
★上の期待値には誤りがあります。★ ★元の数字は消していません★ — ★次に読む人が、この指示書を信じて実装の数を合わせにいくのを防ぐため★です。 ★★実装は正しく、誤っていたのは監督が書いたこの期待値のほうです。★★
上の記述 訂正 ?date=2026-09-13→ ★「R-2 が4件」★★★誤り。正しくは5件★★ — ★施設7を数え漏らしていました★(09-13 に予定が無いが、09-12 評価で出る施設。§1-5 でわざわざ「当該日に予定が無い施設も出す」と指定したのは、まさにこの施設のためでした) ?date=2026-09-13→ 「R-3 が4件」★4件で正しい★。★ただし当時は上限5件だったため、API には2件しか出ていませんでした★(施設1・施設10 が上限で切られていた。指示書AJ §1-1) ★★実測(2026-09-15。上限撤廃後の
develop)★★:?date=2026-09-13 rescue_required 7件 施設9 [escalation_l3, completion_drop] 連続6 施設6 [escalation_l3, completion_drop] 連続4 施設7 [escalation_l3] 連続4(evaluated_on=2026-09-12) 施設3 [escalation_l3] 連続3 施設5 [escalation_l3] 連続3 施設10 [completion_drop] 連続2 施設1 [completion_drop] 連続1 → R-2(escalation_l3)は★5件★、R-3(completion_drop)は★4件★、R-1 は 0 件 ?date=2026-09-12 rescue_required 7件 施設6 [not_executed_streak, escalation_l3] / 施設9 施設7 [escalation_l3] 施設2 施設4 施設8 施設10 [completion_drop] → R-1 は★1件(施設6)★で記述どおり。R-2 は★3件★で記述どおり
★★施設7が 2026-09-13 の結果に含まれることを必ず確かめること★★ — ★09-13 に予定が無いのに出る★のが、§1-5 の指定どおりに実装できている証拠です。 ★出なければ、当該日で絞ってしまっています。★
| # | 落とし穴 | どうなるか |
|---|---|---|
| ★1★ | ★completion_rate を面積から再計算する★ |
1行の率(エリア単位)と施設日の率(面積加重)が混ざる。★数字が微妙にずれるだけで、例外もテスト失敗も出ない★ |
| ★2★ | ★ConsecutiveInput の新しい2列を optional にする★ |
詰め忘れが undefined のまま通り、★面積加重が 0/0 → null に化ける★ |
| ★3★ | ★当該日で母集団を絞る★ | ★予定の無い日のL3施設が消える★(施設7の例) |
| ★4★ | ★急落の閾値を 20 と書く★ |
後で 0.2 と 20 のどちらの単位か分からなくなる。★0.2 で統一★ |
| ★5★ | ★未実施を 0% とみなす★ | 前日未実施の翌日が全部「大幅上昇」扱いになる。★null のまま扱う★ |
| ★6★ | ★連続日数を要レスキュー用に数え直す★ | D-109 が潰した「2箇所で別々に計算する」に戻る |
| ★7★ | ★遡り幅を縮める★ | ★L3 が L1 に化ける★(例外もテスト失敗も出ない) |
| ★8★ | ★日本語を判定層で作る★ | EN切替で和文が残る(D-111) |
| ★9★ | ★新しい .module.css に font-family を書く★ |
★漢字が中華字形になる★ |
| ★10★ | ★段階6 と段階7 を同じコミットにする★ | 壊れたときに切り分けられない |
| 項目 | なぜ |
|---|---|
| ★分類(ホールディングス/屋号/店舗)の復活★ | TeamWorks 確認待ち(第9回 6章・7章)。★/reports/kpi が2顧客で500を返す件も、これとセットでしか直せない★ |
| ★総清掃面積の新画面★ | 分類の絞り込みに乗るため、上が先 |
★LIMIT の SQL 押し下げ★ |
D-131 決定D-4 の作り直し。★別課題★ |
★ORDER BY の向き揃え★ |
★レスポンスの並び順が変わるのでユーザー確認が要る★ |
| ★全ルートへの 5xx 宣言★ | 既に別課題として記録済み |
| ★出発失敗を未実施と別に判定する★ | ★データがDBに無い★(第9回 11章で照会中) |
| ★点検記録2本★ | ユーザーが保留中 |