Newer
Older
client_dwatch / docs / handoff / 指示書AU_第11回の回答を反映する_20261005.md

指示書AU — 第11回の回答を反映する(task_area = 0 は 0%・当期KPIをサマリの式に・「屋号」を「顧客」に・古いコメント4件)

日付: 2026-10-05 / 担当: Cursor / 基点: develop 74cffd5(指示書AR・AS・AT は実装済み) 前提: D-155 / D-156 / D-157 / ★D-158(本書の根拠)★ / CLAUDE.md §4.2・§4.3・§4.7・§6・§7・§9.4 / TeamWorks 第11回の回答(原文 docs/handoff/TeamWorks回答_第11回_20261005.md。設問は docs/handoff/TeamWorks確認事項_第11回_20261002_送付版.md)

★CLAUDE.md §4.2 は監督が直してある★(CLAUDE.md:159-182)。本書はそれにコードを合わせる作業。 ★期待値表(§3 の表1・表2)は監督が確定した正。変えないこと★(CLAUDE.md §0-2・§6-2・§9.4)。


0. 何をするか

# 回答 やること 段階
1 ★P-5=B★ ★task_area = 0 の清掃の1回ごとの完了率を null から 0.0 にする★(runCompletionRate())。null を前提にした説明・テストを直す 段階2
2 ★P-6=A/A/B★ ★当期(アプリがその場で集計する期間)の稼働率・出発成功率・エラー発生率を、サマリと同じ式にする★ 段階3
3 P-7=C 予定を clean_timetables と clean_timetable_histories から読む ★段階3b★
4 ★P-15=B★ ★画面の見出し「屋号」を「顧客」にする★(「全屋号」→「全顧客」ほか)。★データの説明の「屋号(customers.id)」も「顧客」に(§11 Q2)★ 段階4
5 (D-157)・P-4 ★回答が来たら直すと控えていた古いコメント4件★と、★status 6 の説明3か所(§11 Q4)★を直す 段階5
6 (P-6 の帰結) ★fixtures の作り値で稼働率 > 出発成功率になっている1行★を直す(§11 Q5) 段階3(3-4 の表の下)

★変えないもの★: 確定期間(サマリ *_summaries)の値の読み方(そのまま読む)/完了率の平均の取り方(meanCleanRatio())/ 判定(◎○△×・L1〜L3・アドバイス・要レスキュー)の規則/未実施を departed で数えること(指示書AS 案A。D-156 項1)。


1. 背景 — 回答で何が分かったか

1-1. P-5(task_area = 0 の行)= B「0% として数える」

いまは runCompletionRate() が task_area === 0 で null を返し(lib/judge/kpi.ts:148)、分子にも分母にも入れていない(D-155 の暫定。D-156 項1)。 ★これを 0.0 にする★。帰結:

  • ★completion_rate === null ⟺「レポートの行が無い(未実施)」が、再び両向きに成り立つ★(指示書AS で崩れた不変条件が戻る)
  • ★それでも未実施は departed で数え続ける★(AS 案A を残す。理由は D3)
  • 完了率の件数(cleanRatioCount / clean_ratio_count)は、実施と数えた回数と常に同じ値になる(task_area = 0 も件数に入るため)

1-2. P-6(サマリの3つの率)= A / A / B

指標 ★新しい式(サマリと同じ)★ いまの当期の式
稼働率 ★正常終了(status = 4)の回数 ÷ 予定回数★ レポート行がある回数 ÷ 予定回数(★出発成功率と同じ値になっていた★)
出発成功率 ★開始できた回数(レポート行がある回数)÷ 予定回数★(★時刻の条件なし★) 同じ(「定刻出発」と書いてあるが、実際は時刻を見ていない)
エラー発生率 ★エラー回数 ÷ ★予定回数★★ エラー回数 ÷ 実施回数

★確定期間はサマリの値をそのまま読んでいる★(kpiFromSummaryRatios()。lib/db/report-mapping.ts:231-241)ので、 ★変えるのは当期だけ★。式が揃うので、前期比(サマリ)と当期(アプリ)の差が同じ式どうしの差になる。

1-3. P-15(customers の単位)= B「法人」

customers は法人の単位で確定。★画面の見出し語「屋号」を「顧客」に変える★(D-158 当方の対応3)。 ★DB の物理名(customers / customer_name)・API のキー(customer_id)は変えない★(見出し語だけ)。


2. 決定(Cursor は従うこと)

# 決定 理由
D1 ★runCompletionRate() は task_area === 0 のとき 0 を返す★。★戻り値の型を number(null なし)にする★ P-5=B。null を返す場合がもう無いので、型で示す(呼び出し側の「null なら足さない」分岐が要らなくなったことを tsc が教えてくれるわけではないので、§段階2 の一覧で手で消す)
D2 ★clean_area > 0 かつ task_area = 0 も 0★(Infinity / NaN / 100 にしない) P-5=B は「0% として数える」。割り算をしないで返す
D3 ★未実施は引き続き departed で数える★(ConsecutiveInput.departed / RescueRowInput.departed / launchFailedDaysInRun() / foldRescueDays() は中身を変えない) ★完了率の規則がまた変わっても、未実施の数え方が巻き込まれないため★(AS 案A の理由のうち「規則を2つの意味に兼用しない」はいまも正しい)。監督指示
D4 ★稼働率の分子=「その本清掃の(最新の)レポート行があり、その status が CLEAN_TASK_STATUS.completed」の回数★。★4 を直書きしない★(lib/config/index.ts:348-361 に既にある) P-6=A。CLAUDE.md §7(数値の直書き禁止)の趣旨
D5 ★「正常終了か」を決める述語は1つだけ★: lib/judge/kpi.ts に `isCompletedTask(report: { status: number } \ undefined): booleanを足す(lib/judge/index.tsはexport *` なので自動で出る) 規則を2箇所に書かない(D-103 項1 と同じ考え方)。判定層は lib/db/ を import しないので構造的な型にする(runCompletionRate() と同じ作法)
D6 ★CleanStatsBucket に completed: number(正常終了の回数)を足す★。★既存の executed は名前を変えず、意味を「開始できた回数(レポート行がある回数)=出発成功率の分子」と書き直す★ 名前を変えると機械的な差分がレビュー(daichi。D-154)を読みにくくする(指示書AS D7 と同じ判断)。executed を数える規則(isNotExecuted 述語)はいまのまま正しい
D7 ★CleanStatsReportLike に status: number を足す★ 当期の2経路(reportKpis() / facilityPerformance())は cleanTaskReport.findMany を ★select なし(全列)★で引いている(lib/db/index.ts:2085 / :2352)ので、status は既に載っている。★select を足さないこと★(足すなら status を落とさない)
D8 lib/judge/kpi.ts の3関数は名前を変えず、引数名と docstring を変える: operationRate(completed, planned) / launchRate(started, planned) / ★errorRate(errors, planned)★ 中身はどれも rate(分子, 分母) のまま。変わるのは呼び出し側(kpiFromCleanStats())が渡すもの
D9 ★エラー発生率は100%で切らない★(エラー件数が予定回数より多ければ100を超える) サマリの式(error_count ÷ task_count)に上限は無い。1回の清掃に複数のエラーが紐づきうる
D10 ★未来の予定(isFutureSchedule())は、予定・開始・正常終了・エラーのどれにも足さない★(いまと同じ) 指示書V 不具合A・決定1。変えない
D11 ★確定期間(サマリ)の値は1つも変えない★。kpiFromSummaryRatios() / SummaryRatios / periodOfSummaryRow() に触らない D-158。サマリは既に新しい式
D12 ★画面の見出し語は lib/i18n.ts のキー(日本語原文)で持つ★。キーを 屋号 → 顧客 に改名し、★すべての t("屋号…") 呼び出しを新しいキーに直す★(旧キーは残さない) 旧キーを残すと、片方だけ直した画面が日本語で「屋号」のまま出ても気づけない。重複キーは lint が落とす
D13 ★英語は仮★: 「顧客」"Customer" / 「全顧客」"All Customers"(★大文字は既存の "All Holdings" にそろえる。§11 Q7★)ほか(段階4 の表)。★英語は後でユーザーがまとめて見直す★とコメントに書く 監督指示(暫定)

3. ★期待値表(正)★

★表1・表2 は監督が確定した正である。Cursor は値を変えないこと★(CLAUDE.md §0-2・§6-2・§9.4)。 ★段階1 で、実装より先にこの表をテストにする★。表とテストが食い違ったら、テストを表に合わせる。実装の都合で表を直さない。

表1(runCompletionRate の変更分。AS 表1 の R9 を差し替え、R12 を追加。他の R1〜R11 はそのまま)★正・変えない★

# clean_area task_area 期待値 確かめること
R9 10 0 0.0 対象面積0は 0%(P-5=B)。旧: null
R12 0 0 0.0 両方0も 0%

表2(当期 KPI。予定・開始・正常終了・エラーは本清掃・予定 ID ありの行だけで数える。生値で持ち、表示は四捨五入)★正・変えない★

