Newer
Older
client_dwatch / docs / handoff / 指示書U_清掃結果をclean_errorで判定する.md

指示書U — 清掃結果を clean_status_histories.clean_error で判定する

項目 内容
実行環境 ★Cursor 内の Claude が実装します★(.claude/commands/d-watch.md の役割分担表を参照)
実行状態 ★★実行済み(2026-09-10。D-107)★★
実行順 ★指示書S(D-105)と 942225a(isFutureSchedule() の追加)の続き★。★指示書M / P / R / S が作った scripts/seed-local-db.ts / lib/db/prisma.ts / lib/db/local-guard.ts / lib/db/use-fixtures.ts / lib/db/ds-status.ts / lib/db/clean-log-mapping.ts / lib/db/precheck-mapping.ts / lib/prechecks/label.ts の上に積む作業★です。★prismaSource.facilities() / schedules() / dsDevices() / dsAlerts() は1文字も変えません★(cleanLogs() / prechecks() は本指示書の対象です)。★触るコードが重なる未実行の指示書は、着手直前に自分で確認すること★(作成時点では、指示書T(第5歩・レポートサマリ)が docs/decisions.md / docs/handoff.md を共通で触るので、記録の段階だけは順に行うこと)
取得する決定番号 ★**着手直前に `grep -nE "^### D-(0\ 1)[0-9][0-9]" docs/decisions.md \ tail -3で最終番号を実測すること**★。★★改訂時点で別担当がD-106(★本指示書の判定方針を確定した決定★)をdocs/decisions.mdに追記中なので、最初の空き番号は **D-107** の見込み★★。★それでも**必ず自分で実測してから採番する**こと★。★★^### D-0だけでは3桁台(D-100 以降)を拾えない★★。★2026-09-07 にD-068` の二重採番が実際に起きています★
前提 ★★D-106★★(★本指示書の判定方針を確定した決定★。★別担当が改訂時点で追記中なので、着手直前に自分で行番号と内容を確認すること★。★「履歴の行が無い過去の本清掃は出発失敗として残す」という方針の根拠★) / ★D-091 項1★(docs/decisions.md:3442。★出発判定は DB の値で行う。猶予30分を当方で計算しない★) / ★D-099 §1★(:4442。★現行の「履歴の行が無い=未出発」という推測の出所★) / ★D-103 項1★(:5222。★launch_failed と departed === false は同じ出来事。同じ純粋関数を共有する★) / ★D-104★(:5335。出発失敗の行の埋め方 / ★「その他」「不明」という語を使わない★) / ★D-105★(:5435。指示書S の実行記録) / D-077(:1847。★エラー値を変換しない★) / D-089 項3(:3225。★「その他」は使わない★) / D-092(:3599。1清掃 = 3行の仕組み) / D-093(:3719。clean_statuses の役割) / D-094(:3822。率・面積の小数化) / D-042(:226。書き込み許可4カラム) / D-050(:308。段階ごとに検証してからコミット) / D-056(:353。schema.prisma は生成物) / D-026(:122。連続はスケジュール存在日ベース)
段階数 ★7段階★(段階0〜段階6)
★着手前確認★ ★★なし★★ — ★判定に使う値・コード表・ラベルはすべてユーザー回答(2026-09-10)と受領済みの定義書で確定しています★。★★ユーザーの回答を待たずに段階0 から通しで実行してよい★★。★未決が1件残りますが(clean_error が 2/3/4 の行を画面のどこに出すか)、★★本指示書は「値を API まで運ぶ」ところで止めるので、実装は止まりません★★(→「申し送り」)
着手前の基準 ★着手直前に自分で npm test / npx tsc --noEmit / npm run lint を実行し、その実測値を基準にすること★(作成時点の実測は ★43ファイル / 813テスト / tsc 0 / lint 0★ だが、★鵜呑みにしない★)
作成 2026-09-10(作成時点: 決定事項最終番号 D-105 / docs/handoff.md の最新は 追記30(docs/handoff.md:2434)/ HEAD 942225a)
★改訂★ 2026-09-10(★設計3 の方針を反転★: 「履歴の行が無い=除外」→ ★★「履歴の行が無い=出発失敗として残す」★★。★fixtures の値は1行も変更しないことを明記★。★決定番号を D-106 → D-107 にずらした★。HEAD 942225a のまま)

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

★この指示書を読んでいるあなたへ★ — .claude/commands/d-watch.md の「実装は Cursor 側」は 2つの環境の役割分担です。Cursor の中で動いている Claude は実装担当です。 ★調査や改訂ではなく実装を行ってください。★ ★★うまくいかなければ途中で止めて構いません★★ — ただし ★★段階3 はシードと読み出しを 同一コミットで変えます★★(★特にシードだけコミットすると /detail-log の完了率が壊れます★。 → ★「作業の順序」末尾の注記★)。 ★止めるなら段階の切れ目(コミット単位)で止めること★。


★★この指示書の核心 — 推測を1つ消す★★

現状(★すべて 2026-09-10 実測★)

箇所 実測内容
★lib/db/clean-log-mapping.ts:80★ `export function departedFromHistory(history: T \ undefined): boolean { return history !== undefined; }` — ★「履歴の行が無い=未出発」★
★同 :63-79 のコメント★ ★「clean_status(-1/0/1/2)に『出発しなかった』を表す値が無く、行が作られないことが未実施を意味する、という D-091 項1 後段の読みを採っている」★ / ★「実データでどのカラムが未実施を表すかは要確認」★
★lib/db/index.ts:304★ departed: departedFromHistory(history)(cleanLogs())
★lib/db/index.ts:461★ if (departedFromHistory(historyByTimetableId.get(key(row.id)))) continue;(prechecks() の launch_failed)
★scripts/seed-local-db.ts:779★ // ★未実施(departed=false)は clean_status_histories を作らない(設計判断1)★ — ★シードもこの推測に合わせて作られている★

★★つまり「行の有無」で判定しているのは当方の推測であり、根拠のあるカラムを読んでいませんでした★★ (D-099 §1 が「実データでどのカラムが未実施を表すかは依然として不明」と自ら記録しています)。

★★消すのは「★行があるから出発した★」という推測のほう★★です。 ★★「★行が無いなら出発失敗★」という側は残します★★ — ★ただし推測としてではなく、 ★★「過去なのに結果行が無いのはデータ収集処理の障害だから、黙って消さずに見せる」という決定★★ として残します(→ ★設計3★)。★行の有無を両方向に使っていたのを、片方向に絞る作業だと思ってください★。

★★根拠が受領済みの定義書に載っていた★★

★docs/handoff/analysis/table-definitions-02-clean-error.md:213★(clean_status_histories の カラム6「結果」clean_error)の ★原文★:

0: 正常, 1: 異常(清掃未実施), 2: 異常(セキュリティ有効), 3: 異常(D-S制御盤異常), 4: 異常(清掃ロボット異常)
  • ★同じコード表が clean_statuses 側(同ファイル :181。カラム7)にもある★。 ★論理カラム名だけが違う(履歴側は「結果」、clean_statuses 側は「エラー」)★ — ★原文ママ★
  • ★Prisma 上の型★: clean_error Int @default(0) @db.UnsignedTinyInt (prisma/schema.prisma:479 が履歴側 / :438 が clean_statuses 側)
  • ★unsigned tinyint なので 0〜255 が入りうる★(→ ★コード表に無い値の扱いを決める。落とし穴3★)

★★ユーザーが確定させたこと(2026-09-10。この前提で実装すること)★★

# 確定内容
★1★ ★★清掃が実施されなかった場合も履歴の行は作成される★★(★原文: 「書いてある以上は作成すると思うので、それを前提で実装してください」★)
★2★ ★★出発失敗(未実施)は clean_error = 1 のみ★★
★3★ ★★2(セキュリティ有効)/ 3(D-S制御盤異常)は「未実施(出発失敗)とは違う扱い」★★(★原文どおり★)。★4(清掃ロボット異常)も同様に「未実施ではない」側★
★4★ ★clean_timetables には1週間分の予定がまとめて登録される★
★5★ ★予定が無い場合は「清掃予定なし」であって異常ではない★

★確定 #1 は「定義書にそう書いてあるから、そう作られるはず」という★推論に基づく前提★です★ — ★実データでの確認は取れていません★。★★外れたときに何が起きるかを「申し送り」に最上位で書いてあります★★。


★設計(これをそのまま実装すること)★

1. ★コード表は lib/config/ の1箇所に定義する★

★0/1/2/3/4 をロジックに直書きしないこと★(CLAUDE.md §7 / 禁止事項4)。 ★TeamWorks の回答で分類が変わっても1箇所の変更で済む形にする★。

lib/config/index.ts に追加する(★CLEAN_TIMETABLE_PRECHECK の隣が自然★):

