Newer
Older
client_dwatch / docs / handoff / 指示書Z_エリア別完了率を実装する.md

指示書Z — エリア別完了率を実装する(501 仮実装をやめ、施設×日付のエリア一覧にする)

項目 内容
実行環境 ★Cursor 内の Claude が実装します★(.claude/commands/d-watch.md の役割分担表を参照)
実行状態 ★★完了(2026-09-11)★★ — ★段階0〜8 を実行。★★push は未実施(監督確認待ち)★★★。決定番号 ★D-119★ / docs/handoff.md ★追記39★
実行順 ★★指示書Y(docs/handoff/指示書Y_DATETIMEを日本時間として読む.md)と 指示書W(docs/handoff/指示書W_連続問題施設を実DBから作る.md)の完了後に実行すること★★。★★同時に走らせないこと★★(★理由は下の「実行順」節★)
対象 ★★TeamWorks 第8回 A9-2 で「エリア毎の清掃完了率は取得可能」と回答があった★★。★ただし当方が聞いた粒度(1回の清掃の中を売り場・通路・レジ前に区切った内訳)とは違う答え★。★取得できるのは「1エリア(=1ロボット)につき1つの完了率」★。★★そこで GET /clean-logs/{id}/areas の意味を「その清掃ログと同じ施設・同じ日の全エリアの一覧」に定義し直し、501 仮実装をやめて本実装する★★
取得する決定番号 ★★**着手直前に `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-114` ですが、指示書Y と 指示書W がそれぞれ1つずつ取る見込みです。★★見込み番号は書きません — 必ず自分で実測してから採番すること★★★
前提 ★D-114(★第8回回答の受領。★★項4 が本書の出所★★。行番号は着手時に grep -n "^### D-114" docs/decisions.md で実測すること)★ / ★D-034(docs/decisions.md:208。★「確認事項#10 が未確認なので 501 仮実装」と決めた当の決定★。★本書はこれを部分的に上書きする★)★ / ★D-106(:5830。clean_error による判定)★ / ★D-107(:5993。★isFutureSchedule() の導入。★★落とし穴1「未実施を 0% にすると null が 0 に化ける」★★)★ / ★D-109(:6328。当期の分母から未来を外す)★ / ★D-113(:6840。precheck は分数そのもの)★ / D-035(:216。1清掃エリア=1ロボット)/ D-040(:220。★API契約はYAMLが先★)/ D-041(:224。期待値表テスト)/ D-042(:226。書き込み許可4カラム)/ D-044(fixtures で開発)/ D-050(段階ごとに検証してからコミット)/ D-056(schema.prisma は生成物)/ D-100(MariaDB 10.11 とローカル MySQL 8.4 の差)/ ★指示書Y と 指示書W が段階末で追記する記録(★★着手時に実測して読むこと★★)★
段階数 ★9段階★(段階0〜段階8)。★コミットは段階1 / 2 / 3 / 4 / 5 / 6 / 8 の7つ★
★着手前確認★ ★★なし★★ — ★「施設内のエリアごとの完了率を出す画面として作る」はユーザー確定済み(2026-09-11)★。★データ元・全体率の出し方・並び順・未実施の扱い・パスを変えないことは、すべて下の「採る方針」で監督が決定済み★。★★ユーザーの回答を待たずに段階0 から通しで実行してよい★★。★★ただし「仕様が不明だと感じた点」は勝手に埋めず、段階の切れ目で止めて質問すること★★(CLAUDE.md §0-1 / §7)
着手前の基準 ★★着手直前に自分で npm test / npx tsc --noEmit / npm run lint を実行し、その実測値を基準にすること★★。★指示書Y・W の完了後は本書作成時点と数字が変わっています。参考値を鵜呑みにしないこと★
作成 2026-09-11。★作成時点の決定事項最終番号は D-114、docs/handoff.md の最新は 追記34★。★★着手時に実測すること★★

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

★この指示書を読んでいるあなたへ★ — .claude/commands/d-watch.md の「実装は Cursor 側」は 2つの環境の役割分担です。Cursor の中で動いている Claude は実装担当です。 ★調査や改訂ではなく実装を行ってください。★ ★★うまくいかなければ段階の切れ目(コミット単位)で止めて構いません★★ — ★段階1(API契約)→ 段階2(型と純粋関数)→ 段階3(fixtures)→ 段階4(データ層)→ 段階5(APIルート)→ 段階6(画面)の順に、★どこで止めても tsc / lint / test が緑になる形★に 並べてあります★。★★段階5 を終えるまで画面は 501 のメッセージを出したままです(想定内)★★。


★★冒頭に読むべきこと — 何が確定して、何が確定していないか★★

1. ★TeamWorks 第8回 A9-2 の回答(★2026-09-11 受領。原文★)★

Q9-2. エリア毎の清掃完了率は取得可能か
A9-2.robot_task_reportsからエリア毎の清掃完了率は取得可能です. 正確に言えば清掃ロボット毎の清掃率ですが,
清掃ロボットはエリアに紐づいているため, もし複数台紐づいているのであれば計算で導き出すことになります.

2. ★★これは当方が聞いた粒度とは違う答えです(★ここを取り違えないこと★)★★

内容 取得可否
★当方が聞いたこと★ ★1回の清掃の中を売り場・通路・レジ前に区切ったときの★★区域別の内訳★★★ ★★取得できない(依然として未解決)★★
★先方が答えたこと★ ★清掃ロボット毎の清掃率。★★ロボットはエリアに紐づくので「1エリアにつき1つの完了率」★★★ ★取得できる★

★★本書が実装するのは後者だけです★★。 ★★前者(確認事項#10 の本来の問い)は依然として未解決であり、本実装で解決したことにはなりません★★ (★第9回の確認事項に「エリアの粒度の聞き直し」として積んであります。D-114 項7-6★)。 ★申し送りに必ず残すこと(段階8)★。

3. ★ユーザー確定(2026-09-11)★

★★「施設内のエリアごとの完了率」を出す画面として作る★★

★モックのエリアモーダルは元から「施設 × 日付 → その施設のエリア一覧」の形★(d-watch-mock.jsx の getAreaData(facility) は 施設名だけ を引数に取る。★おおよそ276-282行。行番号は実測すること★)。 ★★つまりモックが要求している形は、A9-2 で取得できる粒度でそのまま実現できます★★。

4. ★★現状(★実装前にこの目で確かめること★)★★

場所 現状
app/api/clean-logs/[id]/areas/route.ts ★db.cleanLogs() で 404 判定だけして、★★常に 501 を返す★★★
components/features/detail-log/AreaModal.tsx ★★501 のメッセージを表示するだけの器★★。★フィルタボタン(問題エリアのみ / 全件)は onClick={() => undefined} の飾り★
components/features/detail-log/DetailLogScreen.tsx の openAreas() ★★「失敗側が通常経路」というコメント付きで、501 のメッセージを拾ってモーダルに渡している★★
d-watch-api.yaml(★おおよそ 362-400 行★) ★description に「確認事項#10 に依存 / 取得不可なら Phase B 送りで 501」★。★"501" のレスポンス定義あり★。★冒頭の注記(★おおよそ 23-24 行★)にも同じことが書いてある★
lib/types/ ★★AreaCompletionItem 相当の型が無い★★
lib/fixtures/ ★★エリア別のサンプルデータが無い★★
CleanLogItem(lib/types/index.ts) ★★section_id / section_name を持っていない★★

★★採る方針(★監督が決定済み。理由ごと理解してから実装すること★)★★

★決定1: API のパスは変えない★

★GET /clean-logs/{id}/areas のまま★。★★意味だけを「その清掃ログと同じ施設・同じ日の 全エリアの一覧(★その清掃ログ自身を含む★)」に定義し直す★★。

  • ★理由★: ★画面は清掃詳細ログの行から開く★(CleanLogTable.tsx の「エリア別詳細を見る →」ボタン)。 ★id で引く形が自然★。★パスを /facilities/{id}/areas?date=... のように変えると呼び出し側も変わる★。
  • ★★{id} は「その施設・その日を特定するための入口」であって、「その1行だけのデータを引くキー」ではない★★ — ★このズレを d-watch-api.yaml の description に明記すること(段階1)★。

★決定2: d-watch-api.yaml を先に直す★(CLAUDE.md §6-1 / D-040)

  • ★description の「確認事項#10 に依存 / 取得不可なら Phase B 送りで 501」を書き換える★
  • ★★"501" のレスポンス定義を削除する★★
  • ★冒頭の注記(## データソースに関する注記)の該当2行も直す★

★決定3: レスポンスに「全体の完了率」を足す★

★モックのヘッダは 施設名 | 日付 | 全体 xx% ◎ の形★(d-watch-mock.jsx のエリアモーダル。 ★おおよそ 1917-1975 行★)。★この「全体」はその施設・その日の全エリアを合算した値★。

  • ★★面積ベースで合算する★★: ★★Σclean_area ÷ Σtask_area × 100★★(CLAUDE.md §4.2)
  • ★★clean_ratio の単純平均は禁止★★ — ★担当面積の違う台を等しく扱うことになる★
  • ★★既存の completionRate(cleanedArea, targetArea)(lib/judge/kpi.ts)を使うこと★★。 ★新しい関数を作らない★。★分母0で null を返す挙動もそのまま使う★

★決定4: 未実施のエリアは completion_rate を null にする★(★0% と混ぜない★)

★「0%しか清掃できなかった」と「そもそも実施していない」は別物★ (★D-106 / D-107 落とし穴1 と同じ地雷★。★判定はどちらも × なので、混ぜても画面もテストも赤くなりません★)。

  • ★★completionRateFromHistory()(lib/db/clean-log-mapping.ts)を使えば自動的にこうなります★★。 ★自前で history.clean_ratio を読まないこと★
  • ★★YAML の completion_rate を nullable: true にする★★(段階1)

★決定5: 判定は judgeGrade() を使う★

  • ★★モックの areaGrade() は cp >= 90 ? "◎" : cp >= 70 ? "○" : cp >= 10 ? "△" : "×" で、 CLAUDE.md §4.1 の「90%超」= > と違う★★(d-watch-mock.jsx。★おおよそ282行★)
  • ★★モックのロジックを移植しないこと★★。★移植してよいのは文言・列・並び順・レイアウトだけ★
  • ★judgeGrade(null) は ×(未実施)★。★これで未実施エリアも「問題エリア」に入る★

★決定6: データ元は clean_status_histories★(★clean_task_reports は使わない★)

  • ★prismaSource.cleanLogs() が既に読んでいるテーブル★。★面積(task_area / clean_area)・ 完了率(clean_ratio)・エリアID(section_id)を持っている★
  • ★★理由★★: ★cleanLogs() の completion_rate と数字が食い違わないため★。 ★★同じ画面(清掃詳細ログの行 → そこから開くモーダル)に、定義の違う2つの数字が並ぶ状態を作らない★★
  • ★clean_task_reports は★★シードに1行も無く★★、clean_status_histories と値が一致するかも未検証★
  • ★A9-2 の robot_task_reports は DDL 上の clean_task_reports を指すと思われるが★★未確認★★ (第9回で聞く。D-114 項7-5)

★決定7: 完了率の出し方を cleanLogs() と揃える(★★非対称を意図して残す★★)★

★★completionRateFromHistory() は clean_ratio の生値をそのまま返しており、面積から再計算していません★★ (lib/db/clean-log-mapping.ts。★実装を自分の目で確かめること★)。

出し方
★各エリアの行の completion_rate★ ★★clean_ratio の生値★★(completionRateFromHistory() を流用)
★全体の overall_completion_rate★ ★★Σclean_area ÷ Σtask_area × 100(面積ベース)★★

★★この非対称は意図した挙動です★★。★★合計が行の平均と一致しないことがあります★★。

  • ★★completion_rate を返す関数と、全体を計算する関数の両方の docstring に明記すること★★
  • ★★段階8 の申し送りにも書くこと★★
  • ★理由★: ★行の値を面積から再計算すると cleanLogs() の完了率と食い違う(決定6 と同じ理由)★。 ★一方、全体を clean_ratio の平均で出すと担当面積の違う台を等しく扱うことになる(決定3)★。 ★★どちらを選んでも「揃わない」ので、揃わないことを明示するほうを選んだ★★

★決定8: filter=problem|all はサーバ側で絞る★

  • ★契約(YAML)にあるので維持する★。★★既定は problem★★
  • ★★problem = 判定が △ または ×★★(★未実施は judgeGrade(null) = × なので入る★)
  • ★★overall_completion_rate と件数は filter に関係なく全エリアから計算する★★ — ★「問題エリアのみ」を選んだら全体率まで変わる、という挙動にしないこと★

★決定9: 未来の予定は除外する★

★isFutureSchedule()(lib/db/clean-log-mapping.ts)を使う★(D-107 / D-109)。 ★cleanLogs() と同じ理由・同じ関数★ — ★外すと未来の清掃が「未実施」として並びます★。

★決定10: 並び順は完了率の昇順。★★未実施(null)は先頭★★★

  • ★モックに合わせる★(areas.sort(function (a, b) { return a.cp - b.cp; }))
  • ★★悪いものから見る画面なので、未実施(=いちばん悪い)を先頭に置く★★
  • ★同値のときは section_id の昇順で安定させる★(★並びが実行ごとに変わらないようにするため★)
  • ★★フロントで並べ替えないこと★★(CLAUDE.md §9.6。★ソート順はAPIから受け取る★)

★決定11: meta は付けない★

★CLAUDE.md §9.3 の「一覧APIは必ず meta: { total, page, per_page } を返す」は ページングする一覧の話です★。

  • ★★エリア数は最大でも数個(P1 で最多の施設7 でも3件)。ページングの対象ではない★★
  • ★先例: GET /clean-logs/{id}/advice も meta を返していない★(CleanLogAdviceResponse)
  • ★★ただしモックの件数行(問題エリア(3 / 16エリア))を出すために total_count / problem_count は返す★★(★これは meta ではなく本文の一部★。理由は決定12)

★決定12: total_count と problem_count を返す★

★モックの件数行はこうなっています(★文言は段階6 の表を正とする★)★:

filter=problem のとき → 問題エリア(3 / 16エリア)
filter=all     のとき → 全16エリア(完了率昇順)

★★サーバ側で絞る(決定8)と、filter=problem のときフロントは「全体で何エリアあるか」を 知る手段がありません★★。★だから両方の件数を返します★。 ★★どちらも filter の前に数えること★★。

★★却下した案(★同じ案を再提案しないために、理由ごと残します★)★★

案 却下の理由
★パスを /facilities/{id}/areas?date= に変える★ ★呼び出し側(清掃詳細ログの行)が施設IDと日付を組み立て直すことになる★。★契約変更の影響が広がる★(決定1)
★clean_task_reports を読む★ ★シードに1行も無く、clean_status_histories と値が一致するかも未検証★。★同じ画面に定義の違う数字が並ぶ★(決定6)
★行の completion_rate も面積から再計算して揃える★ ★cleanLogs() の完了率(=一覧の行に出ている数字)と食い違う★(決定7)
★全体を clean_ratio の単純平均で出す★ ★担当面積の違う台を等しく扱うことになる★(決定3。CLAUDE.md §4.2 違反)
★未実施エリアを 0% として並べる★ ★D-107 落とし穴1 と同じ地雷★(決定4)
★CleanLogItem に section_id / section_name を足す★ ★★この API のためだけに一覧の型を変えない★★。★CleanLogItem は6画面が参照している★(決定14)
★フロントで絞り込み・並べ替えをする★ ★CLAUDE.md §9.6「判定結果・ソート順はAPIから受け取る。フロントで再計算しない」★

★決定13: 未実施行の面積は「DBの値をそのまま足す」★

★★既存の aggregateCleanStatsByFacility()(lib/db/report-mapping.ts)と同じ扱いにする★★:

taskArea  += history?.task_area  ?? 0;
cleanArea += history?.clean_area ?? 0;
  • ★★未実施だからといって当方で 0 に潰さないこと★★。★履歴の行が無いときだけ 0★
  • ★理由★: ★同じ「面積ベースの完了率」がレポート画面(F-04)とこの画面で違う定義になるのを避ける★
  • ★ローカルDBでは未実施行の task_area は 0 なので差は出ません★ (scripts/seed-local-db.ts の areaFields。★★シードは読むだけ。編集しないこと★★)。 ★★実DBで未実施行の task_area が非0だと全体率が下がります★★ → ★申し送りに書くこと★

★決定14: 型は areas 専用に新設する★(CLAUDE.md §9.2)

  • ★★lib/types/ に AreaCompletionItem を新設★★(★YAML のスキーマ名と揃える★)
  • ★★CleanLogItem は変更しないこと★★

★調査済みの事実(★現物で裏を取ってから使うこと。★★行番号は必ず実測すること★★)★

1. ★clean_status_histories が持っているもの(prisma/schema.prisma)★

カラム 型 備考
facility_id / section_id BigInt ★エリアの特定に使う★
clean_timetable_id BigInt ★★FK も KEY も無い★★(include で辿れない。手動 join)
clean_error Int ★1 = 異常(清掃未実施)★
task_area / clean_area Float ★★面積ベースの全体率の分母・分子★★
clean_ratio Float ★行の completion_rate の元★(double(4,1))

2. ★section_name の取り方★

★clean_timetables.section_id → sections.section_name★。 ★prismaSource.cleanLogs() は既に include: { section: { include: { robots: ... } } } で section を引いています★ — ★★同じ書き方が使えます(section にはリレーションがあるので include してよい)★★。

★ローカルDBの section_name は 清掃エリア(ロボット107) の形★ (scripts/seed-local-db.ts。★★編集しないこと★★)。 ★★モックの「1F 生鮮エリア」のような名前にはなりません。これは想定内です★★ — ★section_name は DB の値をそのまま出すこと(当方で作らない)★。

3. ★同じ施設・同じ日に複数エリアがあるデータは実在する★

施設 ロボット(=清掃エリア)
★施設7(メガバリューマート緑野店)★ ★★3台★★(8111R5628060099 / ...0100 / ...0177)
施設5 2台
★それ以外★ ★1台★
  • ★★施設7 で3エリアが返ること★★を段階7 で実測すること
  • ★★1台しかない施設で1件だけ返るのが正しい挙動★★(★エラーではない★)
  • ★施設7 の3台が同じ日に予定を持つのは daysBack: 1 の1日だけ★ (lib/fixtures/clean-logs.ts。★実測すること★)。★指示書V が足した検証データ(daysBack: 2/3/4)は 2台ずつ★

4. ★★lib/fixtures/ にエリア別のデータが無い★★

  • ★★新設が要る★★(lib/fixtures/areas.ts。★lib/fixtures/index.ts に export * を足す★)
  • ★★既存の値は1行も変更しないこと★★。★★追加のみ★★ (lib/fixtures/clean-logs.ts の生成式に手を入れない。指示書U / V が同じ制約で通っています)

5. ★画面側の現状(★配線が要る★)★

場所 現状 段階6 でやること
DetailLogScreen.tsx の openAreas() ★501 のメッセージを拾う★ ★200 のレスポンスを受け取り、filter の状態を持つ★
AreaModal.tsx ★メッセージ1行を表示するだけ★ ★★一覧・バー・判定バッジ・件数行・空表示★★
AreaModal.tsx のフィルタボタン ★onClick={() => undefined} の飾り★ ★★再取得に配線する★★
components/ui/ProgressBar.tsx ★percent / tone(ok / wn / ng / ac / pr)★ ★★これを使う。バーを自作しないこと★★
components/ui/GradeBadge.tsx ★grade を受けて色分け★ ★★これを使う★★

6. ★★落とし穴(★踏むとテストが通ったまま壊れます★)★★

# 落とし穴
★1★ ★★未実施を 0 にする★★ — judgeGrade(0) も judgeGrade(null) も × なので画面もテストも赤くならない(D-107 落とし穴1)。★★completionRateFromHistory() を通すこと★★
★2★ ★★?? を `\ \ にする★★ —completion_rateは0が正当な値。\ \ ` だと ★0 が null に化ける★
★3★ ★★モックの >= を移植する★★ — 完了率ちょうど 90.0 が ◎ になる(正しくは ○)。★★judgeGrade() を通すこと★★
★4★ ★★丸めた値で判定する★★ — Math.round() は表示のときだけ(CLAUDE.md §4.3)
★5★ ★★isFutureSchedule() を通し忘れる★★ — 未来の予定が「未実施」として並ぶ(D-107)
★6★ ★★日付の突き合わせに row.start_at の生の Date を使う★★ — ★★jstIsoFromDb() を通してから .slice(0, 10) で JST の日付にすること★★(指示書Y / D-114 項1。aggregateCleanStatsByFacility() が同じことをしています)
★7★ ★★BigInt を Map のキーに === で使う★★ — ★.toString() すること★(既存コードと同じ)
★8★ ★★filter=problem のときに絞ったあとの配列から全体率や件数を計算する★★ — ★絞る前に計算すること★(決定8 / 決定12)
★9★ ★★分母0で 0 を返す★★ — ★completionRate() は分母0で null を返します。それをそのまま使うこと★(★全エリアが未実施の日は「全体 -」になるのが正しい★)

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

# 禁止
★1★ ★★git add -A の使用★★(CLAUDE.md §7。★必ずファイルパスを明示する★)
★2★ ★★メインDBのスキーマ変更・マイグレーション作成★★(prisma/schema.prisma は生成物。D-056)
★3★ ★★許可4カラム以外へのDB書き込み★★(D-042)。★★本書は読み取りのみ★★
★4★ ★★閾値のハードコード★★(90 / 70 / 10 を直書きしない。lib/config から読む。CLAUDE.md §7)
★5★ ★★丸めた値で判定すること★★(CLAUDE.md §4.3)
★6★ ★★モックの areaGrade()(>=)を移植すること★★(決定5)
★7★ ★★clean_ratio の単純平均を全体率にすること★★(決定3)
★8★ ★★lib/fixtures/ の既存値を変更すること★★(★追加のみ★)
★9★ ★★CleanLogItem を変更すること★★(決定14)
★10★ ★★clean_task_reports を使うこと★★(決定6)
★11★ ★★README.md / docs/code-map.md / docs/handoff/管理画面_設計検討_20260910.md /

.claude/worktrees/** / 他の指示書に触れること★★ | | ★12★ | ★★一時スクリプトをリポジトリに残すこと★★(★過去に scripts/tmp-measure.ts が残った★。段階7 参照) | | ★13★ | ★★仕様が不明な箇所を推測で実装すること★★(CLAUDE.md §0-1 / §7。★段階の切れ目で止めて質問する★) | | ★14★ | ★★section_name を当方で作る・整形すること★★(DBの値をそのまま出す) |


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

段階0  前提の確認と実測                    (コミットなし)
  ↓
段階1  d-watch-api.yaml を直す(+ spec.md) (1コミット)★API契約が先★
  ↓
段階2  型 + 純粋関数 + 期待値表テスト        (1コミット)★テストを先に書く★
  ↓
段階3  fixtures 追加 + docs/fixtures.md      (1コミット)
  ↓
段階4  DataSource.areaCompletions()          (1コミット)fixtures版 → prisma版
  ↓
段階5  APIルート(501 を外す)               (1コミット)
  ↓
段階6  画面(AreaModal の配線)              (1コミット)
  ↓
段階7  実測での検証                          (コミットなし)
  ↓
段階8  記録と push                           (1コミット)

★★各段階の終わりで npx tsc --noEmit / npm run lint / npm test がすべて緑であること★★(D-050)。


★★実行順 — 指示書Y と 指示書W の後★★

相手 関係
★★指示書Y★★ ★★必ず先に完了させること★★ — ★本書は jstIsoFromDb() / fromDbJst() に依存します★(落とし穴6)。★また指示書Y の段階4 でローカルDBが再シードされます★。★★本書の実行中にDBが作り直されると段階7 の実測が無意味になります★★
★★指示書W★★ ★★先に完了させること★★ — ★lib/db/index.ts を両方が触ります★。★★同時に走らせるとコンフリクトします★★
指示書Y・W が触るファイル ★lib/db/ / lib/format/date.ts / lib/config/ / scripts/seed-local-db.ts / docs/decisions.md / docs/handoff.md★

★★着手時に git status が clean であること、git log --oneline -5 に指示書Y と 指示書W の 完了コミットがあることを確認してから始めること★★。


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

0-1. 読むもの

資料 見るところ
★CLAUDE.md 全文★ ★§4.1 判定 / §4.2 面積ベース / §4.3 丸め / §5 モックが正 / §6-1 API契約が先 / §7 禁止事項 / §8 1エリア=1ロボット / §9.2 命名★
docs/decisions.md ★★D-114 項4(本書の出所)★★ / D-034(確認事項#10 の出所)/ D-106 / D-107 / D-109 / D-113。★★行番号は grep -n "^### D-1" docs/decisions.md で実測すること★★
docs/spec.md F-02(★「エリア別完了率グラフ: 501仮実装で先行」の記述がある★)
★d-watch-mock.jsx★ ★★エリアモーダル(おおよそ 1917-1975 行)★★ / ★getAreaData()(おおよそ 276-282 行)★ / areaGrade()。★★文言・列・並び順・ヘッダの形を実物から引くこと★★
d-watch-api.yaml ★/clean-logs/{id}/areas(おおよそ 362-400 行)★ / ★冒頭の注記(おおよそ 23-24 行)★
実装 components/features/detail-log/AreaModal.tsx / DetailLogScreen.tsx(openAreas())/ CleanLogTable.tsx(「エリア別詳細を見る →」)/ app/api/clean-logs/[id]/areas/route.ts / ★lib/db/index.ts の cleanLogs()★ / ★lib/db/clean-log-mapping.ts★ / ★lib/judge/grade.ts / lib/judge/kpi.ts★

0-2. ★現状の基準を控える★

npx tsc --noEmit          # 0 であること
npm run lint              # 0 であること
npm test                  # ファイル数 / テスト数を控える
git log --oneline -5      # 指示書Y・W の完了を確認
git status --short        # clean であること

0-3. ★★DBに何が入っているかを控える(段階7 の突き合わせに使う)★★

★★USE_FIXTURES=false で /api/clean-logs を叩き、施設7 の同じ日付の行が3件あることを確認する★★。 ★その3件の id を控えること★(★段階7 でその id を /areas に渡します★)。

# 例(実際のポート・認証は .env.local と proxy.ts の現状に合わせること)
curl -s 'http://localhost:3000/api/clean-logs?per_page=200' | ...

★★DBを直接書き換えないこと★★。★読むだけ★。

この段階の完了条件

  • ★上の資料をすべて読んだ★
  • ★tsc / lint / test の基準値を控えた★
  • ★施設7 の3エリアが同じ日にある清掃ログの id を控えた★

段階1 — ★d-watch-api.yaml を直す★(1コミット)

★★CLAUDE.md §6-1 / D-040: API契約を変えたくなったら、先に YAML を直す★★。

1-1. ★/clean-logs/{id}/areas の書き換え★

★★以下は内容の指定です。行番号・インデントは現物に合わせること★★。

箇所 やること
summary ★「エリア別完了率(施設×日付)」のように、施設×日付であることが分かる文言にする★
★description★ ★★「確認事項#10 に依存 / 取得不可なら Phase B 送りで 501」を削除★★し、代わりに次を書く: ①★{id} はエリアを特定するキーではなく、施設と日付を特定するための入口であること★ ②★★返すのは「その清掃ログと同じ施設・同じ日の全エリア(その清掃ログ自身を含む)」★★ ③★★completion_rate は clean_ratio の生値、overall_completion_rate は面積ベース(Σclean_area ÷ Σtask_area)で、両者は一致しないことがある★★(決定7) ④★並び順は完了率の昇順・★★未実施(null)は先頭★★・同値は section_id 昇順★ ⑤★★overall_* と件数は filter に関係なく全エリアから計算する★★ ⑥★TeamWorks 第8回 A9-2 が出所であること★
★"501"★ ★★レスポンス定義ごと削除★★
completion_rate ★★nullable: true を足す★★(未実施。決定4)
レスポンス本体 ★下の 1-2 の形にする★

1-2. ★★レスポンスの形(★これを正とする★)★★

        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [overall_completion_rate, overall_grade, total_count, problem_count, items]
                properties:
                  overall_completion_rate:
                    type: number
                    format: float
                    nullable: true
                    example: 63.2
                    description: |
                      その施設・その日の全エリアを合算した完了率(丸めない生値)。
                      Σclean_area ÷ Σtask_area × 100(面積ベース。CLAUDE.md 4.2)。
                      分母が0(全エリア未実施など)のときは null。
                      ★items[].completion_rate は clean_ratio の生値なので、
                      この値は items の平均とは一致しない★
                  overall_grade:
                    type: string
                    enum: ["◎", "○", "△", "×"]
                    example: "△"
                    description: overall_completion_rate を judgeGrade() にかけた判定(null は ×)
                  total_count:
                    type: integer
                    example: 3
                    description: その施設・その日の全エリア数(filter の前に数える)
                  problem_count:
                    type: integer
                    example: 2
                    description: 判定が △ または × のエリア数(filter の前に数える)
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/AreaCompletionItem" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/ValidationFailed" }

★components/schemas に AreaCompletionItem を新設する★:

    AreaCompletionItem:
      type: object
      required: [section_id, section_name, completion_rate, grade]
      properties:
        section_id: { type: integer, example: 107 }
        section_name:
          type: string
          example: "清掃エリア(ロボット107)"
          description: ★sections.section_name の生値。当方で整形しない★
        completion_rate:
          type: number
          format: float
          nullable: true
          example: 42.5
          description: ★clean_status_histories.clean_ratio の生値(丸めない)。★★未実施は null(0 ではない)★★
        grade:
          type: string
          enum: ["◎", "○", "△", "×"]
          example: "△"
  • ★★422 は既存の共通レスポンスがあればそれを参照する。無ければ Error スキーマで書くこと★★ (★現物を確認してから決める。★★勝手に新しいエラー形式を作らない★★)
  • ★filter パラメータの定義(problem / all、既定 problem)はそのまま残す★

1-3. ★冒頭の注記を直す★

★## データソースに関する注記 の 「GET /clean-logs/{id}/areas はPudu Cloud APIのエリア別完了率取得可否(確認事項#10)に依存する。 取得不可の場合はPhase B送りとし、501を返す実装で仮置きする。」★を書き換える:

  • ★★TeamWorks 第8回 A9-2 で「1エリア(=1ロボット)につき1つの完了率」が取得可能と確定したこと★★
  • ★★データ元は clean_status_histories(clean_task_reports ではない)★★
  • ★★1回の清掃の中の区域別内訳(確認事項#10 の本来の問い)は依然として未解決★★

1-4. ★docs/spec.md の F-02 を直す★

★「エリア別完了率グラフ: Pudu Cloud API依存(確認事項#10が未解決)。501仮実装で先行」の行と、 M2 のチェックリスト・「外部待ち」表の該当行を、★★本書の内容に合わせて更新する★★ (★★該当箇所は grep -n "501" docs/spec.md / grep -n "#10" docs/spec.md で実測すること★★)。

★★docs/decisions.md / docs/handoff.md / CLAUDE.md はこの段階では触らない★★(段階8 でまとめて)。

この段階の完了条件

  • ★YAML から 501 の記述が消えている★(grep -n "501" d-watch-api.yaml で確認)
  • ★completion_rate が nullable: true★
  • ★AreaCompletionItem スキーマがある★
  • ★YAML が構文として妥当★(★リポジトリに検証手段があれば使う。無ければ目視★)
  • npx tsc --noEmit / npm run lint / npm test が段階0 の基準どおり
  • ★コミット(★ファイルパスを明示★)★: docs: 指示書Z 段階1 エリア別完了率のAPI契約を501仮実装から本実装に改める

段階2 — ★型と純粋関数と期待値表テスト★(1コミット)

★★テストを先に書くこと★★(CLAUDE.md §6-2 / §9.4 / D-041)。

2-1. ★型を新設する(lib/types/index.ts)★

/** GET /clean-logs/{id}/areas の1件(YAML: AreaCompletionItem) */
export interface AreaCompletionItem {
  section_id: number;
  section_name: string;
  /**
   * ★`clean_status_histories.clean_ratio` の生値(丸めない。CLAUDE.md §4.3)★
   * ★★未実施は `null`。`0`(0%で実施した)と混ぜないこと★★(D-107 落とし穴1)
   * ★★面積から再計算した値では**ない**★★ — `cleanLogs()` の `completion_rate` と
   *   同じ出し方に揃えてある(指示書Z 決定7)。
   *   ★このため `AreaCompletionsResponse.overall_completion_rate`(面積ベース)とは
   *   一致しないことがある。★★意図した非対称★★★
   */
  completion_rate: number | null;
  grade: Grade;
}

/** GET /clean-logs/{id}/areas のレスポンス */
export interface AreaCompletionsResponse {
  /**
   * ★★面積ベース: `Σclean_area ÷ Σtask_area × 100`(CLAUDE.md §4.2)★★
   * ★`clean_ratio` の単純平均ではない★(担当面積の違う台を等しく扱わないため)。
   * ★分母0のときは `null`★。★`filter` に関係なく全エリアから計算する★
   */
  overall_completion_rate: number | null;
  overall_grade: Grade;
  /** ★`filter` の前に数えた全エリア数★ */
  total_count: number;
  /** ★`filter` の前に数えた △× のエリア数★ */
  problem_count: number;
  items: AreaCompletionItem[];
}

/**
 * データ層が返す行(`ReportKpiRow` と同じ流儀)。
 * ★API に出る形ではない★ — 絞り込み・並べ替え・全体率の計算は APIルート側で行う。
 */
export interface AreaCompletionRow {
  /** 本清掃の `clean_timetables.id`(= `CleanLogItem.id`) */
  clean_log_id: number;
  facility_id: number;
  /** ★JST の日付 `YYYY-MM-DD`★(`jstIsoFromDb()` の先頭10文字) */
  date: string;
  section_id: number;
  section_name: string;
  /** ★`completionRateFromHistory()` の戻り(未実施は null)★ */
  completion_rate: number | null;
  /** ★面積ベース全体率の分子。履歴の行が無ければ 0★ */
  clean_area: number;
  /** ★面積ベース全体率の分母。履歴の行が無ければ 0★ */
  task_area: number;
}

★★CleanLogItem は変更しないこと★★(決定14)。

2-2. ★lib/judge/grade.ts に isProblemGrade() を足す★

★「問題エリア」の定義を1箇所にするため★:

/** ★「問題あり」の判定= △ または ×★(出発の有無を見ない版) */
export function isProblemGrade(grade: Grade): boolean {
  return grade === "△" || grade === "×";
}

★★既存の isProblem() はこれを呼ぶように書き換える★★(★★挙動は変えないこと★★。 lib/judge/__tests__/grade.test.ts が緑のままであること)。

2-3. ★純粋関数を新設する(lib/clean-logs/areas.ts)★

★lib/clean-logs/query.ts と同じ流儀★(★DBアクセスを混ぜない★):

export type AreaFilter = "problem" | "all";

/** `filter` の解釈。★既定は "problem"★。不正値は 422 */
export function parseAreaFilter(
  searchParams: URLSearchParams
): QueryParseResult<AreaFilter>;

/** その清掃ログと同じ施設・同じ日の行だけを取り出す(★自分自身を含む★) */
export function selectAreaRowsForLog(
  rows: readonly AreaCompletionRow[],
  target: { facility_id: number; date: string }
): AreaCompletionRow[];

/**
 * 判定・並べ替え・件数・全体率・絞り込みをまとめてレスポンスの形にする。
 *
 * ★★全体率は `completionRate(Σclean_area, Σtask_area)`(`lib/judge/kpi.ts`)★★ —
 *   ★`completion_rate` の平均ではない★。★★items の平均と一致しないのは意図した挙動★★
 *   (指示書Z 決定7)。
 * ★★`total_count` / `problem_count` / `overall_*` は `filter` の**前**に計算する★★。
 * ★並び順: 完了率の昇順。★★未実施(null)は先頭★★。同値は `section_id` の昇順★
 */
export function buildAreaCompletions(
  rows: readonly AreaCompletionRow[],
  filter: AreaFilter
): AreaCompletionsResponse;
  • ★★parseAreaFilter() は parseCleanLogQuery() と同じ fail(422, "validation_failed", ...) を使うこと★★
  • ★★閾値を直書きしないこと★★ — 判定は judgeGrade()、問題判定は isProblemGrade()

2-4. ★★期待値表(★これをそのままテストにすること。実装より先に書く★)★★

★A. buildAreaCompletions() の並び順と絞り込み★

★入力(clean_area / task_area は下のBで使うので、ここでは並び順に影響しない値でよい)★:

section_id completion_rate 期待 grade
101 95.0 ◎
102 42.5 △
103 ★null(未実施)★ ★×★
104 5.0 ×
105 70.0 ★△(「70%超」が○なので 70.0 は △)★
期待 値
★filter=all の並び★ ★★103(null) → 104(5.0) → 102(42.5) → 105(70.0) → 101(95.0)★★
★filter=problem の並び★ ★103 → 104 → 102 → 105(★101 が落ちる★)★
total_count ★★どちらの filter でも 5★★
problem_count ★★どちらの filter でも 4★★

★B. 全体率(★★面積ベースであることが分かるケース★★)★

★★単純平均と面積ベースで答えが変わる2台★★:

section_id task_area clean_area clean_ratio
201 1000 500 50.0
202 100 100 100.0
出し方 値 判定
★★面積ベース(正)★★ 600 ÷ 1100 × 100 = ★★54.5454...★★ ★△★
★単純平均(禁止)★ (50 + 100) ÷ 2 = 75.0 ○

★★テストは面積ベースの値を期待値にすること★★(★浮動小数なので toBeCloseTo を使う★)。 ★★このケースを必ず入れること★★ — ★単純平均で実装しても、面積が同じデータだけでは テストが緑のまま通ってしまいます★。

★C. 全体率の境界と分母0★

ケース Σclean_area Σtask_area 期待 overall_completion_rate 期待 overall_grade
ちょうど90.0 900 1000 90.0 ★○(90%超が◎)★
90.0 のすぐ上 901 1000 90.1 ◎
ちょうど70.0 700 1000 70.0 ★△★
ちょうど10.0 100 1000 10.0 ★△(10%未満が×)★
0% 0 1000 0 ×
★★全エリア未実施★★ 0 ★0★ ★★null★★ ★★×★★

★D. エリア数★

ケース 期待
★1エリアだけの施設★ ★★items は1件。エラーにしない★★
★★3エリアの施設(施設7)★★ ★items は3件★
★該当なし(filter=problem で全部 ◎○)★ ★★items は空配列(200。404 ではない)★★。total_count > 0 / problem_count === 0

★E. selectAreaRowsForLog()★

ケース 期待
同じ施設・同じ日 ★含む★
★その清掃ログ自身★ ★★含む★★
同じ施設・違う日 ★含まない★
違う施設・同じ日 ★含まない★

★F. parseAreaFilter()★

入力 期待
★指定なし★ ★★"problem"(既定)★★
filter=problem "problem"
filter=all "all"
★filter=critical(不正値)★ ★★422 validation_failed★★
★filter=(空文字)★ ★★仕様を決めてテストで固定すること★★(★parseCleanLogQuery() の result の扱いに合わせるのが自然★)

★G. isProblemGrade()★

grade 期待
◎ / ○ false
△ / × true

この段階の完了条件

  • ★上のA〜Gがすべてテストになっている★(lib/clean-logs/__tests__/areas.test.ts 等)
  • ★★Bのケース(面積が違う2台)が入っている★★
  • ★isProblem() の既存テストが緑のまま★
  • npx tsc --noEmit / npm run lint / npm test が緑(★テスト数が段階0 より増えている★)
  • ★コミット★: feat: 指示書Z 段階2 エリア別完了率の型と純粋関数を足す

段階3 — ★fixtures を追加する★(1コミット)

3-1. ★★lib/fixtures/ の既存値は1行も変更しないこと★★

★新規ファイル lib/fixtures/areas.ts を作り、lib/fixtures/index.ts に export * を1行足す★。 ★★それ以外の fixtures ファイルには触らないこと★★。

3-2. ★何を作るか★

★AreaCompletionRow[] を作る★。★lib/fixtures/clean-logs.ts が公開している cleanLogs の 既存の行から clean_log_id / facility_id / date を引くこと★ (★★手書きの日付を新たに作らない★★ — at() / dateOnly() を使い、cleanLogs と日付がズレないようにする)。

★★仕込むケース(★境界値を必ず入れる★)★★:

# ケース completion_rate task_area / clean_area
1 ★1エリアだけの施設★ 任意 面積を持つ
2 ★★3エリアの施設(施設7)★★ ★3件ぶん★ ★面積を持つ★
3 ★★未実施を含む★★ ★★null★★ ★0 / 0★
4 ★★面積が違う2台★★(単純平均と面積ベースで答えが変わる) 50.0 / 100.0 1000/500 と 100/100
5 ちょうど 90.0 90.0 面積も 90.0 と整合させる
6 ちょうど 70.0 70.0 同上
7 ちょうど 10.0 10.0 同上
8 0 0 1000 / 0
9 ★該当なし(全部 ◎○ の施設×日)★ 95.0 等 面積を持つ
  • ★★clean_area = task_area × completion_rate ÷ 100 になるように作る★★ (★ケース4 を除く★。★ケース4 は「面積が違う」ことが目的なので、行の clean_ratio と 面積が整合していてよい — ★★整合させたうえで全体が単純平均とズレることを確認する★★)
  • ★★未実施の行は completion_rate: null / task_area: 0 / clean_area: 0★★(決定13)
  • ★section_name は 清掃エリア(ロボット107) のような、★★シードと同じ形★★にすること (★実在の店舗名・住所を入れないこと。docs/fixtures.md §1★)

3-3. ★docs/fixtures.md に追記する★

★## 2. 清掃詳細ログ(clean-logs) の後ろに ### 2.5 エリア別完了率 のような節を足し、 ★★上の表と同じケース一覧★★を書くこと★。★★既存の節を書き換えないこと★★。

この段階の完了条件

  • ★lib/fixtures/areas.ts が新設され、index.ts から export されている★
  • ★★既存 fixtures の diff が index.ts の1行追加だけ★★(git diff で確認)
  • ★docs/fixtures.md に節が増えている★
  • npx tsc --noEmit / npm run lint / npm test が緑
  • ★コミット★: feat: 指示書Z 段階3 エリア別完了率のサンプルデータを足す

段階4 — ★データ層(DataSource.areaCompletions())★(1コミット)

4-1. ★インターフェースに足す★

export interface DataSource {
  // ...既存...
  /**
   * エリア別完了率の行(★全期間・全施設★)。
   * ★引数を取らない契約に従う★ — 施設・日付の絞り込みと `filter` はAPIルート側。
   */
  areaCompletions(): Promise<AreaCompletionRow[]>;
}

★★DataSource は引数を取らない、という既存の契約を崩さないこと★★ (lib/db/index.ts のインターフェースのコメント参照)。

4-2. ★fixtures 実装★

async areaCompletions() {
  return areaCompletions;   // lib/fixtures/areas.ts
},

4-3. ★★prisma 実装(★cleanLogs() と同じ作法で書くこと★)★★

★★新しい読み方を発明しないこと★★。prismaSource.cleanLogs() を読んで、同じ形で書く:

# やること 使うもの
★1★ clean_timetables を include: { section: true } で読む ★section にはリレーションがあるので include してよい★
★2★ clean_status_histories を別クエリで読む ★★FK が無いので include で辿れない(手動 join)★★
★3★ ★latestHistoryByTimetableId() で履歴を1件に絞る★ ★★cleanLogs() と同じ関数★★(規則を2箇所に散らさない)
★4★ ★precheck === CLEAN_TIMETABLE_PRECHECK.main の行だけにする★ ★事前チェック行を混ぜない★
★5★ ★★isFutureSchedule(fromDbJst(row.start_at), now) を除外★★ ★now は1回だけ作って使い回す★(決定9)
★6★ ★date は jstIsoFromDb(row.start_at).slice(0, 10)★ ★★落とし穴6★★
★7★ ★completion_rate は completionRateFromHistory(history)★ ★★決定4 / 決定7★★
★8★ ★task_area / clean_area は history?.task_area ?? 0 / history?.clean_area ?? 0★ ★★決定13(aggregateCleanStatsByFacility() と同じ扱い)★★
★9★ ★section_name は row.section.section_name の生値★ ★整形しない★
★10★ ★BigInt は Number() / .toString() で扱う★ ★既存と同じ★

★★clean_task_reports を読まないこと★★(決定6)。 ★★robots を include する必要はありません★★(section_name があれば足りる)。 ★★cleanLogs() のようにロボット0台で例外を投げる処理は入れないこと★★ — ★この API はロボットではなくエリアを列挙するので、ロボットの有無を見る必要がない★。

4-4. ★docstring に書くこと★

  • ★★completion_rate は clean_ratio の生値であり、面積から再計算していないこと★★
  • ★★全体率だけ面積ベースで出すため、合計が行の平均と一致しないこと(意図した挙動)★★
  • ★データ元が clean_status_histories であり、clean_task_reports ではない理由★
  • ★TeamWorks 第8回 A9-2 が出所であること★

この段階の完了条件

  • ★USE_FIXTURES=true / false の両方で areaCompletions() が動く★
  • ★docstring に 4-4 の4点が書いてある★
  • npx tsc --noEmit / npm run lint / npm test が緑
  • ★コミット★: feat: 指示書Z 段階4 エリア別完了率をデータ層から取れるようにする

段階5 — ★APIルート(501 を外す)★(1コミット)

5-1. ★app/api/clean-logs/[id]/areas/route.ts を書き換える★

1. id を整数にパース → 失敗なら 422(★既存のまま★)
2. filter を parseAreaFilter() で解釈 → 不正なら 422
3. db.cleanLogs() で対象ログを引く → 無ければ 404(★既存のまま★)
   ★facility_id と JST日付(scheduled_at.slice(0, 10))を取り出す★
4. db.areaCompletions() を読む
5. selectAreaRowsForLog(rows, { facility_id, date })
6. buildAreaCompletions(selected, filter) → 200 で返す
  • ★★db.cleanLogs() の呼び出しを残す理由★★: ★404 の判定と、施設・日付の特定★。 ★scheduled_at は既に JST の ISO 文字列なので .slice(0, 10) でよい★ (★★areaCompletions() 側の date と同じ作り方であることを確認すること★★)
  • ★エラーレスポンスは既存の errorBody() / errorResponse() をそのまま使う★
  • ★★try/catch と logApiError() の形を既存に合わせること★★
  • ★★501 を返すコードとコメントを完全に消すこと★★(grep -rn "501" app/ components/ で確認)

5-2. ★判定根拠のログ★

★advice/route.ts が logEvent("info", { event: "judge.advice", ..., thresholds_used }) を 出しています★。★★同じ流儀で event: "judge.areas" を出すこと★★ (★このルートも1回のクリックで1回しか呼ばれないので件数の問題は無い★)。 ★★overall_completion_rate の生値と、分子・分母(Σclean_area / Σtask_area)を含めること★★ — ★★あとで「全体率が行の平均と違う」と言われたときに検算できるようにするため★★。

この段階の完了条件

  • ★リポジトリ全体から 501 の仮実装が消えている★
  • ★filter 未指定で problem、filter=all で全件★
  • ★存在しない id で 404★ / ★filter=xxx で 422★
  • npx tsc --noEmit / npm run lint / npm test が緑
  • ★コミット★: feat: 指示書Z 段階5 エリア別完了率APIを本実装にする(501を外す)

段階6 — ★画面(AreaModal の配線)★(1コミット)

6-1. ★★モックから引く文言・列・並び順(★これを正とする★)★★

★d-watch-mock.jsx のエリアモーダル(おおよそ 1917-1975 行)から★:

箇所 文言・仕様
タイトル ★エリア別清掃完了率★
ヘッダ2行目 ★★`{施設名} {日付} 全体 {xx}% {判定}★★(★★全体はoverall_completion_rateをdisplayRate()` で丸めた値★★。★★行の完了率ではない★★)
フィルタボタン ★問題エリアのみ★ / ★全件★(★2つだけ★)
件数行(problem) ★★問題エリア({problem_count} / {total_count}エリア)★★
件数行(all) ★★全{total_count}エリア(完了率昇順)★★
空表示 ★★問題エリアはありません(全エリア◎○)★★
行の列 ★★エリア名(幅120・省略記号) → バー(可変幅) → {xx}%(右寄せ・幅40) → 判定バッジ★★
バーの色 ★◎○ → ok / △ → wn / × → ng★
行の強調 ★★△× の行はエリア名を太字+濃色にする★★(モックの fontWeight: 600 / color: c.tx)
フッタ ★閉じる★
並び順 ★★完了率の昇順(★APIが返した順をそのまま使う。フロントで並べ替えない★)★★

★★t() を通す文言は既存の AreaModal.tsx の書き方に合わせること★★(JP/EN の器を壊さない。CLAUDE.md §9.6)。

6-2. ★未実施エリアの表示★

  • ★★完了率は displayRate(null) → -★★(★0% と表示しないこと★)
  • ★★バーは 0 幅★★(ProgressBar に 0 を渡す)
  • ★判定バッジは ×★

6-3. ★配線★

場所 やること
DetailLogScreen.tsx の openAreas() ★★「失敗側が通常経路」のコメントごと書き換える★★。★AreaCompletionsResponse を受け取る★。★取得失敗時はモーダル内にエラー文言を出す(★★モーダルを開かない、にしないこと★★ — ボタンを押して何も起きないのが最悪)★
AreaModal.tsx ★items / overall_* / 件数 / loading / error を受け取る★。★フィルタボタンを onClick で親に伝える★
フィルタ切替 ★★filter を state に持ち、変わったら再取得する★★(サーバ側で絞るため。決定8)
AreaModal.module.css ★行・バー・件数行のスタイルを足す★。★★ProgressBar / GradeBadge を使い、バーやバッジを自作しないこと★★

★★grade をフロントで再計算しないこと★★(CLAUDE.md §9.6)。

この段階の完了条件

  • ★モーダルにエリア一覧が出る★
  • ★「問題エリアのみ」「全件」の切替が効く★(★★再取得されていることをネットワークで確認★★)
  • ★未実施エリアが - で先頭に出る★
  • ★該当なしのとき 問題エリアはありません(全エリア◎○) が出る★
  • ★ダーク/ライト、JP/EN が壊れていない★
  • npx tsc --noEmit / npm run lint / npm test が緑
  • ★コミット★: feat: 指示書Z 段階6 エリア別完了率モーダルを実データに配線する

段階7 — ★実測での検証(★コミットなし★)★

7-1. ★★一時スクリプトの作法(★過去に事故が起きています★)★★

★★一時スクリプトには成功時にも process.exit(0) を書くこと★★。

main()
  .then(async () => {
    await prisma.$disconnect();
    process.exit(0);      // ★★これが無いと Prisma の接続が残る★★
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });

★★main().catch(...) だけだと Prisma の接続が残り、実際に4時間39分ハングした事故があります★★。

★★一時ファイルをリポジトリに残さないこと★★ — ★過去に scripts/tmp-measure.ts が残りました★。 ★検証が終わったら削除し、git status が clean であることを確認すること★。

7-2. ★USE_FIXTURES=true での確認★

  • ★段階3 で仕込んだ境界値のケースが画面に出る★
  • ★★面積が違う2台のケースで、全体率が単純平均と違う値になっている★★
  • ★全部 ◎○ の施設×日で 問題エリアはありません(全エリア◎○) が出る★

7-3. ★★USE_FIXTURES=false(ローカルDB)での確認★★

  • ★★施設7 の清掃ログから開くと 3エリア返る★★(★段階0-3 で控えた id を使う★)
  • ★★1台しかない施設から開くと 1件だけ返る★★(★これが正しい挙動★)
  • ★★モーダルの各行の完了率が、清掃詳細ログ一覧の同じ施設・同じ日の行の完了率と一致する★★
    (★★決定6・決定7 が守れているかの決定的な確認★★)
  • ★未実施の清掃がある日で、そのエリアが - で先頭に出る★
  • ★★未来の予定の日付が混ざっていない★★(決定9)
  • ★filter=problem(既定)と filter=all で件数が変わり、★★overall_completion_rate は変わらない★★

★★ローカルDBでは task_area が全行 1000 の固定値です★★(scripts/seed-local-db.ts)。 ★★つまり実DBでしか「面積が違う」ケースは出ません — 面積ベースであることの証明は 段階2 のテスト(期待値表B)と段階7-2(fixtures)で行います★★。 ★★シードを編集して面積を変えないこと★★(★指示書Y が触ったばかりのファイルです★)。

7-4. ★回帰していないこと★

  • ★清掃詳細ログ一覧・ダッシュボード・アラート・レポートが従来どおり★
    (★★`lib/db/index.ts` を触っているので必ず見ること★★)
  • npm test / npx tsc --noEmit / npm run lint

この段階の完了条件

  • ★上のチェックがすべて埋まっている★
  • ★★一時スクリプトを削除し、git status が clean★★

段階8 — 記録と push(1コミット)

8-1. docs/decisions.md に追記(★★着手直前に実測した空き番号を使うこと★★)

grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md | tail -3

★★^### D-0 だけでは3桁台を拾えません★★。★見込み番号を書かないこと★。

★書く内容★:

# 内容
1 ★TeamWorks 第8回 A9-2 の原文と、★★聞いた粒度と違う答えであること★★
2 ★/clean-logs/{id}/areas の意味を「同じ施設・同じ日の全エリア」に定義し直したこと(決定1)★
3 ★★D-034(501 仮実装)を部分的に上書きしたこと★★。★★ただし確認事項#10 の本来の問い(区域別内訳)は未解決のままであること★★
4 ★データ元が clean_status_histories で、clean_task_reports を使わない理由(決定6)★
5 ★★行は clean_ratio の生値、全体は面積ベースという非対称(決定7)★★
6 ★未実施を null にしたこと(決定4)★
7 ★total_count / problem_count を返すことにした理由(決定12)★
8 ★却下した案(上の表)★
9 ★★申し送り(下の節をそのまま)★★

8-2. docs/handoff.md に追記(★番号は実測すること★)

★grep -n "^## 20" docs/handoff.md | tail -3 で最新の追記番号を確認してから採番する★。 ★段階ごとのコミットと、段階7 の実測結果を表にする★。

8-3. CLAUDE.md の更新(★該当があれば★)

★§5 の資料表や §4 に「エリア別完了率は 501 仮実装」と書かれた箇所が無いか grep -n "501\|エリア別" CLAUDE.md で確認し、あれば直す★。 ★★無ければ触らないこと★★。

8-4. push

git push origin develop

★★申し送り(★★段階8 で docs/decisions.md に必ず書くこと★★)★★

# 申し送り
★①★ ★★元の確認事項#10(1回の清掃の中の区域別内訳)は依然として未解決★★。★★本実装はそれとは別のもの★★ — 実装できたのは「1エリア(=1ロボット)につき1つの完了率」であり、売り場・通路・レジ前といった内訳ではない。★第9回の確認事項6 として積んである(D-114 項7-6)★
★②★ ★★A9-2 の robot_task_reports が DDL 上の clean_task_reports のことか未確認★★(第9回の確認事項5)。★仮にそうだとしても、本実装は clean_status_histories を読んでいるので影響しない★
★③★ ★★行の完了率は clean_ratio の生値、全体は面積ベース(Σclean_area ÷ Σtask_area)という非対称★★。★★合計が行の平均と一致しないのは意図した挙動★★。★将来これを揃えるなら、cleanLogs() の completion_rate の出し方ごと変える必要がある(画面の一覧の数字が変わる)★
★④★ ★★1エリアに複数台が紐づく場合の計算★★(A9-2 の「もし複数台紐づいているのであれば計算で導き出す」)は ★P1 では発生しない★(1清掃エリア=1ロボット。CLAUDE.md §8 / D-035)。★★式を buildAreaCompletions() の1箇所に閉じ込めてあるので、必要になったらそこに足すだけで済む★★
★⑤★ ★★未実施行の task_area を DBの値のまま分母に足している★★(決定13。レポートの aggregateCleanStatsByFacility() と同じ扱い)。★ローカルDBでは 0 なので差が出ないが、★★実DBで未実施行に非0の task_area が入ると全体率が下がる★★。TeamWorks に確認する価値がある★
★⑥★ ★★section_name はローカルDBでは 清掃エリア(ロボット107) の形★★で、モックの「1F 生鮮エリア」のような名前ではない。★実データが入れば実名になる(当方で整形しない)★
★⑦★ ★★areaCompletions() は DataSource の契約どおり全期間・全件を読む★★。★cleanLogs() と同じボトルネックを持つ(年間13万行)。期間で絞るなら DataSource の契約から見直すこと(D-092 項3-3 と同じ課題)★

完了条件(全体)

  • ★d-watch-api.yaml から 501 の記述が消え、AreaCompletionItem が定義されている★
  • ★docs/spec.md の F-02 が更新されている★
  • ★★期待値表A〜Gがテストになっている(★特にB: 面積が違う2台★)★★
  • ★lib/fixtures/areas.ts が新設され、既存 fixtures の値は変わっていない★
  • ★docs/fixtures.md にケースが追記されている★
  • ★DataSource.areaCompletions() が fixtures / prisma の両方で動く★
  • ★APIが 200 を返し、filter がサーバ側で効いている★
  • ★モーダルが実データを表示し、フィルタボタンが飾りでなくなっている★
  • ★★施設7 で3エリア、1台の施設で1件が返ることを実測した★★
  • ★★モーダルの行の完了率が一覧の完了率と一致することを実測した★★
  • ★一時ファイルが残っていない(git status が clean)★
  • npx tsc --noEmit = 0 / npm run lint = 0 / npm test 緑
  • ★docs/decisions.md / docs/handoff.md に記録し、push した★

実行状態

段階 内容 状態 コミット
0 前提の確認と実測 ★★完了★★ —
1 d-watch-api.yaml(+ docs/spec.md) ★★完了★★ 3bfed11。★422 は既存の共通レスポンス ValidationError を参照(ValidationFailed は存在しない)★
2 型 + 純粋関数 + 期待値表テスト ★★完了★★ 9edc547。lib/clean-logs/areas.ts 新設 / isProblemGrade() 追加
3 fixtures + docs/fixtures.md ★★完了★★ 49b27cf。lib/fixtures/areas.ts 新設(9ケース)。★既存 fixtures は index.ts の1行追加のみ★
4 DataSource.areaCompletions() ★★完了★★ 53a6d21。★cleanLogs() と同じ作法(latestHistoryByTimetableId() / isFutureSchedule() / jstIsoFromDb())★
5 APIルート(501 を外す) ★★完了★★ 82d98d5。★judge.areas ログのため lib/logger/events.ts に JudgeAreasLogEvent を追加(allowlist 型のため)★
6 画面(AreaModal の配線) ★★完了★★ 828afe6。★filter は state に持ち、変わったら再取得★
7 実測での検証 ★★完了★★ — ★施設7 で3エリア / 一覧との完了率不一致0件(259行)/ 未来0件 / cleanLogs() 259・prechecks() 27 不変★。★★ブラウザでの目視は未実施(監督が別途)★★
8 記録と push ★★記録のみ完了★★ 本コミット。★D-119 / docs/handoff.md 追記39★。★★git push は監督の確認後(本作業では実行していない)★★