# 予定 レポート行あり(開始) うち status=4 エラー件数 稼働率 出発成功率 エラー発生率
K1 10 9 7 2 70.0 90.0 20.0
K2 0 0 0 0 null null null
K3 5 0 0 0 0.0 0.0 0.0
K4 4 4 4 0 100.0 100.0 0.0
K5 3 3 2(残り1行は status=5) 3 66.666… 100.0 100.0
K6 2 2 1(残り1行は status=6) 0 50.0 100.0 0.0
K7 2 + 予定外1行(status=4・予定 ID NULL) 2 2 0 100.0 100.0 0.0(予定外は数えない)
K8 1 1 1(status=4・clean_area=0) 0 ★100.0★ 100.0 0.0
  • ★K8: 正常終了は status で数える。面積は見ない(P-3=A は完了率0%の話)★。この清掃の完了率は 0.0(×)だが、稼働率には正常終了1回として入る。
  • ★エラー発生率は上限で切らない(D9)★。K5 はエラー件数が予定回数と同じなのでちょうど 100.0。★予定回数より多ければ100を超える★(例: 予定3・エラー4 → 133.33…)。100 で止めないこと。
  • ★K2 は「予定が1件も無い」★ので3つとも null(0% にしない。lib/judge/kpi.ts:22-23)。★K3 は「予定はあったが全部出発失敗」★なので 0.0。★エラー発生率も 0.0★(旧式では実施0回で null だった)。
  • ★K6 の status=6★ は Pudu の値で意味は先方も不明(P-4)。★4 以外なので正常終了に数えない★。

テストの書き方の注意(表2):

  • ★aggregateCleanStatsByFacility() → sumCleanStatsBuckets() → kpiFromCleanStats() を通して★ ReportKpi の value を確かめる(lib/db/__tests__/report-mapping.test.ts に新しい describe)。自前で割り算を書かない。
  • 予定はすべて期間内・過去(now より前)にする。1施設でよい。前期(prev)は undefined。
  • 「開始」した予定にだけレポート行を入れる。★「うち status=4」以外の行の status は表に書いた値(K5=5 / K6=6)、K1 の残り2行は 5 と 2 にする★(4 以外なら何でもよいが、テストの中で固定する)。面積は 100/100(K8 だけ 0/100)。
  • エラー件数は errorCount の Map(本清掃 id → 件数)で渡す。K1 は2件を別々の予定に、K5 は3件を1つの予定に寄せてよい。
  • ★K7 は latestReportByTimetableId() を通して★レポートの Map を作る(予定外=clean_timetable_id: null の行が Map に入らないことまで含めて確かめる)。予定 ID が NULL のエラー行も errorCountByMainTimetable() を通して落ちることを1件足してよい。
  • 生値の比較は toBe。★K5 の稼働率だけ toBeCloseTo(200 / 3, 10)★。
  • ★追加で1件★: K1〜K8 のすべてで 稼働率 ≤ 出発成功率(正常終了は開始の部分集合)を確かめる。

表3(本書で表1 から導いたもの。★監督が正として確定(2026-10-05)★)

AS の期待値表のうち、task_area = 0 に当たる行だけが変わる。★他の行は変えない★。

元の表 # 入力 旧の期待値 ★新の期待値★
AS 表4(F-08) F5 本清掃 10/0 の1行だけ executed_count 1 / completion_rate null executed_count 1 / ★completion_rate 0.0★
AS 表5(未実施の数え方) U2 行がある・task_area = 0 departed true / completion_rate null / R-1 数えない / 出発失敗日 数えない / 問題日 departed true / ★completion_rate 0.0★ / R-1 数えない / 出発失敗日 数えない / 問題日(★0.0 は ×★)
AS 段階3-2(当期KPI の完了率) w11 task_area = 0 の行 実施1・完了率の件数0 実施1・★件数1・Σ に 0 を足す★
詳細ログの1行 — 行がある・task_area = 0 完了率 -・判定 × ★完了率 0%・判定 ×★(displayRate(0))

4. ★落とし穴(全段階で読むこと)★

# 罠 どう避けるか
4-1 ★サマリの値まで計算し直す★ ★確定期間はそのまま読む★(D11)。kpiFromSummaryRatios()(lib/db/report-mapping.ts:231-241)・periodOfSummaryRow() に触らない。★route.test.ts の確定期間のテスト(app/api/reports/__tests__/route.test.ts:563-600 / :1020-1229)の期待値は1つも変わらないのが正しい★
4-2 ★status の 4 を直書きする★ CLEAN_TASK_STATUS.completed(lib/config/index.ts:358)を読む。`grep -rnE "status ?===? ?4([^0-9] $)" lib app componentsが0件になること(★status === 422(lib/reports/block-visibility.ts:160`)に当たらない形で探す★)
4-3 ★正常終了の判定を2箇所に書く★ isCompletedTask()(D5)だけ。report-mapping.ts の中で report.status === … と書かない
4-4 ★cleanTaskReport.findMany に select を足して status を落とす★ いまは select なし(D7)。★足さない★。テストのスタブ行にも status を必ず入れる
4-5 ★正常終了の判定に面積を混ぜる★(「clean_area = 0 なら正常終了でない」) ★status だけで数える★(表2 K8)。面積は完了率の話
4-6 ★エラー発生率を100で切る★ 切らない(D9)
4-7 ★未実施の数え方を completion_rate === null に戻す★ ★departed のまま★(D3)。P-5 で null ⟺ 未実施が戻っても、launchFailedDaysInRun()(report-mapping.ts:648)・foldRescueDays()(lib/judge/rescue.ts:168)は変えない
4-8 ★runCompletionRate() で割り算を先にしてから 0 を判定する★ task_area === 0 を ★割る前に★ 見て 0 を返す(10 / 0 = Infinity → Math.min(Infinity, 100) = 100 になる)
4-9 ★生SQL($queryRaw / $executeRaw)★ ★禁止★(CLAUDE.md §3。eslint が止める)。本書の変更はすべて JS の中で済む
4-10 ★閾値・コード値の直書き★ 90/70/10 は GRADE_THRESHOLDS、status は CLEAN_TASK_STATUS、上限100と桁数は RUN_COMPLETION_RATE
4-11 ★画面の文字列を i18n に通さない★ JP/EN は必ず t()(lib/i18n.ts)。★新しい見出し語をコンポーネントに直書きしない★
4-12 ★JP/EN 切替でレイアウトが崩れる★ ★日本語は「屋号」「顧客」とも2文字、「全屋号」「全顧客」とも3文字で幅は変わらない★。英語は "Brand" → "Customer" で少し長くなる。セレクトは幅指定なし(内容の長さで決まる。例 components/features/alerts/AlertsScreen.module.css:58-65)で、ツールバーは flex-wrap: wrap。★幅を固定する CSS を新たに足さない★。段階6 の画面確認で EN の折り返しを見て、崩れていたら直さずに報告する
4-13 ★顧客 の i18n キーが既存と重なる★ 2026-10-05 時点で 顧客 / 全顧客 / 顧客を選ぶ / 選べる顧客が{n}件あります / この条件では選べる顧客がありません は未登録(既存は 顧客を選んでください(lib/i18n.ts:357)だけ)。重複は lint が落とす
4-14 ★「屋号」を一括置換する★ ★しない★。§7 の分類どおり、画面に出る文字列・それを直接説明するコメント・★データの説明(段階4 の 4-3 に列挙した行。§11 Q2)★だけを直す。経緯・処理の理由のコメントは残す
4-15 ★git add -A★ ★禁止★(CLAUDE.md §7)。ファイルパスを明示する

段階0 — 着手前

  1. git status が clean で、develop が 74cffd5 以降であること(.claude/worktrees/ ではなく本体で作業する)。
  2. 行番号は 2026-10-05 の 74cffd5 で確かめたもの。★段階3b(P-7)が先に当たるとずれる★ので、記述(関数名・文)で探すこと。
  3. npx tsc --noEmit -p . と npm test が通る状態から始める(通らなければ着手前に報告)。
  4. ★robotErrorReport の読み取りを select で必要な列だけにする(§11 Q6)★。先方の DDL では robot_id が NULL 可になった(第11回 P-8 b)が、Prisma のスキーマは NOT NULL のまま。★robot_id と robot リレーションを読まなければ、NULL の行が入っても読み取りで落ちない★。grep -rn "robotErrorReport\." app lib で全部の読み取りを洗い、select を付ける(書き込み lib/db/robot-error-write.ts は対象外)。★prisma/schema.prisma は変えない★(db pull は監督が行う)。テスト: スタブに robot_id: null の行を混ぜても落ちないこと