export const CLEAN_ERROR = {
  /** 0: 正常 */
  normal: 0,
  /** ★1: 異常(清掃未実施) — 出発失敗(未実施)はこの値のみ(ユーザー回答 2026-09-10)★ */
  notExecuted: 1,
  /** 2: 異常(セキュリティ有効) — ★未実施とは違う扱い★ */
  securityEnabled: 2,
  /** 3: 異常(D-S制御盤異常) — ★未実施とは違う扱い★ */
  controlBoardError: 3,
  /** 4: 異常(清掃ロボット異常) — ★未実施とは違う扱い★ */
  robotError: 4,
} as const;

★ラベル表も同じ場所に置く★(コード → ★定義書の原文★):

コード ★ラベル(原文ママ。★言い換え禁止★)★
0 正常
1 異常(清掃未実施)
2 異常(セキュリティ有効)
3 異常(D-S制御盤異常)
4 異常(清掃ロボット異常)
  • ★★括弧は半角のまま★★(定義書の原文が半角)。★全角に直さないこと★
  • ★★コード表に無い値(5以上)は String(code) を返す★★ — ★生値をそのまま出す★。 ★★「その他」「不明」という語を使わないこと★★(★D-089 項3 / D-104 項2★。 「その他」は破棄済み D-067 の「未知の値をその他に寄せる」方針と紛らわしい)。 ★前例: MAKER_NAMES[maker_id] ?? String(maker_id)(lib/config/index.ts / lib/db/index.ts の facilities())と formatModelName()(lib/db/clean-log-mapping.ts:27)★
  • ★ラベルは本指示書ではどの画面にも出しません★(→ 段階4 / 申し送り)。 ★それでも★ ここで定義してテストで固定する★理由★: ★後から「読みやすく」書き換えられるのを止めるため★。 ★原文が正であることをテストで宣言しておく★(CLAUDE.md §6-2)

2. ★判定は純粋関数4本に閉じ込め、cleanLogs() と prechecks() が共有する★

★置き場所は lib/db/clean-log-mapping.ts★(★新設しない★ — ★D-103 が「同じ出来事は同じ判定で出す。 中身が1行でも、定義がここ1箇所であることに意味がある」と定めた場所★)。

# 関数 中身 使う側
★1★ ★`hasCleanResult(history: T \ undefined): boolean`★ history !== undefined — ★★意味は「出発したか」ではなく「清掃結果が記録されているか」★★ ★#2 / #3 の内部でだけ使う(lib/db/index.ts からは直接呼ばない)★
★2★ ★`notExecutedFromHistory(history: CleanResultLike \ undefined): boolean`★ ★★`!hasCleanResult(history) \ \ history.clean_error === CLEAN_ERROR.notExecuted★★ — ★★行が無い場合もtrue`(=出発失敗)★★(設計3) cleanLogs() / prechecks()
★3★ ★`completionRateFromHistory(history: (CleanResultLike & { clean_ratio: number }) \ undefined): number \ null`★ ★行が無い → null / 未実施 → null / それ以外 → clean_ratio の丸めない生値★ cleanLogs()
★4★ ★latestHistoryByTimetableId<T extends { clean_timetable_id: bigint; ended_at: Date }>(histories: readonly T[]): Map<string, T>★ ★clean_timetable_id.toString() をキーに、★ended_at が最新の1件★を残す(同着は入力順の先頭)★ cleanLogs() / prechecks()

★★#1 は既存の departedFromHistory() を改称したもの★★(中身は同じ1行)。 ★★departed は !notExecutedFromHistory(history) から作る★★ — ★「行があれば出発した」という推測を、関数名ごと消すことが本指示書の目的★。

★★#2 に「行が無い」条件を含める理由★★ — ★D-103 項1 が「launch_failed と departed === false は 同じ出来事。同じ純粋関数を共有する」と定めている★。★もし departed = hasCleanResult(h) && !notExecuted(h) の ような 2項の AND を呼び出し側に書くと、cleanLogs() と prechecks() の2箇所に同じ規則が生まれる★。 ★★出発失敗の定義は notExecutedFromHistory() 1本だけにすること★★。 ★帰結: hasCleanResult() は外から呼ばれなくなるが、★改称して残す★(★「行があれば出発した」という 推測の名前を消すこと自体が目的であり、テストで意味を固定しておく★)。

★★#4 が必要な理由(★これは見落としやすい。実測で確認済み★)★★:

現状 実測
cleanLogs()(lib/db/index.ts:221-229) ★ended_at が最新の1件を残している★
prechecks()(lib/db/index.ts:384-386) ★new Map(histories.map(...)) = DB返却順の後勝ち★

★★「行があるか」だけを見ていた間はどちらでも同じ答えだったが、clean_error を読むと どの行を選ぶかで答えが変わる★★。★選び方を1箇所に固定しないと /detail-log と /alerts で 出発失敗の件数が食い違う★(D-103 が禁じた状態そのもの)。

3. ★履歴の行がそもそも無い場合 — ★★「出発失敗として残す」★★(★決定。実装はこの通り★)

★★判定式(これが本指示書の1行まとめ)★★:

departed = 履歴の行がある かつ その行の clean_error !== 1
選択肢 内容 採否
A 履歴の行が無い本清掃は、cleanLogs() の結果に含めない / launch_failed にもしない ★★不採用★★
B 行は返し、departed: true / completion_rate: null にする ★不採用★
C ✅ ★★履歴の行が無い(かつ予定時刻を過ぎている)本清掃は、これまでどおり「出発失敗」として残す★★(★現行の13件が維持される★) ★★採用★★

★★C を採る理由★★:

  1. ★★fail-silent より fail-loud を採る★★(★これが決め手★)。 ★ユーザー確定 #1「★未実施でも履歴の行は作成される★」を前提に置くのだから、 ★★過去なのに履歴の行が無い = データ収集処理の障害★★である★。 ★★行ごと消すと(A 案)、運用担当は「何かが足りない」ことにすら気づけない★★ — ★画面には何も出ず、件数がそっと減るだけ★。★出発失敗として並べば、 「翌朝確認型」の運用の中で異常として目に入る★(CLAUDE.md §1)
  2. ★★現行の挙動を変えないので、「変えてはいけない数字」#2(departed === false が 13件)が そのまま維持される★★。★A 案は 13件を 0件にしうる(しかも例外もテスト失敗も出ない)★
  3. ★★B を採ると departed: true という根拠の無い断定を作ることになる★★ (★出発したという証拠も無い★)。★D-098 / D-099 項2 が Standby. について 「実際に読んだ値ではない」ことをわざわざコメントに残したのと同じ問題を、 より目立たない形で作ってしまう★。★C は「出発した」と断定しない側に倒れている★
  4. ★★不変条件 departed === false ⟺ completion_rate === null は C でも同じように守られる★★ — ★履歴の行が無い行は completion_rate も null になるため★ (completionRateFromHistory(undefined) === null)。 ★lib/dashboard/aggregate.ts:39 / :96(ダッシュボードの「予定 / 実施」)と isProblemCompletion()(連続日数)がこの対応を前提にしているが、★C はこれを崩さない★

    ★★注意(★起草時の誤りの訂正★)★★ — ★「不変条件を守るため A を採る」という論旨は成立しない★。 ★不変条件は A でも C でも守られる(崩れるのは B だけ)★。★不変条件は A の根拠にはならないので、 A / C の選択理由として使わないこと★

★★C の帰結(★段階5 で実測し、申し送りに書くこと★)★★: ★前提(未実施でも行が作られる)が外れていても、★★出発失敗が画面から消えることはない★★★ — ★★代わりに「結果がまだ書かれていないだけの清掃」まで出発失敗として出る(過剰検知)★★。 ★★どちらに倒すかの判断として、意図的に過剰検知の側を選んでいる★★(理由1)。 ★段階5 では「clean_status_histories の行数(253)= clean_timetables の本清掃行数(253)」を 実測し、★★差があればそれは「出発失敗として出ている行」の件数である★★ことを確認する。

4. ★isFutureSchedule() は残す(★消さないこと。★C 案の下では必須★)★

  • ★★C 案では isFutureSchedule() が必須★★ — ★未来の予定にはまだ履歴の行が無いので、 ★★外すと未来の清掃が全部「出発失敗」になる★★★
  • ★★加えて、逆向きの事故も止める★★ — ★もし TeamWorks が「予定の行を作った時点で履歴の行も先に作る」 実装だった場合、未来の予定に clean_error = 0(default)の履歴が付き、★未来の清掃が 「完了率0%・×」として並ぶ★。isFutureSchedule() はその事故も1行で止める★
  • ★942225a(fix: 予定時刻が来ていない本清掃を出発失敗から除外する)で入ったもの★。 ★同関数の docstring(lib/db/clean-log-mapping.ts:85-112)にある 「★LAUNCH_GRACE_MINUTES を足さない★」という注意もそのまま残すこと★