段階1 — 期待値表をテストにする(実装より先)

  1. lib/judge/__tests__/run-completion-rate.test.ts:
    • 冒頭コメントの表1(:13-27)の R9 を表1 の行に差し替え、R12 を足す。:8 の「★暫定★」と :11 の「task_area = 0 は null(分子にも分母にも入れない)」を「task_area = 0 は 0(P-5=B。第11回)」に直す。★「本書 §3 が正。変えないこと」と出典に本書を足す★
    • it.each(:74-86)の R9 を ["R9 対象面積0は 0%(P-5=B)", 10, 0, 0.0] に、R12 を足す
    • 表2 の M7・M8 は null を meanCleanRatio() に渡すテストなので ★そのまま★(null は「未実施=レポート行が無い」の意味で残る)
  2. lib/judge/__tests__/kpi.test.ts:
    • 冒頭の式(:13-22)を CLAUDE.md §4.2 の新しい式に直す
    • operationRate(:24-35): 引数の意味を「正常終了の回数 ÷ 予定回数」に。値のテストは変えなくてよい
    • launchRate(:37-46): :38-39 の「定刻出発85件」の例を「開始できた回数 ÷ 予定回数(時刻の条件なし)」に書き直す
    • ★errorRate(:100-109)を逆の仕様に直す★: 「分母は清掃予定回数」errorRate(9, 100) === 9/予定0は null/★上限で切らない★ errorRate(4, 3) が toBeCloseTo(400 / 3, 10)
    • ★isCompletedTask() のテストを足す★: undefined → false/status 4 → true/0・1・2・3・5・6・255 → false/★期待値の 4 は CLEAN_TASK_STATUS.completed から読む★
  3. lib/db/__tests__/report-mapping.test.ts に ★表2 の K1〜K8 と「稼働率 ≤ 出発成功率」★を新しい describe で書く(§3 の注意どおり)
  4. ★この時点ではテストが落ちる(isCompletedTask が無い・completed が無い・R9 が null を返す)のが正しい★。コミットはテストだけで1つ

段階2 — task_area = 0 を 0% にする(P-5)

2-1. 本体

lib/judge/kpi.ts:144-153 の runCompletionRate():

  • if (report.task_area === 0) return 0;(★割る前に★。4-8)
  • 戻り値の型を number にする(D1)
  • docstring(:112-143)を直す:
    • :120-123「★★暫定(D-155。ユーザー判断「一旦B」)★★ — 第11回で…見直す」→ ★確定★(第11回 P-2=A「今の式のまま」・P-1「訂正後の clean_ratio 列もこの式と同じ値」。D-158)。「clean_ratio 列に戻すときはこの関数の中身だけを直す」「呼び出し側は行をそのまま渡すこと(D6)」は ★残す★(1関数に閉じ込める作法として今も正しい)
    • :125-126「task_area === 0 は null(…暫定・第11回で確認)」→ ★0(P-5=B。第11回)★。★clean_area > 0 でも 0★(D2)。旧は null だったことを1行
    • :142 の @returns から「task_area === 0 なら null」を消す
  • 冒頭(:4-24)の :12 の「(暫定)」を「(第11回で確定。D-158)」に

lib/config/index.ts:27-36 の RUN_COMPLETION_RATE の docstring: :34-35 の「暫定…第11回で…見直す」を ★確定(第11回 P-2=A。D-158)★ に。★値(cap: 100 / decimals: 1)は変えない★。

2-2. 「null なら足さない」分岐を消す(★runCompletionRate() の戻りを直接受けている所だけ★)

場所 いま 直し方
lib/db/report-mapping.ts:488-501 runRate ?? 0 / runRate === null ? 0 : 1 runRate は「実施と数えた行 かつ 結果行がある」ときだけ数値、それ以外 null のまま(:492)。★:500-501 はそのままでよい★(null になるのは結果行が無いときだけ)。コメント :488-491 の「task_area = 0 なら null で…件数には入れない」を消す
lib/db/clean-area-source.ts(cleanAreaStatsFromRows() の中。:290 以降) const rate = runCompletionRate(report)。null でなければ足す ★常に足す★(clean_ratio_sum += rate / clean_ratio_count += 1)。docstring :251-253 の「task_area = 0 の清掃も実施1回に数える(表4 F5)」「null でなければ」を表3 の F5 に合わせる
lib/db/fixture-clean-area.ts:76-99 rate ?? 0 / rate === null ? 0 : 1 / if (rate !== null) ★常に足す★。コメント :30-31 / :84-85 の「null(task_area = 0)なら Σ にも件数にも足さない」を消す
lib/db/clean-log-mapping.ts:203-210(completionRateFromReport()) 中身はそのまま(結果行が無ければ null、あれば runCompletionRate()) ★中身は変えない★。docstring を 2-3 のとおり直す

2-3. 説明を直す(★動作は変わらない。null の意味が「未実施だけ」に戻ったことを書く★)

ファイル:行 いま 直し方
lib/judge/kpi.ts:59-62(CleanRatioTally.cleanRatioCount) 「未実施の清掃と task_area = 0 の清掃は数えない」 「★未実施の清掃は数えない★(task_area = 0 は 0% として数える。P-5=B)」
lib/db/clean-log-mapping.ts:171-199(completionRateFromReport()) 表の2行目「行があり task_area = 0 → null」/不変条件「逆は成り立たない」(:192-199) 表を「行が無い → null/それ以外 → runCompletionRate()(task_area = 0 は 0)」の2行に。★不変条件を「departed === false ⟺ completion_rate === null(P-5=B で両向きに戻った)」★に直し、★「それでも未実施は departed で数える(D3。完了率の規則が変わっても未実施の数え方を巻き込まないため)」★と書く。:180-185 の経緯は残し、D-158(確定)を1行足す
lib/db/clean-log-mapping.ts:94(CleanTaskReportLike) 「(D-155。暫定)」 「(D-155。第11回で確定。D-158)」
lib/db/clean-log-mapping.ts:350-363(ConsecutiveInput) :352「未実施と task_area = 0 の清掃は null」/:358-361「task_area = 0 の清掃は completion_rate === null だが出発はしている」 :352 は「未実施は null」。:358-361 は「未実施は departed で数える(D3)」の理由を、task_area = 0 の例ではなく D3 の理由に差し替える
lib/db/consecutive-source.ts:35 / :254-255 「(D-155。暫定)」/「task_area = 0 の清掃は…」 確定に/D3 の理由に
lib/db/report-mapping.ts:293-298(cleanRatioCount)/ :433-440(aggregateCleanStatsByFacility())/ :637-640(launchFailedDaysInRun()) 「task_area = 0 の清掃は実施には数えるが、ここには数えない」/「逆は成り立たない」 「★いまは executed と同じ値になる★(task_area = 0 も 0% として数えるため。P-5=B)。★それでも別に持つ★(Σ と件数の組で meanCleanRatio() に渡す形を変えないため)」/不変条件を両向きに、departed で数える理由は D3
lib/judge/rescue.ts:20-22 / :75-79 / :93-96 / :166-169 「task_area = 0 の清掃は completion_rate が null だが出発はしている」 D3 の理由に差し替える。★:168(!row.departed)と :170-171(null を数えない)は中身を変えない★
lib/db/index.ts:977-980 「逆は成り立たない — task_area = 0 は出発しているが null」 両向きに成り立つ(P-5=B)。★report ? report.clean_ratio : null の形に戻さないこと★(:977-978)は残す
lib/types/index.ts:248-255(CleanLogItem.completion_rate)/ :347 / :1272-1277(CleanAreaFacilityStats.clean_ratio_count) 「行があっても task_area = 0 なら null」/「executed_count を流用せず別に持つ — task_area = 0 は…入らない」 「行があって task_area = 0 なら 0(P-5=B)」/「いまは executed_count と同じ値。形を変えないために別に持つ」。:250 の「★暫定★」を確定に
components/features/detail-log/AreaModal.tsx:42-43 「未実施・task_area = 0=完了率が null の行は…」 「未実施=完了率が null の行は…」
d-watch-api.yaml :340 / :563 / :567 / :598-599 / :918 / :1743 / :1746 / :1796-1797 / :2056 「task_area = 0 は除く」「task_area = 0 のときも null」「D-155(暫定)」 「task_area = 0 は 0」「(D-155。第11回 P-2・P-5 で確定。D-158)」。★:1746「★task_area = 0 のときも null★(このとき departed は true)」と :1797「task_area = 0 も null」は消す★

★最後に次の grep を流し、残ったものが「経緯(旧: …)」の説明だけになっていること★:

grep -rn "task_area = 0\|task_area === 0\|task_area=0\|task_area が 0" app lib components d-watch-api.yaml docs/fixtures.md --include=*.ts --include=*.tsx --include=*.yaml --include=*.md | grep -v "lib/generated" | grep -v __tests__

2-4. テストを直す(★面積から完了率を作っているテストだけ期待値が変わる★)

ファイル:行 直し方
lib/db/__tests__/clean-log-mapping.test.ts:319 / :374-378 / :382-402 / :420-421 表3 の「詳細ログの1行」: { clean_area: 10, task_area: 0 } → completion_rate は 0。:420 の「逆は成り立たない」のテストは ★「両向きに成り立つ」★に書き換える
lib/db/__tests__/clean-log-mapping.test.ts:583-587(isProblemCompletion() の U2) 入力が面積から来ているなら 0 → 問題(×)。期待値(問題)は変わらない。ラベルを直す
lib/db/__tests__/consecutive-source.test.ts:80(Q9)/ :262-266(テスト内の再実装) / :655-697 ★:265 のテスト内の r.task_area === 0 ? null を消し、runCompletionRate() を呼ぶ形にする★(テストの中に規則を写さない)。Q9 の「task_area = 0 は null」→ 0
lib/db/__tests__/report-mapping.test.ts:318 / :689-700(w11) 表3 の w11(実施1・件数1・Σ に 0)
lib/db/__tests__/clean-area-source.test.ts:226 / :252(F5) 表3 の F5(completion_rate 0.0)
lib/db/__tests__/fixture-clean-area.test.ts:145 「実施回数に入り、完了率の件数にも入る(0% として)」に
★completion_rate: null, departed: true を直接渡しているテスト★ — lib/judge/__tests__/rescue.test.ts:27 / :227 / :241、lib/db/__tests__/report-mapping.test.ts:91-93 / :1101 / :1119、lib/db/__tests__/rescue-mapping.test.ts:41 ★入力の組み合わせは DB からは来なくなったが、テストは残す★(「未実施は departed で数える」ことの確認。D3)。ラベルの「task_area = 0」を「departed と completion_rate が食い違う行(防御)」に直すだけ。期待値は変えない
lib/clean-area/__tests__/aggregate.test.ts:19 / :183(A-4) 渡された Σ と件数から出すテストなので中身はそのまま。説明だけ(「clean_ratio から出る」→「渡された Σ と件数から出る」)

段階3 — 当期の KPI をサマリと同じ式にする(P-6)

3-1. 判定層(lib/judge/kpi.ts)

  1. 冒頭(:4-24)の式を CLAUDE.md §4.2(CLAUDE.md:161-166)と同じ4行に直す。「★3つの率はサマリと同じ式(第11回 P-6)★」と、旧の式(実施回数 ÷ 予定・定刻出発 ÷ 予定・エラー ÷ 実施)を1行で残す
  2. operationRate()(:32-35): 引数を (completed, planned) に。docstring「稼働率 = 正常終了(status = 4)の回数 ÷ 清掃予定回数(P-6=A)」
  3. launchRate()(:37-40): 引数を (started, planned) に。docstring「出発成功率 = 開始できた回数(レポート行がある回数)÷ 清掃予定回数。★時刻の条件なし(P-6=A)★」
  4. errorRate()(:155-158): 引数を (errors, planned) に。docstring「エラー発生率 = エラー発生回数 ÷ ★清掃予定回数★(P-6=B)。★上限で切らない(D9)★。旧: 分母は実施回数」
  5. ★isCompletedTask() を足す★(D5):
    import { CLEAN_TASK_STATUS, FACILITY_STATUS_THRESHOLDS, RUN_COMPLETION_RATE } from "@/lib/config";
    /** その清掃が正常終了したか(稼働率の分子。P-6=A)。★status だけで決める。面積は見ない★ */
    export function isCompletedTask(report: { status: number } | undefined): boolean {
      return report !== undefined && report.status === CLEAN_TASK_STATUS.completed;
    }
    docstring に: 出所(第11回 P-6=A・送付版 :87)/★「最新の1行」で見る★(呼び出し側が latestReportByTimetableId() で選んだ行を渡す)/★◎○△× の判定には使わない★(D-155 項2 は変わらない)/★面積は見ない(表2 K8)★
  6. judgeFacilityStatus()(:178-203)は ★中身を変えない★。ただし docstring に「★当期の稼働率は P-6 で『正常終了 ÷ 予定』になった(以前は出発成功率と同じ値)。当期の施設ステータスは以前より低めに出る★」と1行足す

3-2. 集計(lib/db/report-mapping.ts)

  1. CleanStatsBucket(:245-299):
    • ★冒頭の docstring :245-256 を書き直す★(D-157 の :249 もここで直る。段階5 を参照)。新しい中身:
      • 当期の3つの率はサマリと同じ式(第11回 P-6。D-158)。どれも ★分母は planned★
      • planned = 期間内・過去の本清掃の予定の数/executed = そのうちレポート行がある数(★開始できた回数=出発成功率の分子★)/★completed = そのうち最新のレポート行の status が正常終了の数(稼働率の分子)★/errorCount = エラーの数(エラー発生率の分子)
      • ★時刻の条件は無い★(P-6=A)。clean_task_reports.started_at は、予定のある清掃では予定の start_at と同じ値が入る(ユーザー回答 2026-10-05。D-155 項5 の N-9)ので、★定刻かどうかはそもそも区別できない★
      • ★不変条件: completed ≤ executed ≤ planned★(だから 稼働率 ≤ 出発成功率 ≤ 100)
    • executed(:260)の docstring「清掃実施回数(=定刻出発回数。上記コメント参照)」→「開始できた回数(レポート行がある回数)。出発成功率の分子」
    • ★completed: number を足す★(executed の直後)
    • errorCount(:266-282)の docstring: :278-280 は段階5 の文に差し替える。★分母は予定回数★になったことを書く
  2. emptyCleanStatsBucket()(:301-311)・addCleanStats()(:313-323)に completed を足す
  3. CleanStatsReportLike(:394-416)に ★status: number★ を足す(D7)。docstring に「★status は稼働率の分子(isCompletedTask())にだけ使う★」
  4. aggregateCleanStatsByFacility()(:460-505):
    • const completed = executed && isCompletedTask(report);(isCompletedTask は @/lib/judge から import。:16-24 の import に足す)
    • completed: bucket.completed + (completed ? 1 : 0),
    • ★未来の予定は completed にも足さない★(:480-483 の continue の前に何も足さない。D10)
    • docstring(:418-459)の :422-424「稼働率・出発成功率が実際より下振れする」は ★エラー発生率も★ に
  5. kpiFromCleanStats()(:507-535):
    operation_rate: toKpiValue(operationRate(bucket.completed, bucket.planned), prev?.operation_ratio ?? null),
    launch_rate:    toKpiValue(launchRate(bucket.executed, bucket.planned),     prev?.launch_ratio ?? null),
    completion_rate: toKpiValue(meanCleanRatio(bucket), prev?.clean_ratio ?? null),   // ★変えない★
    error_rate:     toKpiValue(errorRate(bucket.errorCount, bucket.planned),    prev?.error_ratio ?? null),
    docstring :510-511「★エラー発生率の分母は「実施回数」のまま★(予定回数にしない)」は ★逆の仕様★。「★3つとも分母は予定回数(第11回 P-6。サマリと同じ)★」に
  6. countMainErrorTypesInRange() の docstring :946-947「エラー率の分母は「清掃実施回数」…なので、分子も清掃を基準にする」→「エラー率の分母は清掃予定回数(P-6=B)なので、分子も予定(本清掃)を基準にする」。★中身は変えない★(期間の帰属は本清掃の start_at のまま)

3-3. データ層(lib/db/index.ts)

  • reportKpis()(:2042-2140)・facilityPerformance()(:2330-2400)は ★コードを変えない★(findMany に select が無いので status は載っている。D7)。aggregateCleanStatsByFacility() の引数も変わらない
  • errorBreakdown() の docstring :2493「エラー率の分母が「清掃実施回数」なので」→「清掃予定回数(P-6=B)なので」

3-4. 説明を直す

ファイル:行 直し方
lib/config/index.ts:337-347(CLEAN_TASK_STATUS) :343-344「★★判定には使わない★★」を「★◎○△× の判定・未実施の判定には使わない。稼働率の分子(正常終了の回数。isCompletedTask())にだけ使う(第11回 P-6=A)★」に。:368-369(6 は「第11回で確認」)は ★段階5 の C5 で直す★(§11 Q4)
lib/reports/target-customer.ts:10-17 の表 出発成功率: 分子 launch_count/分母 task_count(★P-6=A で確定★)/合算「材料はある」。稼働率: 分子 ★列が無い(正常終了の回数)★/分母 task_count/★できない★。エラー発生率: 分子 error_count/分母 ★task_count(P-6=B)★/合算「材料はある」。★表の下に「4指標を揃えて出せない(稼働率・清掃完了率の材料が無い)ので、合算しない方針は変えない」と書く★(★方針は変えない★。§11 Q3)
d-watch-api.yaml:740-741(GET /reports/kpi) 「稼働率の分子(清掃実施回数)とエラー率の分母に当たる列がサマリに無く」→「稼働率の分子(正常終了の回数)と清掃完了率の分子・分母(1回ごとの完了率の合計・件数)に当たる列がサマリに無く」
d-watch-api.yaml:1896-1903(ReportKpi)/ :1905-1907(FacilityPerformance) description に4つの式(CLAUDE.md §4.2)と「確定期間はサマリの値そのまま/当期はアプリが同じ式で集計(第11回 P-6。D-158)」を書く。★$ref の隣に description を書いても無視される★(OpenAPI 3.0)ので、★スキーマ本体の description に書く★
d-watch-api.yaml:1924-1926(status) 「当期の稼働率は正常終了 ÷ 予定」を1行
lib/types/index.ts:713-718(ReportKpi) 各フィールドに1行の docstring(4つの式)。型は変えない
scripts/seed-local-db.ts:1838-1848 / :1875-1876 ★サマリのシード(作り値)の辻褄★: error_count = round(task_count * error_ratio / 100)(分母=予定)に直し、コメント :1844 を「分母=予定回数(第11回 P-6=B)」に。:1843 の executedCount は「稼働率の分子=正常終了の回数」と読み替えるコメントに。★シードの接続先・ガード(lib/db/local-guard.ts)には触らない★。★シードを流すのは任意★(流すなら dwatch_local だけ。ステージングには絶対に向けない。CLAUDE.md §3)
app/api/reports/__tests__/route.test.ts:634-635 当期のエラー発生率: 分子1・★分母は予定2★ → ★50★(旧 100)。:626-628(稼働率・出発成功率 50)は、スタブのレポート行が status: 4(:330)なのでそのまま 50 のはず。★落ちたら status を確かめる★。★スタブのコメント :327-329「意味を持たない任意の生値…集計・未実施の判定には使われない」は逆になった★ので、「★稼働率の分子(正常終了)に使われる★。4 以外にすると当期の稼働率が変わる」に直す
lib/db/__tests__/report-mapping.test.ts の既存テスト toEqual<CleanStatsBucket>({...}) に ★completed を足す★(:364-383 / :416-424 ほか。tsc が全部教える)。CleanStatsReportLike のリテラル行に ★status: CLEAN_TASK_STATUS.completed★ を足す(status を見るテスト以外)。:755-800 の kpiFromCleanStats: ★:790-795 の「executed=0 のときエラー発生率も null」は逆の仕様★ → planned>0 なら 0.0、planned=0 なら null
lib/judge/__tests__/kpi.test.ts 段階1 で済み