5. ★clean_error が 2/3/4 の行の扱い★

  • ★★departed は true★★(未実施ではない。ユーザー確定 #3)。★completion_rate は clean_ratio の生値★ → ★★つまり /detail-log では「出発して、その完了率で終わった清掃」として今と同じように出る★★
  • ★★どこに「セキュリティ有効だった」「D-S制御盤異常だった」を出すかは未決★★ → ★本指示書は ★★「値を API まで運ぶ」ところまで★★ をやる(段階4)。★画面への出し方は別途★
  • ★★推測で 2/3/4 を出発失敗やエラー種別7分類に寄せないこと★★(禁止事項5)

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

1. ★★変えてはいけない数字(★2026-09-10 実測★)★★

# 対象 値
★1★ ★cleanLogs() の件数★ ★253★
★2★ ★cleanLogs() の departed === false★ ★13★(★completion_rate === null と完全一致★。D-099 §1 で実測済み)
★3★ ★prechecks() の件数★ ★27★(★precheck 14 + launch_failed 13★)
★4★ ★/dashboard★ ★9 / 9★(D-099)
★5★ ★テスト★ ★43ファイル / 813テスト★(★増えるのは正しい。減ってはいけない★)
★6★ clean_timetables の行数 ★precheck=0 が 253 / precheck=1 が 253 / precheck=2 が 253★
★7★ clean_status_histories の行数 ★240★(= 253 − 13)→ ★段階3 で 253 になる★
★8★ ★clean_status_histories.clean_error の分布★ ★全件 0★(240行すべて。★だから今は検証できない★→ 段階3 でシードに 1/2/3/4 を足す)

★★#1〜#4 は本指示書の前後で変わってはいけません★★。 ★変わったら設計どおりに実装できていません★(★★特に #2(departed === false が 13)が 13 以外になるのは、シードだけを変えて読み出しを変えなかったとき★★ — ★13行が departed: true / completion_rate: 0 に化ける(落とし穴1)★)。

2. ★落とし穴★

# 落とし穴
★1★ ★★未実施行に履歴を作ると completion_rate が null → 0 に化ける★★。★現行は completion_rate: history ? history.clean_ratio : null(lib/db/index.ts:306)★なので、★行を作った瞬間に 13行が 0(=「0%で実施した」)になる★。★判定(grade)は両方 × なので画面もテストも赤くならない★。★★completionRateFromHistory() に置き換えるのが必須★★
★2★ ★★履歴を選ぶ規則が cleanLogs() と prechecks() で違う★★(設計2 の #4)。★同一 clean_timetable_id に複数行あると答えが分かれる★
★3★ ★clean_error は unsigned tinyint(0〜255)★。★コード表に無い値(5以上)が来ても例外は出ない★。★★「★履歴の行がある場合に★未実施と判定できるのは 1 のときだけ」という形(=== CLEAN_ERROR.notExecuted)にしておけば、未知の値は自動的に「未実施ではない」側に落ちる★★。★逆に「!== 0 なら異常」のような書き方をすると未知の値の扱いが変わるのでしないこと★。★★「履歴の行が無い」ケースはこれとは別の条件(!hasCleanResult(history))で、★そちらは未実施(出発失敗)側★★★(設計3-C)
★4★ ★clean_status_histories.started_at / ended_at は ★NOT NULL★(prisma/schema.prisma:490-491。DateTime、? が付いていない)★。★未実施行を作るとき null にできない★ → ★定義書の原文「★清掃スケジュールの清掃開始日時を格納する★」(table-definitions-02-clean-error.md:217-218)に従い、★予定の時刻をそのまま入れる★★
★5★ ★★未実施行の task_area を 0 にすると、面積からの率の再計算が 0 / 0 になる★★(D-091 項2 が「境界の厳密さが要る場面では面積から計算する」と書いている手が使えない)。★再計算する側は先に clean_error を見ること★。★コメントに残すこと★
★6★ ★clean_ratio は Float(double(4,1)。D-094 で小数化済み)★。★丸めないこと★(CLAUDE.md §4.3)
★7★ ★clean_status_histories.id は AUTO_INCREMENT ではない★(prisma/schema.prisma:466 付近のコメント)。★シードで明示採番する(既存の設計判断3 = 本清掃 id と同値 を崩さない)★
★8★ ★BigInt → Number()★ を忘れると JSON.stringify が例外を投げる(tsc は捕まえない)
★9★ ★clean_status_histories / clean_statuses に deleted_at は無い★ → ★論理削除の絞り込みを書かない★

3. ★シードの現状(scripts/seed-local-db.ts。★実測して確認すること★)★

# 箇所 内容
1 ★:157-165(設計判断1)★ ★「departed: false(未実施)は clean_status_histories の行を作らない」★ — ★★本指示書で書き換える★★
2 ★:779★ if (log.completion_rate === null) continue; — ★★履歴も clean_statuses も作らずに次の行へ飛ぶ★★
3 ★:782-785★ clean_status は SECURITY_DISABLED_EVERY(=5)の剰余で -1 / 2 を散らしている(★決定的・冪等★)
4 ★:209-210★ CLEAN_STATUS_FINISHED_SECURITY_ENABLED = 2 / CLEAN_STATUS_FINISHED_SECURITY_DISABLED = -1。★★0(清掃前)の定数は無い★★
5 ★:788-797★ clean_area / task_area * 100 ≒ clean_ratio になるよう clean_area を逆算している(TASK_AREA_SQM = 1000)
6 ★:812-826★ clean_statuses は ★当日分だけ★ 作る(D-093 項1。「今の状態」用)

★シードを触る作法★: ★deleteMany の逆順(:436-437 付近)を崩さない★ / ★2回続けて実行しても結果が同じであること(冪等)★ / ★ランダムを使わないこと★ / ★実在の住所・電話番号・社名・人名を入れないこと★(CLAUDE.md §7)。


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

# ★してはいけないこと★ 理由
★1★ ★★git add -A を使うこと★★ CLAUDE.md §7。★過去に別作業の途中ファイルを巻き込む事故が起きている★。★必ずファイルパスを明示★
★2★ ★★docs/handoff/管理画面_設計検討_20260910.md に触ること★★ ★★未追跡ファイル(別作業の途中)★★。★git status に ?? で出るが、本指示書のコミットに含めない・編集しない・削除しない★
★3★ ★★メインDBのスキーマ変更(prisma migrate / db push / db pull / prisma/schema.prisma の手書き)★★ CLAUDE.md §7 / D-056 / D-059。★clean_error は既にスキーマにある(prisma/schema.prisma:438 / :479)ので、スキーマ変更は1つも要らない★
★4★ ★★書き込み許可4カラム以外への DB 書き込み★★ D-042。★本作業の読み出しは読むだけ★。★書き込みは scripts/seed-local-db.ts(dwatch_local のみ)だけ★。★メインDB / dwatch_ddl に接続しないこと★
★5★ ★★コード値(0/1/2/3/4)・閾値をハードコードすること★★ CLAUDE.md §7。★lib/config/ の CLEAN_ERROR から読む★。★シードも同じ定数を使う★(★「シードだから直書きでいい」は無し。分類が変われば両方直す必要がある★)
★6★ ★★推測で分類すること★★ D-077 / §4.8。★7分類に寄せない★ / ★2/3/4 を出発失敗に読み替えない★ / ★ラベルを言い換えない(原文ママ)★ / ★★「その他」「不明」という語を使わない★★(D-089 項3 / D-104 項2)
★7★ ★★isFutureSchedule() を消すこと・猶予を足すこと★★ ★設計4★。★LAUNCH_GRACE_MINUTES を lib/db/ から読まないこと★(D-091 項1。★設定値を変えると過去のログの判定まで変わる★)
★8★ ★タイムゾーン(DBがJSTかUTCか)の話に手を付けること★ ★★別件★★。★本指示書は toJstIso() の既存の使い方を1文字も変えない★
★9★ ★prismaSource の facilities / schedules / dsDevices / dsAlerts を変更すること★ ★指示書M / P の成果物★。★本作業が触るのは cleanLogs / prechecks の2本だけ★
★10★ ★サマリ系4本(reportKpis / facilityPerformance / errorBreakdown / continuousProblems)/ 点検記録2本に手を付けること★ ★指示書T 以降の範囲★。★notImplemented のまま★
★11★ ★lib/db/prisma.ts / local-guard.ts / use-fixtures.ts / ds-status.ts / precheck-mapping.ts を変更すること★ ★完成しており、そのまま効く★
★12★ ★★lib/fixtures/ の既存の値を変更すること★★ ★CLAUDE.md §9.5★。★★許可するのは型追従(clean_error の導出フィールド追加)だけ★★。★既存の departed / completion_rate / ★status_1h / status_30m★ / 施設名 / 件数を1つも変えないこと★。★★特に :139 / :157 の status_1h: "セキュリティ有効" + departed: false は★新仕様と矛盾しない★ので、「矛盾している」と思って直さないこと★★(→ 段階4-3 の注意書き)
★13★ ★テストを DB 依存にすること★ ★遅く・不安定になり、DB が空の環境で落ちる★。★テストは fixtures と純粋関数だけ★
★14★ ★画面・CSV に clean_error を出すこと★ ★★出し方が未決★★(設計5 / 申し送り)。★components/features/detail-log/ と app/api/clean-logs/export/route.ts に差分を出さない★
★15★ ★npm run dev を Bash で起動すること★ ★Browser pane の preview_start を使う★。★無ければ curl で代替し、その旨を記録する★
★16★ ★.env.local をコミットすること / DB の認証情報を文書やコードに書くこと★ CLAUDE.md §9.3。★確認後は USE_FIXTURES=true に戻す★

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

★段階0(前提の確認。★読むだけ・コミットなし★) → 段階1(lib/config/ のコード表) → 段階2(★期待値表 → テスト → 純粋関数★。呼び出し側は変えない) → 段階3(★★シードと読み出しを同時に切り替える★★) → 段階4(clean_error を API まで運ぶ) → 段階5(実測での検証。★コミットなし★) → 段階6(記録と push)★

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

★★なぜ段階2 と段階3 を分けるのか★★ — ★段階2 で関数を「足す」だけなら挙動が変わらないので、 ★期待値表とテストを落ち着いて作れる★。★逆に、シード(書き込み側)と読み出し側は 同時に変えなければならない★ — ★★シードだけ変えると、13行に clean_error = 1 の履歴ができるのに 読み出しは古い departedFromHistory()(行があれば出発)のままなので、★13行が departed: true / completion_rate: 0 に化ける★★★(落とし穴1)。★tsc / lint / npm test は通ってしまう★ (DB を見ていないため)。 ★参考: 逆向き(読み出しだけ変える)は、★設計3-C のおかげで件数が壊れない★(★履歴が無い13行は notExecutedFromHistory() が true を返すので、departed === false / launch_failed 13件が維持される★)。 ★★それでも同一コミットにすること★★ — ★2/3/4 の検証データが揃わず、段階5 の実測が半分しかできないため★。


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

# 対象 見る点
1 ★docs/handoff/analysis/table-definitions-02-clean-error.md:170-228★ ★clean_statuses(:181)と clean_status_histories(:213)の clean_error の原文★。★ラベルを原文のまま写すため★
2 ★lib/db/clean-log-mapping.ts 全文(197行)★ ★departedFromHistory()(:80)の docstring が何を「推測」と認めているか★ / ★isFutureSchedule()(:113)の docstring★
3 ★lib/db/index.ts:193-320(cleanLogs())★ / ★:360-506(prechecks())★ ★履歴 Map の作り方が2箇所で違うこと(落とし穴2)を自分の目で確認する★
4 ★scripts/seed-local-db.ts:150-230 / :770-830★ ★設計判断1〜4★ と ★履歴・clean_statuses の作り方★
5 ★D-091 項1(docs/decisions.md:3442)/ D-099 §1(:4442)/ D-103 項1(:5222)★ ★「行の有無で判定する」と決めた経緯と、そこで「要確認」と残されていたもの★
6 ★D-104(:5335)★ ★「その他」「不明」を使わない理由★ / ★出発失敗の行の埋め方(★本指示書では変えない★)★
7 ★lib/config/index.ts:1-80★ ★コード表の置き方の作法(CLEAN_TIMETABLE_PRECHECK / ROBOT_STATUS_STANDBY の docstring)★

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

  • ★.env.local の DATABASE_URL が dwatch_local を指していることを確認する★。 ★値そのものを画面・ログ・文書に出さないこと★(CLAUDE.md §9.3)
  • ★.env.local に ALLOW_DIRECT_ACCESS=true があることを確認する★(D-101 項2。 ★無いと proxy.ts が全ルートを止めるので段階5 で詰まる★)
  • ★★着手前の基準値を自分で実測して控えた★★(npm test のファイル数・テスト数 / tsc / lint)
  • ★★DBの現状を実測して控えた★★:
"/c/Program Files/MySQL/MySQL Server 8.4/bin/mysql.exe" -u root --default-character-set=utf8mb4 dwatch_local -e "
SELECT clean_error, COUNT(*) FROM clean_status_histories GROUP BY clean_error;
SELECT COUNT(*) AS main_rows FROM clean_timetables WHERE precheck = 0;
SELECT COUNT(*) AS histories FROM clean_status_histories;"

★作成時点の実測★: clean_error は ★0 が240件のみ★ / main_rows = ★253★ / histories = ★240★。


段階1 — ★lib/config/ に clean_error のコード表を定義する★

1-1. lib/config/index.ts

  • ★CLEAN_ERROR(設計1 のオブジェクト)を追加する★
  • ★ラベル表を追加する★(★原文ママ。半角括弧★)。★形は既存の作法に合わせる★ (ROBOT_STATUS_TONES / MAKER_NAMES のような Record か、CLEAN_ERROR と対になる定数)
  • ★docstring に必ず書くこと★:
# 書く内容
1 ★出所★ — ★docs/handoff/analysis/table-definitions-02-clean-error.md:213(履歴側)/ :181(clean_statuses 側)の原文★
2 ★★1(異常(清掃未実施))だけが出発失敗(未実施)★★(ユーザー回答 2026-09-10)
3 ★2 / 3 / 4 は「未実施とは違う扱い」であり、★どこに出すかは未決★(申し送り)
4 ★ラベルは原文ママ。★言い換え禁止★(D-077 の思想)★
5 ★コード表に無い値は String(code)。★「その他」「不明」に寄せない★(D-089 項3 / D-104 項2)★
6 ★unsigned tinyint(0〜255)なので5以上が入りうる★
7 ★1箇所に閉じ込める理由★ — ★lib/db/(DB→API)と scripts/seed-local-db.ts(fixtures→DB)の

両方が同じ値を必要とする★(ROBOT_STATUS_STANDBY と同じ形) |

1-2. ★ラベルを固定するテスト★

  • ★置き場所★: ★lib/config/__tests__/ が無ければ新設してよい★(★既存の慣例を確認すること★)。 ★lib/db/__tests__/clean-log-mapping.test.ts に混ぜないこと★(★別の関心事★)
  • ★内容★: ★5つのコードに対するラベルが定義書の原文と1文字も違わないこと★ / ★未知の値(例 5 / 255)で String(code) が返ること★ / ★★「その他」「不明」という語がラベル表に出てこないこと★★

この段階の完了条件

  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス
  • ★★挙動が1つも変わっていないこと★★(★まだ誰もこの定数を使っていない★)
  • ★0/1/2/3/4 の直書きが lib/config/index.ts 以外に増えていないこと★
  • ★ここで1コミット★

段階2 — ★期待値表 → テスト → 純粋関数★(★この順序を崩さないこと★)

★CLAUDE.md §6-2 / §9.4★: ★期待値表を先に作り、それをテストで固定してから実装する★。 ★目視確認だけで済ませないこと★。

2-1. ★期待値表(★これをそのままテストにすること★)★

★notExecutedFromHistory(history)★ — ★★この7ケースを必ず全部テストにすること★★

# 入力 期待 意味
1 { clean_error: 0 } false 正常
★2★ ★{ clean_error: 1 }★ ★true★ ★異常(清掃未実施) = 出発失敗★
3 { clean_error: 2 } false ★セキュリティ有効。未実施とは違う扱い★
4 { clean_error: 3 } false ★D-S制御盤異常。同上★
5 { clean_error: 4 } false ★清掃ロボット異常。同上★
★6★ ★{ clean_error: 5 }(★コード表に無い値★)★ false ★未知の値は「未実施ではない」側に落ちる(落とし穴3)★
★★7★★ ★★undefined(★履歴行なし★)★★ ★★true★★ ★★過去なのに結果行が無い=データ収集処理の障害。出発失敗として残す(設計3-C。fail-loud)★★

★completionRateFromHistory(history)★ — ★★同じく7ケース(clean_error 0 / 1 / 2 / 3 / 4 / 未知(5) / 履歴行なし)を必ず含めること★★

# 入力 期待 意味
1 { clean_error: 0, clean_ratio: 92.4 } ★92.4★ ★丸めない生値(CLAUDE.md §4.3)★
1' ★{ clean_error: 0, clean_ratio: 0 }★ ★0★ ★★「0%で実施した」と「未実施」は別物。null にしないこと★★
★2★ ★{ clean_error: 1, clean_ratio: 0 }★ ★null★ ★未実施(落とし穴1 の本体)★
★2'★ ★{ clean_error: 1, clean_ratio: 55 }★ ★null★ ★矛盾したデータでも clean_error を正とする★
3 { clean_error: 2, clean_ratio: 30.5 } 30.5 ★未実施ではないので率を返す★
4 { clean_error: 3, clean_ratio: 0 } 0 同上
5 { clean_error: 4, clean_ratio: 61.2 } 61.2 同上
★6★ ★{ clean_error: 5, clean_ratio: 44.4 }(★未知の値★)★ ★44.4★ ★未実施ではない側に落ちるので率を返す★
★★7★★ ★★undefined(★履歴行なし★)★★ ★★null★★ ★結果が記録されていない★。★★これで不変条件 departed === false ⟺ completion_rate === null が

履歴行なしの場合にも保たれる★★(notExecutedFromHistory(undefined) === true なので departed も false) |

★hasCleanResult(history)★

入力 期待
履歴の行 true
undefined false
★map.get("存在しないキー")★ ★false★(★Map.get の戻りをそのまま渡す使い方を固定する★)

★★上の2表を cleanLogs() の出力に翻訳した早見表(★これも1本テストにしておくと事故に気づける★)★★

clean_error departed completion_rate 不変条件
0 true clean_ratio ✅
★1★ ★false★ ★null★ ✅
2 / 3 / 4 true clean_ratio ✅
未知(5) true clean_ratio ✅
★★履歴行なし★★ ★★false★★ ★★null★★ ✅ ★★(★設計3-C。行を消さずに出発失敗として出す★)★★

★latestHistoryByTimetableId(histories)★

入力 期待
1件 ★キーは clean_timetable_id.toString()★(例 map.get("1001") で引ける)
★同一 timetable 2件(ended_at が異なる)★ ★ended_at が新しい方★
★同一 timetable 2件(ended_at 同着)★ ★入力順の先頭★(★現行の < 比較の挙動を維持する★)
★同一 timetable 2件で ★clean_error が 0 と 1★ で ended_at が違う★ ★★新しい方の clean_error が採られる★★(★どの行を見るかで答えが変わることを固定する★)
別 timetable 2件 2エントリ
空配列 空の Map

2-2. ★実装★

  • ★lib/db/clean-log-mapping.ts に追加する★(★設計2 の #2 / #3 / #4★)。 ★#1(departedFromHistory → hasCleanResult の改称)は段階3 で行う★ (★呼び出し側と同時に変えないとビルドが通らないため★)
    • ★★この段階では #2 / #3 は既存の departedFromHistory() を呼ぶ★★(★中身は history !== undefined で 同じもの★)。★段階3 の改称でそのまま hasCleanResult() に置き換わる★。 ★★history === undefined をインラインで書かないこと★★(★改称のときに直し漏れる★)
  • ★CleanResultLike インターフェースを定義し、docstring に出所(prisma/schema.prisma:479)を書く★
  • ★★0/1 を直書きせず CLEAN_ERROR を使う★★(禁止事項5)
  • ★docstring に必ず書くこと★: ★なぜ「行の有無」ではなく clean_error を読むのか(D-091 項1 の解決)★ / ★★undefined(履歴行なし)を true(出発失敗)にする理由★★(★設計3-C。★過去なのに結果行が無いのは データ収集処理の障害であり、黙って消すより出発失敗として見せる(fail-loud)★)/ ★未実施のとき clean_ratio を返さない理由(落とし穴1)★ / ★履歴を1件に絞る規則を1箇所にした理由(落とし穴2 / D-103)★

この段階の完了条件

  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス(★テスト数が増えていること★)
  • ★★lib/db/index.ts に差分が無いこと★★(★まだ呼び出していない★)
  • ★★挙動が1つも変わっていないこと★★ — ★cleanLogs() 253件 / prechecks() 27件は不変★
  • ★ここで1コミット★

段階3 — ★★シードと読み出しを同時に切り替える(1コミット)★★

★★この段階だけは、シード(書き込み側)と lib/db/(読み出し側)を同じコミットにすること★★。 ★片方だけコミットすると「変えてはいけない数字」が壊れた状態が履歴に残る★。

3-1. ★シード(scripts/seed-local-db.ts)★

# やること
★1★ ★★未実施行(fixtures の completion_rate === null)にも clean_status_histories を作る★★。★:779 の continue を消し、履歴を作る側に回す★
★2★ ★未実施行の clean_error は ★CLEAN_ERROR.notExecuted★(★直書きしない★)★
★3★ ★未実施行の clean_status は ★0(清掃前)★★。★新しい定数(例 CLEAN_STATUS_BEFORE = 0)を :209-210 の隣に定義し、★「★定義書の履歴側の備考には 2 / -1 しか書かれていない(原文)ので、これは当方の暫定値★」とコメントすること★(→ 申し送り / TeamWorks 確認事項)
★4★ ★未実施行の task_area / clean_area / clean_ratio は ★すべて 0★★。★理由をコメントに書く★ — ★「取得元(PUDU Cloud の clean task log)が存在しないため既定値のまま」★。★★併せて「面積から率を再計算する処理は 0 / 0 になるので、先に clean_error を見ること」を書く★★(落とし穴5)
★5★ ★未実施行の started_at / ended_at は ★予定の時刻をそのまま★★(NOT NULL。落とし穴4)。★定義書の原文「清掃スケジュールの清掃開始日時を格納する」を根拠としてコメントに引く★
★6★ ★★既存の完了行に clean_error = 2 / 3 / 4 を散らす★★。★★clean_ratio / clean_area / task_area / clean_status は変えないこと★★(★変えると 253行の grade・連続日数・ダッシュボードが動く★)。★規則は剰余など決定的なものにする(ランダム禁止・冪等)★。★2 / 3 / 4 がそれぞれ3件以上になるようにする★。★13行の未実施行には当てないこと★
★7★ ★clean_statuses(当日分。:812-826)にも同じ clean_error を入れる★。★履歴と clean_statuses で値が食い違わないようにする★。★当日の未実施行にも行ができるので件数が1件増える(clean_statuses は本作業の読み出し対象ではないので画面は変わらない)★
★8★ ★console.log の完了出力に ★clean_error の分布★ を足す★(例 clean_status_histories=253 (0=..., 1=13, 2=..., 3=..., 4=...))。★段階5 の検証が楽になる★
★9★ ★★設計判断1(:157-165)のコメントを書き換える★★ — ★「行を作らないことで未実施を表す」→「clean_error = 1 で未実施を表す」★。★★出所(定義書 :213)とユーザー回答(2026-09-10)を書く★★。★古い記述を残さないこと(次に読む人が両方を信じてしまう)★
★10★ ★冪等であること(npm run seed:local を2回実行して結果が同じ)★

3-2. ★読み出し(lib/db/index.ts / lib/db/clean-log-mapping.ts)★

# やること
★1★ ★departedFromHistory() を ★hasCleanResult()★ に改称し、★docstring を全面的に書き換える★★ — ★★「★行があるから出発した★」という推測の記述を消す★★(★これが本指示書で消したい推測★)。★新しい意味(結果が記録されているか)と、★D-103 の「2箇所に書かない」意図は残す★。★★なお「★行が無いなら出発失敗★」という側は推測ではなく決定として残る★★(設計3-C。★docstring では notExecutedFromHistory() を参照させる★)
★2★ ★cleanLogs(): 履歴 Map の生成を ★latestHistoryByTimetableId(histories)★ に置き換える★(★:221-229 のインライン実装を削除。★ended_at 最新を採る規則とコメントは関数側へ移す★)
★3★ ★★cleanLogs(): mainRows のフィルタは ★isFutureSchedule() の条件のみ★。1文字も変えない★★ — ★★履歴の有無でフィルタを足さないこと★★(設計3-C。★履歴の行が無い過去の本清掃も返す。返さないと出発失敗が消える★)。★isFutureSchedule() が必須である理由(未来の予定は履歴が無いので、外さないと全部出発失敗になる)をコメントに書く★(設計4)
★4★ ★cleanLogs(): departed: !notExecutedFromHistory(history)★
★5★ ★cleanLogs(): completion_rate: completionRateFromHistory(history)★(★落とし穴1★)
★6★ ★prechecks(): 履歴 Map の生成を ★同じ latestHistoryByTimetableId()★ に置き換える★(★new Map(histories.map(...)) をやめる。落とし穴2★)
★7★ ★prechecks() の launch_failed ループの条件を ★★①未来ならスキップ(isFutureSchedule。★残す。必須★)→ ②notExecutedFromHistory() が真なら出す★★ の2段だけにする★。★★「履歴が無ければスキップ」は入れないこと★★(設計3-C。★履歴が無い行は notExecutedFromHistory() が true を返すので、そのまま launch_failed になる★)
★8★ ★★「本清掃行を指すエラー行を launch_failed に読み替えない」という現行の扱いを変えないこと★★(D-104。lib/db/index.ts:402-406)
★9★ ★既存テスト lib/db/__tests__/clean-log-mapping.test.ts:86-105(departedFromHistory の3件)と :111 / :162(isFutureSchedule のテストが同関数を使っている)を追従させる★。★★改称(hasCleanResult)に合わせるだけで、期待値は変わらない★★(undefined → false のまま。★意味が「未出発か」から「結果が記録されているか」に変わる点だけ、テスト名と説明文を直す★)。★★「履歴の行が無い=出発失敗」という結果も変わらない★★ — ★ただしその判定を担う関数が notExecutedFromHistory()(undefined → true)に移ることに注意★。★行番号は自分で取り直すこと★

この段階の完了条件

  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス
  • ★npm run seed:local を2回実行して結果が同じ(★出力の件数が一致★)★
  • ★★DB の実測★★:
"/c/Program Files/MySQL/MySQL Server 8.4/bin/mysql.exe" -u root --default-character-set=utf8mb4 dwatch_local -e "
SELECT clean_error, COUNT(*) FROM clean_status_histories GROUP BY clean_error ORDER BY clean_error;
SELECT COUNT(*) AS histories FROM clean_status_histories;"

★期待★: ★histories = 253★(240 → 253)/ ★clean_error = 1 が 13件★ / ★2 / 3 / 4 がそれぞれ 3件以上★ / ★合計が 253★

  • ★grep -n "clean_error" scripts/seed-local-db.ts lib/db/*.ts で、★数値の直書きが無いこと★を確認した
  • ★ここで1コミット★(★シードと読み出しを同じコミットに入れたこと★)

段階4 — ★clean_error を API まで運ぶ(画面には出さない)★

★実装より先に契約を直す★(CLAUDE.md §6-1)。

4-1. d-watch-api.yaml(★CleanLogItem は :1118-1153★。★行番号は自分で取り直すこと★)

  • ★clean_error を追加★:
    • ★★{ type: integer, nullable: true, example: 0 }★★ — ★★nullable を忘れないこと★★ (★設計3-C により、★履歴(clean_status_histories)の行が無い過去の本清掃も返す★ため。 ★その行には読むべき clean_error が存在しない★)
    • ★description に定義書の原文のコード表を書く★(0:正常 / 1:異常(清掃未実施) / 2:異常(セキュリティ有効) / 3:異常(D-S制御盤異常) / 4:異常(清掃ロボット異常))
    • ★「★値は変換しない生値★」「★★1 または null(履歴の行が無い)のとき departed=false / completion_rate=null になる★★」を書く
    • ★null の意味も書く★: ★「★履歴の行が無い(=清掃結果が記録されていない)★。★出発失敗として扱う★」★
  • ★★facility_id も追加する★★ — ★lib/types/index.ts:59 にはあるのに YAML に無い(実測)★。 ★同じスキーマブロックを触るので、契約と実装の乖離をここで閉じる★(CLAUDE.md §6-1。 ★指示書S が PrecheckItem で同じ抜けを直したのと同じ★)
  • ★completion_rate の description(:1138「未出発時はnull」)は ★★「未実施(clean_error=1)、または履歴の行が無い(clean_error=null)ときnull」に直す★★
  • ★departed の description(:1132)にも ★根拠が clean_error であること★ を書く

4-2. lib/types/index.ts

  • ★★CleanLogItem に clean_error: number | null; を追加★★(★★nullable にする★★ — ★設計3-C により、★履歴の行が無い過去の本清掃も返す★ため。★その行の clean_error は null★。 ★★null の行は departed: false / completion_rate: null(=出発失敗)である★★ことをコメントに書く)
  • ★completion_rate / departed のコメントも clean_error 基準に直す★ (★「clean_error === 1 または clean_error === null のとき departed === false」★)

4-3. lib/fixtures/clean-logs.ts

★★最初に読むこと — fixtures の既存の値は1行も変更しません★★ ★この段階でやるのは ★row() に導出フィールドを1つ足す型追従だけ★ です★。 ★★既存の行の departed / completion_rate / status_1h / status_30m / 施設名 / 件数を 1つも書き換えないこと★★(禁止事項12)。

  • ★row()(:44)で ★clean_error を departed から導出する★★: ★departed ? CLEAN_ERROR.normal : CLEAN_ERROR.notExecuted★(★数値を直書きしない★)
  • ★コメントに書くこと★: ★★fixtures では departed が入力で clean_error が導出、 DB 経由では clean_error が正で departed が導出(★向きが逆★)★★
  • ★★fixtures に 2/3/4 の行は足さない★★ — ★画面への出し方が未決なので、 検証すべき対象がまだ無い★。★2/3/4 はシード(段階3)だけで検証する★。 ★★帰結: USE_FIXTURES の true / false で clean_error の分布が違う(想定内。段階6 で記録する)★★
  • ★fixtures は departed が入力なので、★clean_error が null になる行は fixtures には存在しない★ (null は DB 経由でだけ出る)。★型は number | null だが fixtures 側は常に number★

★★注意 — 「status_1h: "セキュリティ有効" + departed: false は新仕様と矛盾する」は誤り★★

★★lib/fixtures/clean-logs.ts:139 と :157 を「矛盾しているから直す」と考えないこと★★。 ★★矛盾していません。直す必要はありません★★。

カラム 何を表すか 出所
★status_1h / status_30m★ ★★事前チェック時点(60分前 / 30分前)のロボットの状態★★ ★robot_error_reports.error_type 由来★
★clean_error★ ★★その清掃の結果★★ ★clean_status_histories 由来★

★★別のカラム・別の時点の話なので、 「60分前にセキュリティ有効だった → 結果その清掃は実施されなかった(clean_error = 1)」は 新仕様の下でも完全に成立する組み合わせ★★です。 ★むしろ因果としては自然な並び★(★セキュリティが有効なままだったので出発できなかった★)。

★★この誤解は起草時に実際に発生しました★★ — ★同じ勘違いで fixtures を書き換えないよう、 この注意書きを消さないこと★。

4-4. prismaSource.cleanLogs()

  • ★★clean_error: history?.clean_error ?? null★★(★生値をそのまま。★履歴の行が無ければ null★)★を draft に足す
  • ★★?? を || にしないこと★★ — ★clean_error は 0(正常)が正当な値なので、|| だと 0 が null に化ける★ (★tsc も lint も捕まえない。段階5 の「clean_error の分布が DB と完全一致」で初めて分かる★)

4-5. ★型追従が必要な箇所(★自分で grep し直すこと★)★

# 箇所 内容
1 ★lib/clean-logs/__tests__/query.test.ts:6★ row(overrides: Partial<CleanLogItem>): CleanLogItem の土台オブジェクト
2 ★lib/dashboard/__tests__/aggregate.test.ts:24★ CleanLogItem を組み立てるヘルパー
3 ★lib/api/__tests__/client.test.ts:35★ ListResponse<CleanLogItem> の本文
★4★ ★app/api/clean-logs/export/route.ts(toCsv()。:25)★ ★★列を足さない★★(禁止事項14)。★差分が無いことを確認するだけ★
★5★ ★components/features/detail-log/CleanLogTable.tsx★ ★★差分なし★★(禁止事項14)

この段階の完了条件

  • npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス
  • ★d-watch-api.yaml と lib/types/index.ts の内容が一致していること★(★型 / nullable / description★)
  • ★★components/ と app/api/clean-logs/export/route.ts に差分が無いこと★★
  • ★cleanLogs() 253件 / prechecks() 27件が不変(★段階5 で実測する★)★
  • ★ここで1コミット★

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

5-1. ★DB 側★

"/c/Program Files/MySQL/MySQL Server 8.4/bin/mysql.exe" -u root --default-character-set=utf8mb4 dwatch_local -e "
SELECT clean_error, COUNT(*) FROM clean_status_histories GROUP BY clean_error ORDER BY clean_error;
SELECT COUNT(*) AS main_rows FROM clean_timetables WHERE precheck = 0;
SELECT COUNT(*) AS histories FROM clean_status_histories;
SELECT clean_error, COUNT(*) FROM clean_statuses GROUP BY clean_error ORDER BY clean_error;"

★確認する点★:

# 項目
1 ★main_rows(253)と histories(253)が ★一致★ すること★ — ★★これは「段階3 のシードが未実施13行にも履歴を作れたか」の確認★★。★★一致しない場合、差分の行は cleanLogs() から落ちるのではなく、★「出発失敗」として画面に出る★★★(設計3-C。★だから departed === false の件数が 13 を超える★)。★★件数が減っていないことも併せて見ること★★
2 ★clean_error = 1 が 13件★(★「変えてはいけない数字」#2 と同じ★)
3 ★2 / 3 / 4 がそれぞれ3件以上あること★
4 ★clean_statuses 側の clean_error が履歴と食い違っていないこと★

5-2. ★API 側(USE_FIXTURES=false)★

★★.env.local を書き換えず、コマンドラインで env を渡すこと★★(★戻し忘れを防ぐ★):

USE_FIXTURES=false DATABASE_URL="mysql://root@localhost:3306/dwatch_local" npx tsx -e "
import { db } from './lib/db';
const logs = await db.cleanLogs();
const pre = await db.prechecks();
const count = (rows, f) => rows.filter(f).length;
console.log('cleanLogs', logs.length);
console.log('departed=false', count(logs, r => !r.departed));
console.log('rate=null', count(logs, r => r.completion_rate === null));
console.log('不一致(departed=false と rate=null がズレた行)', count(logs, r => !r.departed !== (r.completion_rate === null)));
console.log('clean_error 分布', JSON.stringify(logs.reduce((a, r) => (a[String(r.clean_error)] = (a[String(r.clean_error)] ?? 0) + 1, a), {})));
console.log('clean_error=1 の departed', [...new Set(logs.filter(r => r.clean_error === 1).map(r => r.departed))]);
console.log('clean_error=null の件数', count(logs, r => r.clean_error === null));
console.log('clean_error=null の departed', [...new Set(logs.filter(r => r.clean_error === null).map(r => r.departed))]);
console.log('clean_error=2/3/4 の rate が非null', count(logs, r => [2,3,4].includes(r.clean_error) && r.completion_rate !== null));
console.log('prechecks', pre.length, 'launch_failed', count(pre, p => p.severity === 'launch_failed'));
"

★★期待値(★1つでも外れたら実装が違う★)★★:

# 項目 期待
★1★ cleanLogs() の件数 ★253★
★2★ departed === false ★13★
★3★ completion_rate === null ★13★
★4★ ★departed === false と completion_rate === null のズレ★ ★0★(★落とし穴1 を踏んでいないことの証明★)
★5★ clean_error の分布 ★DB の分布と完全一致(変換していないことの証明)★。★★0 のバケツが null に化けていないこと(?? を ` ` にすると化ける)★★
★6★ clean_error === 1 の departed ★[false] だけ★
★★7★★ ★★clean_error === null の件数★★ ★★0件★★(★シードが253行すべてに履歴を作っているため★)。★★もし 0 でなければ、それは「履歴の行が無い過去の本清掃」であり、★出発失敗として出ている★(設計3-C。★消えていないことが正しい★)。その場合 departed は ★[false] だけ★ であることを確認する★★
★8★ clean_error が 2/3/4 の行の completion_rate ★全件 非null★(★未実施扱いになっていないこと★)
★9★ prechecks() ★27件 / launch_failed 13件★

★npx tsx でエイリアス(@/)が解決できない場合★は ★dev サーバ+curl に切り替えてよい★ (/api/clean-logs?per_page=300 / /api/prechecks)。★★その場合も上の9項目を実測すること★★。

5-3. ★画面(回帰していないこと)★

画面 期待
★/detail-log★ ★200 / 253件★。★✗(未出発)が 13件★
★/dashboard★ ★200 / 9 / 9★(D-099)
★/alerts★ ★200 / 27件(launch_failed 13)★。★「出発失敗」が出ていること★
/facilities / /schedules / /ds-logs / /ds-alerts ★200。回帰していないこと★
/reports ★500(第5歩・指示書T の範囲)★
  • ★エラーメッセージが [db] xxx のPrisma実装はまだありません。… であること★(lib/db/index.ts:164)
  • ★★404 が出たら Edge Runtime バンドル破壊の回帰を疑うこと★★(D-086)。 ★500 = 想定どおりの未実装 / 404 = ビルドが壊れている★
  • ★Browser pane(preview_start)が使えなければ curl で代替し、★目視できなかった項目を必ず記録する★★

5-4. ★USE_FIXTURES=true での確認★

  • ★.env.local を触っていないなら、そのまま USE_FIXTURES=true で dev サーバを起動するだけ★
  • ★/detail-log / /dashboard / /alerts が★従来どおり★動くこと★
  • ★★fixtures 側の件数・判定が1つも変わっていないこと★★(禁止事項12)

この段階の完了条件

  • ★5-1 の4項目・5-2 の9項目をすべて実測した★
  • ★5-3 の回帰確認をした(できなかった項目は理由を控えた)★
  • ★★.env.local が USE_FIXTURES=true のままであることを確認した★★

段階6 — 記録と push

docs/decisions.md に追記

★着手直前に grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md | tail -3 で最終番号を実測すること★ (★^### D-0 だけでは3桁台を拾えない★。★作成時点の最終は D-105。改訂時点で別担当が D-106 (本指示書の判定方針を確定した決定)を追記中なので、★★D-107 の見込み★★★)。

★記録すること★:

# 内容
★1★ ★★clean_error のコード表を根拠に判定を切り替えたこと★★ — ★出所(定義書 :213)とユーザー回答(2026-09-10)の原文★。★★D-091 項1 / D-099 §1 の「実データでどのカラムが未実施を表すかは要確認」が定義書ベースで解決した(実データでの確認は未了)★★
★2★ ★コード表を lib/config/ の1箇所に置いたこと★と、★ラベルを原文ママにした理由★
★3★ ★★履歴の行が無い過去の本清掃を、これまでどおり「出発失敗」として残した判断(設計3 の C案)★★と、★不採用にした A案(行ごと除外)/ B案(departed: true)とその理由★。★★fail-silent より fail-loud を採ったこと★★(★行ごと消すと運用担当が「何かが足りない」ことにすら気づけない★)。★★併せて「不変条件 departed === false ⟺ completion_rate === null は A でも C でも守られるので、A案の根拠にはならない」ことも書く★★(★起草時にこれを A案の根拠にしていた。同じ誤りを繰り返させないため★)
★4★ ★★isFutureSchedule() が C案の下では必須であること★★(★未来の予定には履歴の行が無いので、外すと未来の清掃が全部「出発失敗」になる★)。★加えて、逆向き(未来に clean_error=0 の履歴が先に作られる)の事故も止める二重の役割★
★5★ ★★履歴を1件に絞る規則を latestHistoryByTimetableId() に統一したこと★★ — ★cleanLogs() と prechecks() で規則が違っていた(実測)事実も書く★
★6★ ★★completion_rate が null → 0 に化ける落とし穴(落とし穴1)と、それをどう防いだか★★
★7★ ★シードに足したもの★(未実施13行の履歴 / 2/3/4 の分布 / clean_status = 0 / 面積を0にした理由)
★8★ ★★シードの clean_status = 0 が当方の暫定値であること★★(★定義書の履歴側の備考は 2 / -1 のみ★)
★9★ ★API に clean_error を足したこと★と、★画面・CSV には出していないこと(未決)★。★★nullable にした理由(設計3-C により履歴の行が無い過去の本清掃も返すため)★★
★10★ ★USE_FIXTURES の true / false で clean_error の分布が違うこと(想定内)★。★fixtures には clean_error = null の行が存在しない(departed が入力なので)ことも書く★
★★10'★★ ★★lib/fixtures/ の既存の値は1行も変更していないこと★★と、★★その理由★★ — ★status_1h / status_30m(事前チェック時点のロボットの状態。robot_error_reports.error_type 由来)と clean_error(清掃の結果。clean_status_histories 由来)は別のカラムなので、★status_1h: "セキュリティ有効" + departed: false(lib/fixtures/clean-logs.ts:139 / :157)は新仕様と矛盾しない★。★起草時に「矛盾する」と誤認した経緯ごと記録し、次に読む人が fixtures を書き換えないようにする★
★11★ ★★TeamWorks への確認事項(下記「申し送り」の表をそのまま)★★。★第8回の確認書の候補として記録する★
★12★ ★実測値★(npm test のファイル数・テスト数 / tsc / lint / cleanLogs() / prechecks() の件数。★着手前の基準値との差★)
★13★ ★段階5 で確認できなかった項目があれば、その理由と限界★

docs/handoff.md に追記

  • ★最新は 追記30(docs/handoff.md:2434)★。次は ★追記31★ の見込み。★着手直前に自分で確認すること★
  • ★★冒頭(docs/handoff.md:6-7)の「最新の追記」が 追記29 のまま(実測)★★ → ★★自分の追記に合わせて直すこと★★
  • ★「次に触る人が最初に読むべき点」に必ず書くこと★:
    1. ★★出発失敗の判定は ★clean_error = 1★ または ★履歴の行が無い★ になった★★ (★「行があれば出発した」という推測は消えた。★「行が無ければ出発失敗」は決定として残した★★)
    2. ★★履歴の行が無い過去の清掃は「出発失敗」として画面に出る★★ — ★★消さない★★(★データ収集処理の障害を運用担当に見せるため。fail-loud)★。 ★帰結: 結果がまだ書かれていないだけの行も出発失敗として出る(★過剰検知は承知のうえ★)★
    3. ★★isFutureSchedule() は必須。消さないこと★★(★消すと未来の清掃が全部「出発失敗」になる★)
    4. ★★lib/fixtures/ の status_1h: "セキュリティ有効" + departed: false は新仕様と矛盾しない★★ (★別カラム・別時点。fixtures は書き換えていない★)
  • ★「次にやること」の ★「departed を実データでどう判定するかの確認」(追記30 の4番)★ を ★★本作業の内容に更新する★★(★カラムは決まった。残るのは「未実施でも行が作られるか」の実データ確認★)

push とこの段階の完了条件

  • ★git add は必ずファイルパスを明示。git add -A は禁止★(禁止事項1)
  • ★★docs/handoff/管理画面_設計検討_20260910.md をコミットに含めないこと★★(禁止事項2)
  • ★.env.local をコミットしないこと★(禁止事項16)
  • git push origin develop(★main へは push しない★)
  • ★完了条件★: docs/decisions.md に決定番号を追記した(★上記13項目すべて★)/ docs/handoff.md に追記し★冒頭の最新番号も直した★ / ★origin/develop へ push 済み★ / ★文書末尾の「実行状態」表を更新した★

★★申し送り★★

1. ★未決(★本指示書では実装しない★)★

# 未決事項 状態
★1★ ★★clean_error が 2 / 3 / 4 の行をどこに・どう出すか★★(/detail-log の列を増やす / /alerts に種別を足す / 別画面 など) ★★未決★★。★本指示書は「値を API まで運ぶ」ところで止めている★。★★着手前確認にはしていない(実装を止めないため)★★。★出し方を決めるときは、lib/config/ のラベル表(段階1)をそのまま使えば原文が保たれる★
★2★ ★2(セキュリティ有効)は D-091 項3 の「セキュリティ無効はアラートにしない」という内部調整と関係しうる★ ★★D-091 項3 の結果待ち★★。★先に決めないこと★
★3★ ★出発失敗の行に対応状況(status / comment / responded_at / resolved_at)の置き場が無い★ ★D-104 項3 のまま(★変わっていない★)。★clean_error = 1 の行には robot_error_reports の行が無い★

2. ★★TeamWorks への確認事項(★第8回の確認書の候補★)★★

# 確認したいこと なぜ重要か
★1★ ★★清掃が実施されなかった場合も clean_status_histories の行は作成されますか★★(clean_error = 1 の行) ★★本指示書の設計の土台★★。★★前提が外れても出発失敗が消えることはない(設計3-C)★★が、★代わりに「★結果がまだ書かれていないだけの行★」まで出発失敗として出る(★過剰検知★)★。★fail-loud に倒す判断をしているので、実データでどちらの挙動になるかを確認したい★。★★最優先★★
★2★ ★未実施の行の clean_status には何が入りますか★ ★定義書の clean_status_histories 側の備考は ★2 / -1 のみ★(:212。原文)。★0(清掃前)を使ってよいのか不明★。★当方のシードは暫定で 0 にしている★
★3★ ★★未実施の行の task_area / clean_area は 0 ですか。それとも「予定していた清掃対象面積」が入りますか★★ ★★CLAUDE.md §4.2 の清掃完了率(Σ清掃面積 ÷ Σ清掃対象面積)の分母が変わる★★。★サマリ集計は TeamWorks 実装なので、当方の再計算値と一致するかに直結する★
★4★ ★clean_error が 2 / 3 / 4 のとき、clean_ratio / 面積には何が入りますか★(途中まで清掃した値か、0か) ★/detail-log の完了率と判定(◎○△×)の意味が変わる★
★5★ ★1回の清掃に複数の異常が同時に起きた場合、clean_error はどれになりますか★(優先順位はあるか) ★1カラムに1値しか入らないため★
★6★ ★clean_error に 5以上の値が入る可能性はありますか★ ★unsigned tinyint(0〜255)★。★当方は「1 以外は未実施ではない」として扱っている★
★7★ ★clean_statuses.clean_error(論理名「エラー」)と clean_status_histories.clean_error(論理名「結果」)の使い分けは何ですか★ ★同じコード表だが論理名が違う(原文)★。★当方は履歴側だけを読んでいる★
★8★ ★1つの clean_timetable_id に対し clean_status_histories は必ず1行ですか★ ★複数あると ended_at 最新の1件を採る規則(本指示書で統一)が効く。★その規則が妥当かの裏取りが要る★★

3. ★本指示書では触らないもの★

対象 いつ
★タイムゾーン(DBがJSTかUTCか未確定)★ ★★別件★★(禁止事項8)
★サマリ系4本(reportKpis / facilityPerformance / errorBreakdown / continuousProblems)★ ★指示書T(第5歩)★
★点検記録2本(inspections / inspectionDetail)★ ★最後(D-072 項7)★
★dsAlerts の security_off 側★ ★D-091 項3 の内部調整の結果待ち★
★PATCH /prechecks/{id}/status の API ルート実装★ ★別の指示書★
★docs/decisions.md の既存記述の整理(D-099 §1 の「要確認」の書き換え等)★ ★★本指示書は追記のみ★★。★既存の決定文を書き換えないこと★

完了条件(全体)

  1. ★★着手前確認なし。段階0 から通しで実行した★★
  2. 段階0〜6 をすべて実施した(★途中で止めた場合は、どこで止めたかを実行状態表に書くこと★)
  3. npx tsc --noEmit エラー0 / npm run lint 0 / npm test 全パス(★813テストから増えていること★)
  4. ★★clean_error のコード表とラベルが lib/config/ の1箇所にあり、0/1/2/3/4 が他のどこにも直書きされていないこと★★
  5. ★★ラベルが定義書の原文と1文字も違わないこと(半角括弧を含む)/「その他」「不明」という語が無いこと★★
  6. ★★出発失敗の判定が ★「履歴の行が無い」または clean_error === CLEAN_ERROR.notExecuted★ になり、 cleanLogs() と prechecks() が同じ純粋関数(notExecutedFromHistory())を共有していること★★ (★2項の AND を呼び出し側に書いて規則を2箇所に散らしていないこと★) 6'. ★★cleanLogs() の mainRows フィルタに「履歴があること」の条件が足されていないこと★★ (★履歴の行が無い過去の本清掃も返る=出発失敗として出る。設計3-C★)
  7. ★★履歴を1件に絞る規則が1箇所(latestHistoryByTimetableId())に統一されていること★★
  8. ★★isFutureSchedule() が残っていること★★(★呼び出し2箇所ともそのまま★)
  9. ★★実測: cleanLogs() 253件 / departed === false 13件 / completion_rate === null 13件(ズレ0)/ prechecks() 27件(launch_failed 13)/ /dashboard 9 / 9★★
  10. ★★シードに clean_error = 1(13件)と 2 / 3 / 4(各3件以上)が入り、clean_status_histories が 253行になっていること★★/★2回実行して同じ結果になること★
  11. ★d-watch-api.yaml の CleanLogItem に clean_error(★nullable: true★)と facility_id が定義され、 lib/types/index.ts(★clean_error: number | null★)と一致していること★ 11'. ★★lib/fixtures/ の既存の値に差分が無いこと★★(★row() への導出フィールド追加だけ★。 ★git diff lib/fixtures/ で departed / completion_rate / status_1h / status_30m / 施設名 / 行数が 1つも変わっていないことを確認する★)
  12. ★★components/ / app/api/clean-logs/export/route.ts / prisma/schema.prisma / lib/db/prisma.ts / local-guard.ts / use-fixtures.ts / ds-status.ts / precheck-mapping.ts / prismaSource の facilities / schedules / dsDevices / dsAlerts / サマリ系4本 に差分が無いこと★★
  13. ★docs/handoff/管理画面_設計検討_20260910.md が未追跡のまま残っていること(コミットに入っていない)★
  14. docs/decisions.md / docs/handoff.md(★冒頭の最新番号も★)の更新 / ★origin/develop へ push 済み★ / ★文書末尾の「実行状態」表を更新した★

実行状態

段階 状態 コミット
★着手前の確認★ ★★なし★★ — ★判定に使う値(clean_error = 1)・コード表・ラベルはすべて確定済み★。★未決1件(2/3/4 の出し方)は「値を API まで運ぶ」ところで止めることで回避★ —
段階0 前提の確認 ★★完了★★ コミットなし
段階1 lib/config/ のコード表 ★★完了★★ d744285
段階2 期待値表 → テスト → 純粋関数 ★★完了★★ 703690b
段階3 ★シードと読み出しを同時に切り替え★ ★★完了★★(★同一コミット★) 3bbd504
段階4 clean_error を API まで運ぶ ★★完了★★ 6dfd0b8
段階5 実測での検証 ★★完了★★(★5-1 の4項目 / 5-2 の9項目とも期待どおり★) コミットなし
段階6 記録と push ★★完了★★ 本記録のコミット

実行日: ★★2026-09-10★★ / 取得した決定番号: ★★D-107★★(実測で最終番号が D-106 であることを確認して採番) 実測値(着手前 / 完了後): ★43ファイル / 813テスト / tsc 0 / lint 0★ → ★★44ファイル / 862テスト / tsc 0 / lint 0★★ / ★cleanLogs() 253件(departed === false 13 / completion_rate === null 13 / ズレ0)/ prechecks() 27件(launch_failed 13)/ /dashboard 9 / 9 / clean_status_histories 253行(clean_error 0=225 / 1=13 / 2=5 / 3=5 / 4=5)★ ★決めたこと(実行前に確定済み)★: ★★departed = 履歴の行がある かつ その行の clean_error !== 1★★ / 出発失敗(未実施)の判定 = ★clean_error === 1★ または ★履歴の行が無い★ / 履歴の行が無い場合 = ★★これまでどおり「出発失敗」として残す(★行ごと除外しない★。現行の13件を維持。fail-loud)★★ / isFutureSchedule() = ★残す(★C 案の下では必須★)★ / コード表の置き場所 = ★lib/config/index.ts の1箇所★ / ラベル = ★定義書の原文ママ(未知の値は String(code)。「その他」「不明」は使わない)★ / clean_error = ★2/3/4 も含めて API まで運ぶ(★nullable★。画面・CSV には出さない)★ / lib/fixtures/ = ★★既存の値は1行も変更しない(型追従の導出フィールド追加のみ)★★ ★改訂で反転した点(起草版との差)★: ★起草版は「履歴の行が無い=除外」だったが、★★「出発失敗として残す」に反転★★★ / ★起草版が持っていた「fixtures が新仕様と矛盾する」という前提は★誤り★として撤回(fixtures は無変更)★ / ★決定番号を D-106 → D-107 にずらした★ 詳細: docs/decisions.md の該当番号を参照