★lib/fixtures/reports.ts の KPI(:159-373)は作り値で、当期も「顧客単位の集計値」として置いてある★(lib/db/fixture-reports.ts:68-69)ので ★式を変えても画面の数字は変わらない★。 ★値は1か所だけ直す(§11 Q5)★: 新しい式では ★稼働率 ≤ 出発成功率★ が必ず成り立つが、:256-257(施設6 バリューマート南丘駅前店)だけが 稼働率 72.4(前期 70.1)> 出発成功率 65.0(前期 68.0)になっている(2026-10-05 監督が全行を走査。他の行は成り立っている)。

  • ★出発成功率の側を上げる★: launch_rate: toKpiValue(65.0, 68.0) → ★toKpiValue(75.0, 71.0)★。★稼働率(72.4 / 70.1)・status・並び順は変えない★
  • ★稼働率の側を下げないこと★(§11 Q5 の当初の例「72.4 → 60.5」は採らない): 施設別成績は稼働率の昇順に並べてあり、 lib/fixtures/__tests__/reports.test.ts:212(「稼働率の昇順(ワースト順)に並んでいる」)が守っている。 60.5 にすると直前の施設9(68.0。:245)より小さくなり、このテストが落ちる。並べ替えると画面のワースト1位が変わる
  • ★lib/fixtures/__tests__/reports.test.ts にテストを1件足す★: 施設別成績の全行と KPI の全期間で、operation_rate と launch_rate が ともに数値なら value も prev も ★稼働率 ≤ 出発成功率★(作り値が新しい式の不変条件を破らないことを固定する)
  • ★65.0 / 68.0 を期待値に持つテストは無い★(2026-10-05 監督が grep で確認)。落ちるテストが出たら値を確かめてから報告する

段階3b — 予定を clean_timetables と clean_timetable_histories の両方から読む(P-7。D-158 項4)

★予定(本清掃・事前チェックの行)は2つの表を合わせて読む★。★同じ id は1件として扱い、clean_timetables 側を採る★。 ★実績は clean_task_reports だけのまま★(D-151。clean_statuses / clean_status_histories は読まない)。 ★期間の絞り込みは今と同じく start_at★(end_at で絞る経路は今も無い。足さない)。

3b-0. 2つのモデルの違い(prisma/schema.prisma:587-619 / :621-650)

列 CleanTimetable CleanTimetableHistory アプリの読み
id BigInt UNSIGNED・★AUTO_INCREMENT★ BigInt UNSIGNED・★AUTO_INCREMENT 無し★(:622-624) 読む(突き合わせのキー)
facility_id / section_id / clean_schedule_id / parent_id 同じ型・NOT NULL 同じ 読む
start_at / end_at DateTime(0) NOT NULL 同じ(索引名だけ started_at/ended_at。:642-648) start_at を読む
precheck UnsignedTinyInt・既定 0 同じ 読む
inactive UnsignedTinyInt ★無い★ ★読んでいない★(どの経路も inactive で絞らない。lib/db/index.ts:726 / :1193)
created_at / updated_at Timestamp / ★updated_at あり★ created_at のみ・★updated_at 無い★ どちらも読んでいない
リレーション clean_schedule / facility / section ★同じ3つ★ include: { facility, section } はどちらにも書ける

★アプリが使う列は全部、履歴側にもある★。★where に inactive を書くと履歴側で型エラーになる★(それでよい。書かないこと)。 ★他のモデルから clean_timetables へのリレーションは無い★(clean_task_reports / robot_error_reports / clean_statuses の clean_timetable_id は FK 無し)ので、include 経由の読みは0件。

3b-1. 読んでいる箇所(★全12本。これ以外に無いことを grep -rn "cleanTimetable\." app lib で確かめる★)

# 場所 何で絞るか 何に使うか
1 lib/db/index.ts:765 cleanLogs() 本清掃・start_at 範囲・id(単一)・facility_id IN。include: facility, section.robots.device。orderBy あり 詳細ログ F-02・ダッシュボード F-01・アラート F-03・/clean-logs/{id}/advice・/{id}/areas
2 lib/db/index.ts:831 cleanLogs() parent_id IN(塊ごと)。select: id, parent_id, precheck 1 の事前チェック行(status_1h / status_30m)
3 lib/db/index.ts:1104 areaCompletions() 本清掃・facility_id・1日ぶんの start_at。include: section エリア別完了率モーダル
4 lib/db/index.ts:1230 prechecks() 本清掃・start_at 範囲・facility_id IN。include: facility, section.robots 事前チェックアラート・出発失敗
5 lib/db/index.ts:1271 prechecks() parent_id IN(塊ごと)。select 3列 4 の事前チェック行
6 lib/db/index.ts:1448 precheckItemById() findUnique({ id })。select 3列 PATCH /prechecks/{id}/status(指示書AT)
7 lib/db/index.ts:1457 precheckItemById() findFirst({ id: parent_id, precheck: 本清掃 })。select: start_at 6 の親(scheduled_at)
8 lib/db/index.ts:2067 reportKpis() 当期 本清掃・start_at 範囲・facility_id IN。select 3列 レポート KPI(当週・当月・当年)
9 lib/db/index.ts:2333 facilityPerformance() 当期 8 と同じ 施設別成績
10 lib/db/index.ts:2552 errorBreakdown() 当期 8 と同じ エラー内訳
11 lib/db/consecutive-source.ts:356 loadConsecutiveInputs() 本清掃・start_at(遡りぶんを足した範囲。無制限もありうる)。select 4列 連続日数・L1〜L3(cleanLogs :1001 / rescueRequired :1834 / facilityPerformance :2236 / continuousProblems :2457)・要レスキュー
12 lib/db/clean-area-source.ts:399 loadCleanAreaFacilityStatsWithRowCount() id IN(レポートの clean_timetable_id)+本清掃。select: id 総清掃面積 F-08 の「本清掃に紐づく行か」(指示書AS 段階4)

★count / groupBy / skip / take で clean_timetables を読む箇所は0件★(take/skip は P1 では SQL に降ろさない決定。lib/db/index.ts:743-745)。 ★/schedules は clean_schedules だけを読む★(lib/db/index.ts:1759-1797)。予定表(timetable)は読まない → ★本段階の対象外★。

★12 は今のままだと静かに壊れる★: レポートの clean_timetable_id が履歴にしか無いと「本清掃に紐づかない行」になり、★完了率・実施回数から黙って落ちる★(面積だけ残る)。

3b-2. 共通の読み口を1つ作る(lib/db/timetable-source.ts。新規)

  1. ★純粋関数★ mergeTimetableRows<T extends { id: bigint }>(mainRows: T[], historyRows: T[], shadowedIds: ReadonlySet<string>): T[]
    • mainRows を全部採る。historyRows は ★id.toString() が mainRows にも shadowedIds にも無いものだけ★足す
    • ★historyRows の中で同じ id が2回出たら例外★(履歴の主キーは id なので起きない。起きたら呼び出しの誤り)
    • ★並び順は保証しない★(下の 3 を参照)
  2. findTimetables(prisma, args) — ★同じ args を cleanTimetable.findMany と cleanTimetableHistory.findMany に Promise.all で渡す★
    • args の型は ★両モデルの FindMany 引数の両方を満たすもの★(satisfies で両方に通す、または ConsecutiveSourcePrisma(consecutive-source.ts:105)と同じ「最小の形」の interface を書く)。★where を2回書かないこと★(片方だけ直すと静かにずれる)
    • ★args.select / args.include には id が必ず含まれること★(型で強制する)
    • ★「clean_timetables 側が勝つ」を厳密に守るための影チェック★: 履歴だけに出た id を chunkForInClause() で分けて cleanTimetable.findMany({ where: { id: { in } }, select: { id: true } }) を撃ち、★当たった id を shadowedIds にする★。 理由: 同じ id で start_at や precheck が2表で違うと、clean_timetables 側は where から外れ、履歴側だけが当たる。影チェックが無いと★負けるはずの履歴の行が出る★。★履歴だけの id が0件ならクエリを撃たない★
    • 戻りは mergeTimetableRows() の結果
  3. orderBy: ★渡してよいが、合わせた後の順序は保証されない★。1(cleanLogs())は :1017-1022 の .sort() が正なので問題ない。★他の11本は順序に依存しない★(集計・Map 化・ルート側の sortPrechecks())。docstring の「orderBy はその写し」(:798-803)に「2表を合わせるので DB の順序はもう写しにもならない」と1行足す
  4. ★count / groupBy を2表にまたがって書かない★(同じ id の重複を引けない)。必要になったら ★findTimetables() で id を取ってから数える★。今は該当0件
  5. precheckItemById() 用に findTimetableById(prisma, id, select) を足す(clean_timetables を findUnique → 無ければ cleanTimetableHistory.findUnique)。7 は findTimetableById(row.parent_id) を引き、precheck === 本清掃 を JS で確かめる(findFirst の条件をそのまま移す)

3b-3. 各経路の書き換え

  • 1・3・4・8・9・10・11: prisma.cleanTimetable.findMany(args) → findTimetables(prisma, args)。★where の中身は1文字も変えない★
  • 2・5(parent_id IN): 塊ごとに findTimetables() を呼び、★.flat() の後にもう一度 id で重複を落とす★ (2表で parent_id が違う同じ id は別の塊に入りうるので、塊の中の重複落としだけでは足りない)。 findTimetables() に { dedupeAcrossChunks } を持たせるより、★timetable-source.ts に findTimetablesByChunks(prisma, chunks, buildArgs) を置き、全塊を2表とも引いてから1回だけ mergeTimetableRows() を通す★ほうが規則が1箇所で済む
  • 6・7: 3b-2 の 5
  • 12: CleanAreaStatsSourcePrisma.cleanTimetable(clean-area-source.ts:147-153)の隣に cleanTimetableHistory を足し、findTimetables() を使う。★where: { id: { in }, precheck } は両表に同じものを渡す★
  • 11: ConsecutiveSourcePrisma(consecutive-source.ts:105-125)に cleanTimetableHistory を足す
  • robot_error_reports / clean_task_reports の引き直し(clean_timetable_id IN)は ★変えない★(合わせた後の id で引くので、履歴の予定に紐づく行も取れる)

3b-4. 性能

  • ★往復は1本→2本(+履歴だけの id があれば影チェック)★。Promise.all で並べる。両表とも start_at / facility_id / parent_id に索引あり(:612-617 / :641-648)
  • ★古い予定が履歴へ移るなら、行の大半(保持2年)は履歴側★。★期間無指定の経路★(cleanLogs() の日付なし・CONSECUTIVE_RANGE_UNBOUNDED・12 の期間なし)は★履歴も全期間読む★。今と同じ性質なので新たに絞らないが、judge.* ログの行数が増えることを docstring に書く
  • IN の分割は既存の chunkForInClause()(上限 DB_IN_CLAUSE_MAX_ITEMS = 1000。lib/config/index.ts:548)をそのまま使う

3b-5. fixtures とシード

  • ★fixtures には予定の表の層が無い★(lib/fixtures/ は結合済みの CleanLogItem を持つ。lib/fixtures/clean-logs.ts:222 の history は★「過去の清掃ログ」の意味で、clean_timetable_histories とは無関係★。名前で取り違えないこと)。 → ★fixtures 版のデータソースは変えない★。USE_FIXTURES の true/false で数字は変わらない
  • scripts/seed-local-db.ts: ★clean_timetable_histories を今は一切書かず、消してもいない★(:848-858)。
    1. cleanSchedule.deleteMany より前(:854 の隣)に cleanTimetableHistory.deleteMany({}) を足す(FK が clean_schedules に向いているため)
    2. 次の3種を入れる(★id は既存の採番と衝突しないことをコードで検査する。:1525-1532 の dangling 検査に履歴も足す★): (a) ★履歴だけの予定★: 過去の本清掃1件とその事前チェック2行を clean_timetables に入れず履歴に入れる。clean_task_reports を1行紐づける (b) ★両方にある同じ予定(中身も同じ)★ (c) ★両方にある同じ id で中身が違う★(履歴側の start_at を1日ずらす)
    3. 末尾の件数ログ(:2062)に clean_timetable_histories= を足す

3b-6. テスト(★実装より先に書く★)

lib/db/__tests__/timetable-source.test.ts(新規。スタブの prisma で):

# 入力 期待
T1 本表だけに id=1 id=1 が1件
T2 履歴だけに id=2 id=2 が1件(影チェックで本表に無い)
T3 両方に id=3(同じ中身) ★1件★。中身は本表のもの
T4 両方に id=4(start_at が違う。どちらも期間内) ★1件・本表の start_at★
T5 id=5: 本表は期間外、履歴は期間内 ★0件★(影チェックで履歴を捨てる)
T6 履歴だけの id が0件 影チェックのクエリを撃たない
T7 2表に同じ where が渡る 両方の呼び出し引数が toEqual で一致
T8 parent_id IN の塊をまたいで同じ id flat 後に1件

既存のスタブ(★cleanTimetableHistory が無いと undefined で落ちる★): app/api/{clean-logs,clean-logs/[id],dashboard,prechecks,prechecks/[id]/status,reports,clean-area}/__tests__/route.test.ts・lib/db/__tests__/{consecutive-source,clean-area-source,report-mapping}.test.ts。 履歴は空配列を返すスタブを足し、★calls.cleanTimetable の引数を見ているテストには calls.cleanTimetableHistory も同じ引数であることを足す★。 経路ごとに1本ずつ「履歴だけの予定が出る」ケースを足す(12 は「履歴だけの予定に紐づくレポートが完了率・実施回数に入る」)。

3b-7. 監督の決定(2026-10-05)

# 論点 決定
Q1 履歴の id は本表の id か ★本表の id をコピーしている(ユーザー回答 2026-10-05)★。★id で重複を除く設計で正しい★。同じ id で facility_id / section_id / start_at のどれかが違ったら [db] の warn を出す(Q2)
Q2 同じ id の中身が違うとき ★本表を採り、warn ログを出す★(イベント名は既存の db.* の流儀に合わせる。id・どの列が違ったか・両方の値を出す。認証情報は出さない)
Q3 期間の基準 ★start_at のまま★(期間の振り分けの基準を変えない。CLAUDE.md §4.7)
Q4 履歴の予定に紐づくエラー行も PATCH で書けるようになる ★それでよい★(実在するアラートなので)
Q5 /schedules ★対象外★(clean_schedules だけを読む)
Q6 移る時期が未実測 ★実装はどちらの表にあっても読めるので影響しない★。接続できたら行数・移る時期を実測して D-158 に記録する(監督)

段階4 — 画面の見出し「屋号」を「顧客」にする(P-15)

4-1. i18n(lib/i18n.ts)— ★キーごと改名する(D12)★

行 いま ★新★
:54 屋号: "Brand" 顧客: "Customer"
:56 全屋号: "All Brands" 全顧客: "All Customers"(§11 Q7)
:358 "選べる屋号が{n}件あります": "{n} brands to choose from" "選べる顧客が{n}件あります": "{n} customers to choose from"
:359 屋号を選ぶ: "Choose a brand" 顧客を選ぶ: "Choose a customer"
:360 この条件では選べる屋号がありません: "No brands available for this filter" この条件では選べる顧客がありません: "No customers available for this filter"
  • コメント :44-52 を書き直す: 「★第11回 P-15=B(customers は法人の単位)で確定。2026-10-05 に見出し語を『屋号』から『顧客』に変えた(D-158)★」。旧の経緯(2026-09-15「屋号でよいです…」)は1行で残す。★英語は仮(D13)★
  • コメント :364 / :375 の 屋号 を 顧客 に(キー名を指しているため)
  • :357 の 顧客を選んでください: "Select a customer" は ★そのまま★

4-2. 呼び出し側

ファイル:行 いま ★新★
lib/masters/options.ts:46 ALL_CUSTOMERS_LABEL = "全屋号" "全顧客"(docstring :33 の「全屋号」も)
components/features/clean-area/CleanAreaScreen.tsx:66 customer: "屋号" customer: "顧客"(粒度タブ :356 と表の見出し :385 に出る)。docstring :61 の「ホールディングス / 屋号 / 店舗」も
components/features/clean-area/CleanAreaScreen.tsx:238 aria-label={t("屋号")} t("顧客")
components/features/alerts/AlertsScreen.tsx:392 同上 同上
components/features/detail-log/DetailLogScreen.tsx:370 同上 同上
components/features/ds-alerts/DsAlertsScreen.tsx:181 同上 同上
components/features/ds-logs/DsLogScreen.tsx:346 同上 同上
components/features/reports/ReportsScreen.tsx:376 同上 同上
components/features/reports/ReportsScreen.tsx:509 / :522 / :530 t("選べる屋号が{n}件あります") / t("屋号を選ぶ") / t("この条件では選べる屋号がありません") 4-1 の新キー
d-watch-api.yaml:51 / :905(清掃面積の粒度) 「ホールディングス/屋号/店舗」 「ホールディングス/顧客/店舗」
d-watch-api.yaml:172-177(GET /customers) 「屋号一覧」「画面の見出し語は「屋号」…TeamWorks 照会中(第9回 Q7-1)」 「顧客一覧」「画面の見出し語は「顧客」(第11回 P-15=B: customers は法人の単位。2026-10-05。D-158)。旧: 「屋号」(2026-09-15)」。★照会中の記述は消す★
d-watch-api.yaml:1680(Customer) 「屋号(customers)」 「顧客(customers。法人の単位。第11回 P-15)」
  • lib/hooks/use-classification-filter.ts:161 は t(ALL_CUSTOMERS_LABEL) なので ★直さなくてよい★
  • ★t() に生の文字列 "屋号" を渡している所が他に無いこと★を最後に確かめる: grep -rn "\"屋号\|屋号\"\|'屋号" app lib components --include=*.ts --include=*.tsx | grep -v __tests__ が ★0件★

    4-3. データ・絞り込み条件の説明(★§11 Q2: 「顧客」にそろえる★)

P-15=B で customers は法人の単位と確定したので、「屋号(customers.id)」という説明は ★誤り★ になった。 ★次の行の中の「屋号」だけを「顧客」に直す★(所属屋号 → 所属顧客、屋号・施設は → 顧客・施設は など。★行の他の部分は変えない★):

  • lib/clean-area/query.ts:58・lib/clean-logs/query.ts:31・lib/ds-alerts/query.ts:25・lib/ds-devices/query.ts:21・lib/facilities/query.ts:28・ lib/prechecks/query.ts:32・lib/reports/query.ts:120・lib/schedules/query.ts:53・lib/api/master-id.ts:89 (どれも /** 屋号(\customers.id`)… */`)
  • lib/types/index.ts:180 / :196 / :954 / :985 / :1299 / :1308-1310
  • d-watch-api.yaml:1519-1520 / :1534 / :2086 / :2100-2101 / :2287

★経緯を書いた文(「旧: 屋号」「2026-09-15『屋号でよいです』」等)は残す★。★処理の理由を述べるコメント・fixtures の説明・テストは対象外★(§7 の表)。

  • テスト lib/masters/__tests__/options.test.ts:157-181 の "全屋号" は ★引数で渡しているだけなので直さなくても通る★。直すなら "全顧客" に(任意)。"屋号A1" などはテスト用の名前なのでそのまま

段階5 — D-157 に控えた古いコメント4件(+ status 6 の C5)

★どれもコメントだけで、動作は変わらない★(C5 も同じ)。行番号は 2026-10-05(74cffd5)で確かめた。

# 場所 いま ★新しい文(この中身で書く)★
C1 lib/db/clean-area-source.ts:424-425 「clean_task_reports.clean_timetable_id は nullable で、★TeamWorks がまだ埋めていない★(指示書AQ 決定5 — 「今後埋める前提」)」 「clean_task_reports.clean_timetable_id は nullable(prisma/schema.prisma:524)。★NULL は予定外のタスク★(予定を持たない実在の清掃。ユーザー回答 2026-10-01。CLAUDE.md §4.7)で、★埋まる予定は無い(NULL の行は常にある)★。旧: 「TeamWorks が今後埋める前提」(指示書AQ 決定5)— 誤り」。★続く :426-429(NULL が1つのグループにまとまる)は今も正しいので残す★
C2 lib/fixtures/clean-task-reports.ts:24 「★CSV では189行すべて NULL★(今後 TeamWorks が埋める前提。決定5)」 「★CSV では189行すべて NULL★。★NULL は予定外のタスク★(予定のある清掃には id が入る。CLAUDE.md §4.7。旧: 「今後 TeamWorks が埋める前提」— 誤り)」
C3 lib/db/report-mapping.ts:245-256(:249 を含む段落) 「clean_timetables.started_at は常に予定時刻と同値でシードされており…稼働率と出発成功率は同じ executed を分子に使う」(★clean_timetables.started_at は存在しない列★) ★段階3-2 の1. で段落ごと書き直す★。時刻の記述は「clean_task_reports.started_at は、予定のある清掃では予定の start_at と同じ値、予定外のタスクでは実際の開始時刻が入る(ユーザー回答 2026-10-05。CLAUDE.md §4.7)。★出発成功率は時刻を見ない(P-6=A)★」
C4 lib/db/report-mapping.ts:278-280(errorCount の注意) 「clean_error が 2(セキュリティ有効)/ 3(D-S制御盤異常)の清掃は robot_error_reports の行が作られないため…『エラー発生率』は実質『ロボット異常の発生率』」(★いまは読んでいない clean_error 列が前提★) 「★清掃の結果は clean_task_reports だけから読み、clean_error 列は読まない★(D-151)。★制御盤・セキュリティのエラー、清掃を開始できなかったエラーも robot_error_reports に入り★(第11回 P-8・P-10)、★clean_timetable_id も入る★(P-9=A)。★本清掃の予定の id が入った行は、区分を問わずこの件数に入る★(どの予定の id が入るかは無回答)。★区分(ロボット/制御盤/セキュリティ・清掃開始失敗)の見分けと D-Security アラートの取得元の切り替えは次フェーズ★(D-158 当方の対応5)。旧: 第8回 A6-1「clean_error 2/3 の清掃は行が作られない」— 第11回で変わった」
C5 ★status 6 の説明3か所(§11 Q4)★: lib/config/index.ts:368-369(CLEAN_TASK_STATUS_LABELS の docstring)・lib/fixtures/clean-task-reports.ts:25(表の status の行)・lib/format/clean-result.ts:14 「意味は未受領。第11回で確認」/「★6 は未受領★」 「★6 は Pudu の値で、先方も意味が分からない(第11回 P-4。D-158)★。分かれば先方がデータを入れる。★数字のまま表示する★」。★:369 の続き「「その他」「不明」に潰さないこと」は残す★。★cleanTaskStatusLabel() の中身・CLEAN_TASK_STATUS_LABELS の値は変えない★

段階6 — 全体の確認

  1. npx tsc --noEmit -p . — エラー0件
  2. npm run lint — 指摘0件(重複キー・$queryRaw・any が無いこと)
  3. npm test — ★全件通過★(表1・表2・表3 を含む)
  4. ★USE_FIXTURES=true で npm run build★ — 成功(PowerShell なら $env:USE_FIXTURES='true'; npm run build)
  5. ★画面で確かめる(USE_FIXTURES=true)★: .claude/launch.json の ★d-watch-fixtures(ポート 3100)★ で起動する(Claude Code なら preview_start の d-watch-fixtures)。★確かめ終わったら、自分で起動した dev サーバーは必ず止める★
    • ★屋号→顧客★: 清掃詳細ログ・アラート・D-Sec ログ・D-Sec アラート・レポート・総清掃面積の6画面で、分類のセレクトの先頭が「全顧客」。総清掃面積の粒度タブと表の見出しが「顧客」。レポートで顧客を選ばずに KPI が 422 になる状態(fixtures は顧客4件)で「選べる顧客が4件あります」「顧客を選ぶ →」
    • ★JP/EN 切替★: EN で "All Customers" / "Customer" / "4 customers to choose from" / "Choose a customer"。★JP では幅が変わらないこと★(文字数が同じ)。★EN でツールバーが不自然に折り返さないこと★をデスクトップ幅とモバイル幅(375px)で見る。崩れていたら直さずに報告(4-12)
    • ★数字が変わらないこと★: fixtures には task_area = 0 で実施した行が無く(lib/db/fixture-clean-area.ts:20-25)、レポートの KPI は作り値(lib/fixtures/reports.ts)なので、★詳細ログ・エリア別詳細・総清掃面積・レポートの数字は変わらないのが正しい★。指示書AS 段階7-5 の境界値(施設2 = 90% ◎ / 施設3 = 90% ○ / 施設5 = 70% ○ / 施設8 = 10% △ / 施設9 = 10% × / 施設10 = 0% ×、エリア別詳細の施設7・1日前が全体 81% ○)がそのまま出ること
    • ★P-5・P-6 の効き目は画面では見えない★(上の理由)。★単体テスト(表1〜表3)で確かめる★。任意で、シードを入れたローカルDB(dwatch_local。USE_FIXTURES=false)のレポート当期で「稼働率 ≤ 出発成功率」になっていることを見てよい(★ステージングには向けない★)
  6. 最後の grep(★0件になること★):
    grep -rnE "status ?===? ?4([^0-9]|$)|定刻出発|清掃実施回数 ÷ 清掃予定|÷ 清掃実施回数" app lib components d-watch-api.yaml --include=*.ts --include=*.tsx --include=*.yaml | grep -v lib/generated
    grep -rn "t(\"屋号\|t(\"全屋号\|\"選べる屋号\|\"屋号を選ぶ\|選べる屋号がありません" app lib components --include=*.ts --include=*.tsx

7. 「屋号」の出現箇所の分類(2026-10-05。grep -rn "屋号" app lib components --include=*.ts --include=*.tsx。テストを除き約140件)

分類 場所 扱い
★画面に出る文字列(i18n のキー・既定の見出し語)★ lib/i18n.ts:54 / :56 / :358-360、lib/masters/options.ts:46、components/features/clean-area/CleanAreaScreen.tsx:66 ★直す★(段階4)
★画面に出る文字列の呼び出し(t("…"))★ aria-label 6件(AlertsScreen.tsx:392 / CleanAreaScreen.tsx:238 / DetailLogScreen.tsx:370 / DsAlertsScreen.tsx:181 / DsLogScreen.tsx:346 / ReportsScreen.tsx:376)、ReportsScreen.tsx:509 / :522 / :530 ★直す★
★見出し語そのものを説明するコメント★ lib/i18n.ts:44-52 / :364 / :375、lib/masters/options.ts:33、CleanAreaScreen.tsx:61 / :374 ★直す★
★API 定義のうち見出し語・customers の単位を述べる所★ d-watch-api.yaml:51 / :172-177 / :905 / :1680 ★直す★
データ・絞り込み条件の説明(「屋号(customers.id)で絞り込み」など) lib/*/query.ts(clean-area:58 / clean-logs:31 / ds-alerts:25 / ds-devices:21 / facilities:28 / prechecks:32 / reports:120 / schedules:53)、lib/api/master-id.ts:89、lib/types/index.ts:180 / :196 / :954 / :985 / :1299 / :1308-1310、d-watch-api.yaml:1519-1534 / :2086 / :2100-2101 / :2287 ★直す★(段階4 の 4-3。★列挙した行だけ。一括置換しない。§11 Q2★)
処理の理由・経緯を述べるコメント(「この屋号の昨夜の成績」「屋号が0件のときは出さない」など) components/features/*/*.tsx の JSX コメント、lib/hooks/use-classification-filter.ts、lib/masters/options.ts(:2-26 / :105-190)、app/api/**/route.ts、lib/db/*.ts、lib/facilities/*.ts、lib/clean-area/aggregate.ts、lib/logger/events.ts:218、lib/judge/kpi.ts:51 ★残す★
fixtures の説明 lib/fixtures/customers.ts / groups.ts / facilities.ts / reports.ts:203-212 ★残す★(顧客名の値もそのまま)
テスト app/api/**/__tests__ / lib/**/__tests__(約120件) ★残す★(t() のキーを文字列で比べているテストは無い。2026-10-05 確認)
モック d-watch-mock.jsx(「全会社」) ★触らない★(自前の辞書を持ち、アプリの i18n に依存しない)

8. テスト一覧(本書で足す・直すもの)

ファイル 内容 段階
lib/judge/__tests__/run-completion-rate.test.ts 表1 R9 差し替え・R12 追加 1
lib/judge/__tests__/kpi.test.ts 3つの率の分母・errorRate 上限なし・isCompletedTask() 1
lib/db/__tests__/report-mapping.test.ts 表2 K1〜K8・稼働率 ≤ 出発成功率/既存の completed・status の追加/w11(表3)/kpiFromCleanStats のエラー発生率 1 / 2 / 3
lib/db/__tests__/clean-log-mapping.test.ts task_area = 0 → 0/不変条件が両向き 2
lib/db/__tests__/consecutive-source.test.ts Q9・テスト内の再実装を消す 2
lib/db/__tests__/clean-area-source.test.ts F5(表3) 2
lib/db/__tests__/fixture-clean-area.test.ts :145 2
lib/judge/__tests__/rescue.test.ts / lib/db/__tests__/rescue-mapping.test.ts ラベルだけ(期待値は変えない) 2
app/api/reports/__tests__/route.test.ts 当期のエラー発生率 50 3
lib/fixtures/__tests__/reports.test.ts ★作り値の全行で 稼働率 ≤ 出発成功率(value / prev)★(§11 Q5) 3
lib/db/__tests__/timetable-source.test.ts(新規)ほか 3b-6 の一覧 T1〜T8・既存スタブに cleanTimetableHistory 3b
robotErrorReport を読む経路のテスト スタブに robot_id: null の行を混ぜても落ちない(段階0 の 4) 0

9. 触ってはいけないもの

  • CLAUDE.md / docs/decisions.md(監督が直す)
  • prisma/schema.prisma / lib/generated/prisma/**(★DB の定義は TeamWorks の領域。CLAUDE.md §7★。P-8 の DDL 変更の取り込み(db pull)も本書ではやらない)
  • docs/code-map.md(監督が直す)
  • ★D-Security アラートの取得元(いまは control_error_histories)★ — robot_error_reports への切り替え(P-10)は ★次フェーズ★(D-158 当方の対応5)
  • ★robot_error_reports の3区分(ロボット/制御盤/セキュリティ・清掃開始失敗)の見分け★ — ★次フェーズ★。error_type を推測で分類しない(CLAUDE.md §4.8)
  • 4カラム UPDATE(status / comment / responded_at / resolved_at。指示書AT)
  • kpiFromSummaryRatios() / SummaryRatios / periodOfSummaryRow()(lib/db/report-mapping.ts:99 付近・:218-241)
  • meanCleanRatio() の中身・judgeGrade() / displayRate() / GRADE_THRESHOLDS / RUN_COMPLETION_RATE の値
  • latestReportByTimetableId() / notExecutedFromReport() / isFutureSchedule() の中身(lib/db/clean-log-mapping.ts)
  • launchFailedDaysInRun() / foldRescueDays() の中身(D3)
  • scripts/seed-local-db.ts の ★接続先とガード★(lib/db/local-guard.ts を含む)
  • d-watch-mock.jsx

10. 範囲外(本書ではやらない。監督が扱う)

  • P-8〜P-10(エラーの集約・3区分・D-Sec アラートの取得元)— 次フェーズ
  • 複数顧客の KPI の合算(出発成功率・エラー発生率は材料がそろった)— §11 Q3

11. 監督の決定(2026-10-05。D-158)

# 論点 決定
Q1 表3 を正にするか ★正として固定する★
Q2 「屋号(customers.id)」というデータの説明 ★「顧客」にそろえる★(P-15 で customers は法人と確定したので、「屋号」は誤りになった)。経緯を書いた文(「旧: 屋号」等)は残す。★対象の行は段階4 の 4-3★
Q3 複数顧客の KPI 合算 ★変えない★(指示書AK の方針のまま。本書は表の記述を直すだけ)
Q4 「status 6 は未受領・第11回で確認」のコメント ★直す★: 「6 は Pudu の値で先方も意味不明(第11回 P-4)。分かれば先方がデータを入れる。数字のまま表示」
Q5 lib/fixtures/reports.ts:256 の稼働率 > 出発成功率 ★直す★。★出発成功率の側を上げる(65.0 → 75.0 / 前期 68.0 → 71.0)★。不変条件のテストを1件足す(段階3-4 の表の下)。★改訂(2026-10-05)★: 当初は「稼働率側を下げる(72.4 → 60.5)」としたが、稼働率の昇順を守るテスト(lib/fixtures/__tests__/reports.test.ts:212)が落ち、並べ替えれば画面のワースト1位が変わるため取り下げた
Q6 robot_id が DDL では NULL 可・Prisma では NOT NULL ★段階0 に足す: robotErrorReport の読み取りはすべて select で必要な列だけにし、robot_id・robot リレーションを読まない★(NULL の行が入っても読み取りで落ちないように)。★prisma db pull は監督がステージングに接続できるようになってから行う★(本書ではスキーマを変えない)
Q7 EN の大文字小文字 ★"All Customers" / "Customer"★(既存の "All Holdings" にそろえる。英訳はユーザーが後でまとめて見直す)
Q8 稼働率の式が変わり、施設ステータス(閾値 good 90 / warning 75。FACILITY_STATUS_THRESHOLDS。★訂正 2026-10-06: 当初「90/80」は誤り★)と稼働率ワースト順が以前より悪く出る ★閾値は据え置く★(変えるならユーザー判断。監督からユーザーに伝える)
範囲を広げた3点 完了率の「暫定」→「確定」の記述 / シードの error_count の分母 / runCompletionRate() の戻り値を number に ★3点とも採る★

12. コミット

段階ごとに分ける(段階0 の 4 / 段階1 / 段階2 / 段階3 / 段階3b / 段階4 / 段階5)。★git add はファイルパスを明示する★(git add -A 禁止)。 メッセージには ★何を変えたか★ と ★なぜ(D-158。TeamWorks 第11回 P-5 / P-6 / P-15)★ を書く。★段階1 のコミットは「テストだけ(この時点では落ちる)」と明記する★。 ★分割 PR の作成とマージは daichi がレビューしてから行う★(D-154)。