continuousProblems)を実DBから作る| 項目 | 内容 | ||
|---|---|---|---|
| 実行環境 | ★Cursor 内の Claude が実装します★(.claude/commands/d-watch.md の役割分担表を参照) |
||
| 実行状態 | ★完了(D-116。2026-09-11)★ | ||
| 実行順 | ★指示書V(D-110)完了後★。★指示書M / P / R / S / T / U / V が作った lib/db/clean-log-mapping.ts / lib/db/report-mapping.ts / lib/db/index.ts の上に積む作業★です。★★prisma/schema.prisma と lib/fixtures/ は1文字も変えません★★が、★★d-watch-api.yaml / lib/types/index.ts / lib/i18n.ts / 画面(components/features/reports/ReportsScreen.tsx)は変えます★★。★触るコードが重なる未実行の指示書は着手直前に自分で確認すること★ |
||
| 対象 | ★prismaSource.continuousProblems()(lib/db/index.ts:1115-1117 の notImplemented("continuousProblems"))の実装1本★。★★これが片付くと監視側の DataSource は全部埋まります★★(inspections / inspectionDetail は点検記録で後回し。★D-111 項6★) |
||
| 取得する決定番号 | ★**着手直前に `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-112**(docs/decisions.md:6706)なので、本書の実行記録は **D-113** の見込み★。★それでも必ず自分で実測してから採番すること★。★★作成時点で **D-111とD-112が未コミット**(git diff docs/decisions.mdに出る。合計256行)★★ — ★★git add docs/decisions.md` は2件ごと巻き込みます★★(段階9-3 参照) |
| 前提 | ★D-112(docs/decisions.md:6706。★TeamWorks 第7回の回答受領★)★ / ★★D-111(docs/decisions.md:6584。★summary は事実だけを組み立てる。原因も助言も書かない★。★★ただし同 項2 の決定4「混在は出発失敗を優先する」は本書で C案に差し替わりました(後述「ユーザー確定」)★★)★★ / ★D-026(:122。★「連続」はカレンダー日ではなくスケジュール存在日の連続★)★ / ★D-102(:5139。★項4 が「continuousProblems は対応するサマリが無い。詳細テーブルから算出する必要がある(第6歩)」と明記している★)★ / ★D-103(:5222。consecutive_problem_days はA案 = computeConsecutiveProblemDays() を流用する / 規則を2箇所に書かない)★ / ★D-106(:5830。★1週間分の予定がまとめて登録される=未来の本清掃行が常時存在する★)★ / ★D-107(:5993。★isFutureSchedule() は必須★)★ / ★D-109(:6328。★連続日数は施設単位・同一日は OR で1日に畳む・70.0 は問題あり★)★ / ★D-110(:6447。指示書V の実行記録)★ / D-042(:226。書き込み許可4カラム)/ D-050(:308。段階ごとに検証してからコミット)/ D-056(:353。schema.prisma は生成物)/ D-072 項4(:864。年次は P1 では読まない)/ D-077(:1847。エラー値を変換しない)/ D-089 項3・D-104(★「その他」「不明」という語を使わない★)/ D-071(月跨ぎ週は月境界で分割する) |
||
| 段階数 | ★10段階★(段階0〜段階9)。★コミットは段階1 / 2 / 3 / 4 / 5 / 6 / 7 / 9 の8つ★ | ||
| ★着手前確認★ | ★★なし★★ — ★判定の分かれ目(summary の文面3種 / 混在ランの書き方(C案)/ △と× を畳むこと / 確定期間も詳細から算出すること / i18n はテンプレート方式 / L1 もデータ層は返すこと / ロボット0台の施設を除外しないこと / 連続0日は行を作らないこと)は ★★すべて確定済み★★(ユーザー確定 2026-09-11 + 監督判断)。★★ユーザーの回答を待たずに段階0 から通しで実行してよい★★ | ||
| 着手前の基準 | ★着手直前に自分で npm test / npx tsc --noEmit / npm run lint を実行し、その実測値を基準にすること★。★作成時点の実測(2026-09-11。★鵜呑みにしない★): npm test = 45ファイル / 928テスト(全パス) / tsc = 0 / lint = 0 / db.cleanLogs() = 259件 / db.prechecks() = 27件★ |
||
| 作成 | 2026-09-11(作成時点の決定事項最終番号 D-112(docs/decisions.md:6706)/ docs/handoff.md の最新は 追記33(docs/handoff.md:2774)。★着手時に実測すること★) |
実行後はこの表と、文書末尾の「実行状態」表を更新してください。
★この指示書を読んでいるあなたへ★ —
.claude/commands/d-watch.mdの「実装は Cursor 側」は 2つの環境の役割分担です。Cursor の中で動いている Claude は実装担当です。 ★調査や改訂ではなく実装を行ってください。★ ★★うまくいかなければ段階の切れ目(コミット単位)で止めて構いません★★ — ★段階1(API契約)→ 段階2〜5(純粋関数を1本ずつ)→ 段階6(配線)→ 段階7(画面)★ の順に、 ★どこで止めても単体で意味が通る形★に並べてあります。 ★★ただし段階6 まで行かないと画面は 500 のままです★★(=現状と同じ。壊れはしません)。
prismaSource.continuousProblems()(lib/db/index.ts:1115)は ★★監視側の DataSource で最後に残った 未実装1本★★です。★対応するサマリテーブルが無い★(D-102 項4)ので、 ★★確定期間(先週・先月)も当期(今週・今月)も、どちらも詳細テーブルから算出します★★。
★★返すもの★★(ContinuousProblem。lib/types/index.ts:258):
| フィールド | 作り方 |
|---|---|
facility_id / facility_name |
★facilities(deleted_at: null)から引く。★robots を join しない★(後述 決定5)★ |
consecutive_problem_days |
★computeConsecutiveProblemDays() + consecutiveProblemDaysAtPeriodEnd()★(★既存関数。別実装しない★) |
escalation_level |
★judgeEscalationLevel()★(★0日は null を返す=行を作らない★) |
last_problem_on |
★期間内でその施設の最終エントリの日付★(★consecutiveProblemDaysAtPeriodEnd() の内部から切り出す★) |
summary |
★★下の3種のテンプレート文字列のいずれか★★(★数値を埋めずに {n} {m} のまま返す★) |
★launch_failed_days(新設・任意項目)★ |
★ランのうち出発失敗(未実施)があった日数★。★summary の {m} に画面が差し込む★ |
★★summary の3種(これが仕様のすべて)★★:
| # | 条件 | ★戻り値(テンプレートのまま)★ |
|---|---|---|
| ★1★ | ★ランに出発失敗の日が1日も無い★ | ★"{n}日連続で完了率が△以下"★ |
| ★2★ | ★ランの全日が出発失敗★ | ★"{n}日連続で出発失敗"★ |
| ★3★ | ★混在(C案)★ | ★"{n}日連続で△以下(うち{m}日は出発失敗)"★ |
★★この節は確定済みです。★★ ★迷ったらこの節に戻ること★。★★本書の他の記述とこの節が食い違っていたら、この節が正★★。
★対処の助言も原因の推定も書かない★。 ★fixtures の summary(lib/fixtures/reports.ts:339-437。「マップ再作成またはオンサイト訪問を検討」等)は 再現しません★ — ★★USE_FIXTURES の true / false で文言が違うのは想定内★★(D-111 項5。 前例は status_1h の日本語 ↔ Standby.(D-098 / D-105))。 ★★見た目の差分をもって「実装がおかしい」と判断しないこと★★。
出発失敗の日と完了率低下の日が混ざった連続では:
{n}日連続で△以下(うち{m}日は出発失敗)
★★却下した2案と理由(記録に残すこと)★★:
| 案 | 内容 | ★却下理由★ |
|---|---|---|
| ★(a)★ | ★「{n}日連続で出発失敗」(=D-111 項2 の決定4「出発失敗を優先する」)★ | ★★直近に清掃を実施していても「出発失敗」と書くことになり事実と食い違う★★(=確定1「事実だけを組み立てる」に反する) |
| ★(b)★ | ★「{n}日連続で完了率が△以下」★ | ★★出発失敗を隠してしまう★★ |
★★実データに2件実在します★★: ★施設5(2日中1日が出発失敗)★ / ★施設6(4日中3日が出発失敗)★。 ★★つまり「混在は起きない」という前提で書いてはいけません★★。
★1つの連続の中で △ と × が混ざるため★ — ★★実測: 施設9 は5日連続で△のあと、最終日だけ 9.9% で ×★★。 ★モックの副題(d-watch-mock.jsx:713「2日以上連続で△以下または出発失敗の施設」)とも一致します★。
★★帰結★★: ★judgeGrade() は今回使わなくてよくなる可能性が高い★(△と× を分けないため)。 ★使うかどうかは実装者判断★。★使う場合も丸める前の生値で呼ぶこと★(CLAUDE.md §4.3)。
CLAUDE.md §6 作業ルール1)★★★現在の d-watch-api.yaml:1324-1329 は summary を「別物」として定義しています★★:
summary:
type: string
example: "スケジュール存在日ベースで3日連続。カレンダー上は5日間"
description: >-
連続判定の内容を説明する文。完了率の数値ではなく、
D-026(スケジュール存在日ベースの連続)の説明として画面に出す
★これは「D-026 の説明を画面に出す」という定義★で、★★確定1・確定2 のA案/C案とは中身が違います★★。 ★★description と example を先に書き換えてから実装に入ること★★(=段階1)。 ★実装だけ変えて YAML と乖離させない★(CLAUDE.md §6-1 / §7)。
todayJst() / isCurrentPeriod() を使った当期の場合分けを書かないこと★★ — ★期間キーはデータ(清掃ログの日付)から列挙するだけ★(段階4)lib/i18n.ts:100("他{n}件を表示 →")/ :163("{n}分前")/ :175("{n}日以上連続")。 ★lib/i18n.ts:85-99 のコメントが「方式A(プレースホルダ + replace)★推奨★」と明記している★ReportsScreen.tsx:394 は t() を通していないので、★そこも直す★){m} を使う C案 は replace が2回★★。★プレースホルダ名を分けてあるので String.replace で足ります★ (String.replace は文字列指定だと最初の1件しか置換しないが、{n} も {m} も各テンプレートに1回しか出ない)★★起草時に補った点(★実装者はこの形で作ること★)★★: ★{m} に入れる数値の出どころが ContinuousProblem に無かった★ため、 ★★launch_failed_days(任意項目)を API契約と型に足します★★。
lib/fixtures/reports.ts:339-437 の12件が 型エラーになり、★★lib/fixtures/ を変更しない★★(禁止事項)を守れなくなる★prismaSource は必ず数値を入れる★★(0 を含む)。★任意なのは fixtures を凍結しているためだけ★launch_failed_days が undefined のとき {m} の置換を行わない★ (★?? 0 で 0 を捏造しない★。fixtures の summary にはそもそも {m} が無いので置換不要)lib/reports/query.ts:236-242 の filterContinuousProblems() に実装済み★ (閾値は ESCALATION_THRESHOLDS.level2Days。★ハードコードされていない★)lib/reports/query.ts と app/api/reports/continuous-problems/route.ts は1文字も変えないこと★★facilityPerformance()(lib/db/index.ts:883-898)は ★ロボット0台の施設を行ごと除外している★ が、 ★★あちらは FacilityPerformance.serial_number を出すため★★(lib/types/index.ts:249)ContinuousProblem に serial_number は無い★★(lib/types/index.ts:258-265)ので ★join 自体が不要★prisma.facility.findMany({ where: { deleted_at: null } }) に include: { robots: ... } を書かないこと★★judgeEscalationLevel()(lib/judge/escalation.ts:52-59)は ★0日で null を返す★ContinuousProblem.escalation_level は ★non-null★(EscalationLevel)as EscalationLevel でのキャストや ?? "L1" での握り潰しを禁止★★ — ★0日は「問題が続いていない」であって L1(初回)ではない★。 ★★null が返ったら行を作らない(continue)★★| 関数 | 場所 | 役割 |
|---|---|---|
★computeConsecutiveProblemDays()★ |
lib/db/clean-log-mapping.ts:366 |
★行id → 連続日数(施設単位・同一日OR畳み込み済み)★ |
★foldProblemDaysByDate()★ |
lib/db/clean-log-mapping.ts:322 |
★同一日 OR 畳み込み(D-109 決定2)★ |
★consecutiveProblemDaysAtPeriodEnd()★ |
lib/db/report-mapping.ts:282 |
★期間内最終エントリの連続日数(A案。D-103)★ |
judgeEscalationLevel() |
lib/judge/escalation.ts:52 |
L1〜L3。★0日は null★ |
latestHistoryByTimetableId() |
lib/db/clean-log-mapping.ts:214 |
履歴を1件に絞る規則(D-107 落とし穴2) |
completionRateFromHistory() |
lib/db/clean-log-mapping.ts:184 |
★未実施は null・丸めない生値★ |
isProblemCompletion() |
lib/db/clean-log-mapping.ts:281 |
★△以下 or 未実施(70.0 は問題あり。D-109 決定3)★ |
★isFutureSchedule()★ |
lib/db/clean-log-mapping.ts:263 |
★★外すと未来7日ぶんが全部「出発失敗」になり、全施設が常時L3になります★★ |
weekOfMonth() / periodRange() |
lib/format/period.ts:126 / :157 |
★月跨ぎ週の帰属を知っているのは weekOfMonth() だけ(D-071)★ |
toJstIso() |
lib/format/date.ts |
★既存の使い方を1文字も変えない★(タイムゾーンは別件。D-106 項8) |
★judgeGrade()★ は ★確定3(△以下に畳む)★ のため今回は不要になる可能性が高い。★実装者判断★。
| # | 何 | なぜ既存で足りないか | ★置き場所★ |
|---|---|---|---|
| ★1★ | ★期間内の「最終エントリ」を返す関数★ | ★consecutiveProblemDaysAtPeriodEnd()(lib/db/report-mapping.ts:282-297)が内部で選んでいるが、★日数しか返さない★。last_problem_on に日付が要る★ |
★lib/db/report-mapping.ts★(★切り出す。★同関数のシグネチャは変えない★) |
| ★2★ | ★ランの範囲(日付配列)を返す関数★ | ★foldProblemDaysByDate() の戻りから「末尾 n 日」を採る★ |
★lib/db/report-mapping.ts(内部で foldProblemDaysByDate() を呼ぶ)★ |
| ★3★ | ★ラン内の「出発失敗の日数」を返す関数★ | ★★foldProblemDaysByDate() は {date, isProblem} しか返さず、未実施 / × / △ の別を落としている★★ |
★同上★ |
| ★4★ | ★期間キーの列挙★ | ★facilityPerformance() はサマリ行から期間キーを列挙している(lib/db/index.ts:906-913)。★詳細行から列挙するヘルパは無い★ |
★lib/db/report-mapping.ts★ |
| ★5★ | ★1期間ぶんの ContinuousProblem[] を組み立てる関数★ |
★★DB に触らない形にしておかないと「連続0日は行を作らない」等をテストできない★★(禁止事項「テストを DB 依存にしない」) | ★同上★ |
ScheduleDayResult(lib/judge/escalation.ts:19-24)と foldProblemDaysByDate() の戻り型を 拡張しないこと★★ — ★lib/db/__tests__/clean-log-mapping.test.ts:620-700 が toEqual で 完全一致比較しており、フィールドを1つ足すだけで全部落ちます★。 ★★区別が要るなら、畳み込みの結果とは別に「元の行」を見ればよい★★(新関数2・3 はそうしてある)consecutiveProblemDaysAtPeriodEnd() のシグネチャを変えないこと★★ — ★lib/db/__tests__/report-mapping.test.ts:400-440 が依存している★。 ★★中身を新関数1 に置き換えるのは可(引数と戻り値が同じなら既存テストはそのまま通る)★★| 事象 | ★判定★ |
|---|---|
| ★出発失敗(未実施)★ | ★★completion_rate === null★★(lib/db/clean-log-mapping.ts:176-181 の不変条件: departed === false ⟺ completion_rate === null) |
| ★完了率が△以下★ | ★それ以外で isProblemCompletion() が true★(★× と △ を分けない。確定3★) |
| ★同じ日に「未実施の行」と「実施した行」が混在★ | ★★その日は「出発失敗の日」として1日ぶんだけ数える★★(OR。★D-109 決定2 の畳み込みと同じ規則にそろえる★。★同じ日を2回数えない★) |
CLAUDE.md §4.3)。 ★Math.round(9.9) = 10 で × が △ に化ける★ — ★今回は△と× を畳むので文面は変わらないが、 ★★70.0 前後では判定がひっくり返る★★(isProblemCompletion() は <= 70。70.4 を丸めた 70 と 70.6 を丸めた 71 で結果が変わる)。★丸めた値を判定に渡さないこと★m ≤ n が必ず成り立つこと★★(ランの日だけを数えるため)。★ランの外の未実施日を数えない★| # | ★してはいけないこと★ | 理由 |
|---|---|---|
| ★1★ | ★★git add -A を使うこと★★ |
CLAUDE.md §7。★過去に別作業の途中ファイルを巻き込む事故が起きている★。★★必ずファイルパスを明示すること★★ |
| ★2★ | ★★README.md / docs/code-map.md / docs/handoff/管理画面_設計検討_20260910.md / `.claude/worktrees/` に触ること**★★ |
★★すべて別作業のもの★★。★本指示書のコミットに含めない・編集しない・削除しない★ |
| ★3★ | ★★メインDBのスキーマ変更(prisma migrate / db push / db pull / prisma/schema.prisma の手書き)★★ |
CLAUDE.md §7 / D-056。★★本書はスキーマ変更を1つも必要としません★★(★シードにも1行も足しません★) |
| ★4★ | ★★書き込み許可4カラム(status / comment / responded_at / resolved_at)以外への DB 書き込み★★ |
D-042。★★本書は読むだけです。検証でも DB に1行も書きません★★(★指示書V 段階6-2 のような一時 INSERT はしない★) |
| ★5★ | ★★閾値(90/70/10、猶予30分、L2=2日/L3=3日)をハードコードすること★★ | CLAUDE.md §7。★lib/config/ から読む★。★★summary の文面に「2日以上」等の閾値を焼き込まないこと★★ |
| ★6★ | ★★表示用に丸めた値で判定すること★★ | CLAUDE.md §4.3 |
| ★7★ | ★★isFutureSchedule() を外すこと・猶予を足すこと・境界(>)を変えること★★ |
D-107 設計4 / D-110。★★外すと未来7日ぶんが全部「出発失敗」になり、全施設が常時L3になります★★ |
| ★8★ | ★★ScheduleDayResult / foldProblemDaysByDate() の戻り型を変えること★★ |
★lib/db/__tests__/clean-log-mapping.test.ts:620-700 が toEqual で完全一致比較している★ |
| ★9★ | ★★consecutiveProblemDaysAtPeriodEnd() のシグネチャを変えること★★ |
★lib/db/__tests__/report-mapping.test.ts:400-440 が依存している★ |
| ★10★ | ★★lib/fixtures/ を1行でも変更すること★★ |
CLAUDE.md §9.5 / D-111 項5。★fixtures の summary は人間が書いた文章で、実装では再現しない★ |
| ★11★ | ★lib/reports/query.ts / app/api/reports/continuous-problems/route.ts を変更すること★ |
★★「2日以上」の絞り込み・並べ替え・meta は既に実装済みで正しい★★(決定4)。★データ層でも絞ると規則が2箇所になる(D-103)★ |
| ★12★ | ★lib/judge/ を変更すること★ |
★countConsecutiveProblemDays() / judgeEscalationLevel() / judgeGrade() / lib/judge/advice.ts は完成している★ |
| ★13★ | ★prismaSource の他メソッド(cleanLogs / prechecks / reportKpis / facilityPerformance / errorBreakdown / D-Sec 系)の挙動を変えること★ |
★範囲外★。★★段階2 のリファクタは「内部の切り出し」だけで、facilityPerformance() の値が1つでも変わったら実装が違います★★(段階8 で突き合わせる) |
| ★14★ | ★エラー種別を7分類に変換すること・「その他」「不明」という語を使うこと★ | D-077 / D-089 項3 / D-104 項2。★★summary に原因(オフライン / ブラシエラー等)を書かないこと★★(確定1) |
| ★15★ | ★summary に対処の助言を書くこと★ | D-111 項2 の決定2・決定3。★★「現場へ連絡」「オンサイト訪問を検討」は書かない★★(アドバイス R1〜R4 と二重になる) |
| ★16★ | ★summary を「完成した日本語文」で返すこと★ | 決定3。★★EN 切替でこのカードだけ和文が残ります★★ |
| ★17★ | ★テストを DB 依存にすること★ | ★遅く・不安定になり、DB が空の環境で落ちる★。★テストは純粋関数と手書きの入力だけ★ |
| ★18★ | ★.env.local をコミットすること / DB の認証情報を文書やコードに書くこと★ |
CLAUDE.md §9.3。★検証後は USE_FIXTURES=true に戻す★ |
| ★19★ | ★★DBに触る一時スクリプトに process.exit(0) を書かないこと★★ |
★★D-111 項9。main().catch(...) だけだと Prisma の接続が残り、★実際に4時間39分ハングした事故★が起きている★★ |
| ★20★ | ★npm run dev を Bash で起動すること★ |
★Browser pane の preview_start を使う★。★無ければ next build && next start -p <別ポート> か curl で代替し、その旨を記録する★(★dwatch_local と .env.local は複数セッションで共有。D-108 項8★) |
★段階0(前提の確認。★読むだけ・コミットなし★) → 段階1(★API契約★: d-watch-api.yaml → lib/types/index.ts) → 段階2(純粋関数①: 期間内最終エントリの切り出し) → 段階3(純粋関数②③④: ランの範囲・出発失敗日数・summary テンプレート) → 段階4(純粋関数⑤: 期間キーの列挙) → 段階5(純粋関数⑥: 1期間ぶんの ContinuousProblem[] 組み立て) → 段階6(★配線★: prismaSource.continuousProblems()) → 段階7(画面 + i18n) → 段階8(実測での検証。★コミットなし★) → 段階9(記録と push)★
★★段階1・2・3・4・5・6・7・9 でそれぞれ1コミット(計8コミット)★★。 ★段階ごとに npx tsc --noEmit / npm run lint / npm test を実行し、通ってからコミットすること★ (D-050。docs/decisions.md:308)。
★★期待値表 → テスト → 実装 の順序を崩さないこと★★(
CLAUDE.md§6-2 / §9.4)。 ★★各段階のX-1(期待値表)を先に書き、X-2(テスト)で固定し、落ちることを確認してからX-3(実装)★★。 ★特に段階3 は、実装を先に書くと「自分の実装がそのまま期待値になる」ため、表を先に埋めること★。★★なぜこの順番なのか★★
- ★段階1 が先頭なのは
CLAUDE.md§6 作業ルール1「API契約を変えたくなったら先に YAML を直す」★- ★段階2〜5 で純粋関数を1本ずつ積むのは、★★段階6 で配線したときに「どの関数が間違っていたか」を テストで切り分けられるようにするため★★。★段階6 は DB を読むのでテストが書けない★
- ★段階7(画面)を段階6 の後に置くのは、★★実データが出るようになってから見た目を直したいため★★
- ★段階8(実測)を最後に置くのは、★★期待値(4件・0件・施設5=2日 等)が全段階の積み上げでしか出ないため★★
| # | 対象 | 見る点 |
|---|---|---|
| 1 | ★CLAUDE.md 全文★ |
★★§4.3(丸め)/ §4.5(R1〜R4・L1〜L3)/ §6(API契約が先・期待値表が先)/ §7(禁止事項)/ §9.4(判定ロジックの作法)★★ |
| 2 | ★docs/decisions.md の D-111(:6584)★ |
★★summary の生成規則(A案)★★。★★ただし 項2 の決定4「混在は出発失敗を優先」は本書の確定2(C案)で差し替わる★★。★項9(一時スクリプトの process.exit(0))も読む★ |
| 3 | ★D-026(:122)/ D-102(:5139。★項4★)/ D-103(:5222)/ D-106(:5830)/ D-107(:5993)/ ★D-109(:6328)★ / D-110(:6447) |
★スケジュール存在日の連続 / サマリが無いこと / A案 / 1週間分の予定 / isFutureSchedule() / 施設単位と同一日OR / 指示書V の実行記録★。★行番号は自分で取り直すこと★ |
| 4 | ★docs/handoff.md の末尾(★追記33★。:2774)★ |
★指示書V の完了内容と申し送り★。★「次にやること」1番が本書★ |
| 5 | ★docs/handoff/指示書V_連続日数を施設単位に揃え当期の分母から未来を外す.md★ |
★段階の切り方・期待値表の書き方・記録の作法★ |
| 6 | ★lib/db/clean-log-mapping.ts 全文(392行)★ |
★isFutureSchedule()(:263)/ isProblemCompletion()(:281)/ ConsecutiveInput(:286)/ foldProblemDaysByDate()(:322)/ computeConsecutiveProblemDays()(:366)★ |
| 7 | ★lib/db/report-mapping.ts 全文(389行)★ |
★FacilityConsecutiveEntry(:261)/ consecutiveProblemDaysAtPeriodEnd()(:282)/ periodMapKey()(:31)★ |
| 8 | ★lib/db/index.ts:874-1117★ |
★facilityPerformance() の組み立て方(★これが本書のひな形★)★ / ★continuousProblems()(:1115)★ / ★施設マスタの引き方(:883-898。★本書は robots を join しない★)★ |
| 9 | ★lib/judge/escalation.ts 全文★ |
★★countConsecutiveProblemDays() が日付を見ていないこと★★ / ★judgeEscalationLevel() が0日で null を返すこと★。★★どちらも変更しない★★ |
| 10 | ★lib/reports/query.ts:232-254 / :303-308★ |
★「2日以上」の絞り込みと並べ替えが既に実装済みであること★。★★変更しない★★ |
| 11 | ★app/api/reports/continuous-problems/route.ts 全文★ |
★meta.total の意味(2日以上の絞り込み後・limit 適用前)★。★★変更しない★★ |
| 12 | ★components/features/reports/ReportsScreen.tsx:358-418 / ReportsScreen.module.css:251-257★ |
★カードの描画(:394 が t() を通していない)★ / ★.contSummary に省略記号の指定が無いこと(申し送り②)★ |
| 13 | ★lib/i18n.ts:85-101 / :160-176★ |
★方式A の前例と書き方★ |
| 14 | ★lib/db/__tests__/clean-log-mapping.test.ts:600-700 / lib/db/__tests__/report-mapping.test.ts:380-450★ |
★既存テストの書き方(ヘルパー)に揃えるため★ / ★★変えてはいけない toEqual 比較の場所を自分の目で確認する★★ |
npx tsc --noEmit npm run lint npm test
★作成時点の実測(2026-09-11。★鵜呑みにしない★)★: tsc = 0 / lint = 0 / npm test = ★45ファイル・928テスト★
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();
console.log('cleanLogs', logs.length, 'prechecks', pre.length);
const fp = await db.facilityPerformance();
console.log(JSON.stringify(fp.map(r => [r.period, r.year, r.month, r.week,
r.item.facility_id, r.item.consecutive_problem_days, r.item.status])));
process.exit(0); // ★★必ず書くこと(D-111 項9)★★
"
★作成時点の実測★: cleanLogs = ★259件★ / prechecks = ★27件★。 ★★facilityPerformance() の出力は丸ごとファイルに控えておくこと★★ (★段階2 のリファクタで1つでも変わったら実装が違う★)。
.env.local の DATABASE_URL が dwatch_local を指していることを確認する★。 ★値そのものを画面・ログ・文書に出さないこと★(CLAUDE.md §9.3).env.local に ALLOW_DIRECT_ACCESS=true があることを確認する★(D-101 項2)grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md | tail -3)。 ★最終が D-112 なら本書の記録は D-113★d-watch-api.yaml の ContinuousProblem(:1300-1329)を直す★★summary の description と example を差し替え、launch_failed_days を足す★★。 ★他のフィールド(facility_id / facility_name / consecutive_problem_days / escalation_level / last_problem_on)と、スキーマ冒頭の description(「L1(初回)は返さない(2日以上のみ)」)は 変えないこと★ — ★★API の応答としては今も L1 を返さない(絞り込みはルートが行う)ので、記述は正しい★★。
★書き換え後の形(★このとおりに★。既存の記法・インデントに合わせる)★:
summary:
type: string
example: "{n}日連続で△以下(うち{m}日は出発失敗)"
description: >-
連続の内容を事実だけで述べたテンプレート文字列(D-111)。原因の推定も対処の助言も含まない。
画面が {n} に consecutive_problem_days を、{m} に launch_failed_days を差し込む(方式A)。
サーバは数値を埋めずにテンプレートのまま返す(完成した日本語文を返すと EN 切替で和文が残るため)。
返す文字列は次の3種のいずれか:
出発失敗の日が無いとき「{n}日連続で完了率が△以下」/
全日が出発失敗のとき「{n}日連続で出発失敗」/
混在するとき「{n}日連続で△以下(うち{m}日は出発失敗)」。
△ と × は書き分けず「△以下」に畳む(1つの連続の中で混ざるため。モック:713 の副題と同じ粒度)
launch_failed_days:
type: integer
example: 3
description: >-
連続の中で出発失敗(未実施)があった日の数。0 <= launch_failed_days <= consecutive_problem_days。
summary の {m} に差し込む。同じ日に未実施の行と実施済みの行が混在する場合は、その日を
出発失敗の日として1日だけ数える(D-109決定2 の同一日OR畳み込みと同じ規則)。
★任意項目★: fixtures(手書きの完成文)を変更しないための措置であり、
実DB経由(prismaSource)では常に数値が入る
lib/types/index.ts:258-265 の ContinuousProblem に足す /**
* 連続の中で出発失敗(未実施)があった日の数(summary の {m})。
* ★任意項目にしてあるのは `lib/fixtures/reports.ts` の手書き12件を変更しないため★
* (`CLAUDE.md` §9.5 / D-111 項5)。★`prismaSource` は常に数値を入れる(0 を含む)★。
*/
launch_failed_days?: number;
★summary のコメントも「テンプレート文字列である」ことが分かるように直すこと★ (★型を読んだ人が完成文を入れないように★)。
npx tsc --noEmit = 0 / npm run lint = 0 / npm test 全パス(★件数は段階0 と同じ★)★lib/fixtures/ に差分が無いこと★★(=任意項目にできている)git diff --stat が d-watch-api.yaml と lib/types/index.ts の2ファイルだけ★git commit -m "docs/feat: 指示書W 段階1 ContinuousProblem の summary 契約を事実ベースに直す"★対象★: lastEntryInPeriod(entries, facilityId, range) → FacilityConsecutiveEntry | undefined
| # | 入力 | ★期待★ |
|---|---|---|
| ★1★ | entries = [] |
★undefined★ |
| ★2★ | その施設の行が1件も無い(別施設の行だけ) | ★undefined★ |
| ★3★ | その施設の行はあるが全部範囲外(from の前日 / to の翌日) |
★undefined★ |
| ★4★ | 範囲内に3件(日付バラバラ) | ★scheduled_at が最大の1件★ |
| ★5★ | ★同じ日に2件(時刻違い)★ | ★★時刻が遅いほう★★(scheduled_at の文字列比較。既存実装と同じ) |
| ★6★ | ★境界: from ちょうどの行だけ★ |
★その行(範囲に含む)★ |
| ★7★ | ★境界: to ちょうどの行だけ★ |
★その行(範囲に含む)★ |
| ★8★ | 範囲内と範囲外が混在(範囲外のほうが新しい) | ★★範囲内の最新★★(範囲外を拾わない) |
★★加えて、consecutiveProblemDaysAtPeriodEnd() の既存テスト (lib/db/__tests__/report-mapping.test.ts:400-440)が1文字も変えずに通ること★★。
lib/db/__tests__/report-mapping.test.ts に追記する★(新規ファイルを作らない)lib/db/report-mapping.ts の consecutiveProblemDaysAtPeriodEnd()(:282-297)の ★ループ部分をそのまま lastEntryInPeriod() に切り出す★consecutiveProblemDaysAtPeriodEnd() は新関数を呼ぶだけにする★:export function consecutiveProblemDaysAtPeriodEnd(
entries: readonly FacilityConsecutiveEntry[],
consecutiveById: ReadonlyMap<number, number>,
facilityId: number,
range: { from: string; to: string }
): number {
const latest = lastEntryInPeriod(entries, facilityId, range);
if (!latest) return 0;
return consecutiveById.get(latest.id) ?? 0;
}
continuousProblems() 側で 「最終エントリを自分で探す」コードを書かない★★。必ずこの関数を呼ぶlast_problem_on の算出にも使う」ことを書く★tsc = 0 / lint = 0 / npm test 全パス(★テスト数は増える★)★consecutiveProblemDaysAtPeriodEnd() のシグネチャが変わっていない★★git diff --stat が lib/db/report-mapping.ts と lib/db/__tests__/report-mapping.test.ts の2ファイル★git commit -m "refactor: 指示書W 段階2 期間内の最終エントリを切り出す"★★problemRunDates() には「その施設の全期間の行」を渡すこと★★。 ★期間内の行だけで畳むと、★★前月から続いている連続でランが途中で切れ、{m} が過小になります★★★ (consecutive_problem_days は全期間走査で出しているので、★日数だけ 4 なのにランの日付が 2 件★という 食い違いになる)。★★期待値表 (a)-9 でこれを固定します★★。
problemRunDates(facilityRows, endDate, consecutiveDays) → string[]★★入力の facilityRows は ★その施設ぶんの ConsecutiveInput[](全期間)★。 戻りは ★日付昇順の YYYY-MM-DD 配列★。
| # | 入力 | ★期待★ |
|---|---|---|
| ★1★ | facilityRows = [] / consecutiveDays = 0 |
★[]★ |
| ★2★ | consecutiveDays = 0(最終日が問題なし) |
★[]★ |
| ★3★ | 1日だけ・問題あり / n=1 |
★["2026-09-10"]★ |
| ★4★ | 09-08 / 09-09 / 09-10 が全部問題 / endDate="2026-09-10" / n=3 |
★["2026-09-08","2026-09-09","2026-09-10"]★ |
| ★5★ | ★スケジュール非存在日を挟む★(09-01 / 09-03 / 09-05 の3日だけ行があり全部問題)/ n=3 |
★★["2026-09-01","2026-09-03","2026-09-05"]★★(★カレンダー上は5日。D-026★) |
| ★6★ | ★同一日に2行(1台が問題・1台が正常)★ | ★★その日は1日ぶんだけ★★(OR。D-109 決定2) |
| ★7★ | ランの前に問題なしの日がある(09-01 正常 / 09-02・09-03 問題)/ n=2 |
★["2026-09-02","2026-09-03"]★(★正常日を含めない★) |
| ★8★ | endDate が行に存在しない |
★[]★(★呼び出し側が同じ行から取った日付を渡す契約。例外にしない★) |
| ★9★ | ★★n が行の日数より大きい★★(行は2日ぶんしか無いのに n=4) |
★★slice が負にならず、先頭から endDate まで(2件)★★(Math.max(0, ...)) |
| ★10★ | endDate より後の日にも行がある |
★★endDate より後を含めない★★ |
launchFailedDaysInRun(facilityRows, runDates) → number★| # | 入力 | ★期待★ |
|---|---|---|
| ★1★ | runDates = [] |
★0★ |
| ★2★ | ラン3日・全日 completion_rate === null |
★3★ |
| ★3★ | ラン3日・null が1日も無い(40 / 55 / 62) |
★0★ |
| ★4★ | ★混在(3日中1日が null)★ |
★1★ |
| ★5★ | ★★同じ日に「null の行」と「完了率60の行」★★ |
★★1(その日を1日として数える。2回数えない)★★ |
| ★6★ | ランの外の日に null がある |
★★数えない★★ |
| ★7★ | ★完了率ちょうど 70.0 の日★ | ★★0(問題日ではあるが出発失敗ではない)★★ |
| ★8★ | ラン2日・両方 null(= m === n) |
★2★ |
continuousProblemSummary(consecutiveDays, launchFailedDays) → string★★★戻り値は「数値を埋めていないテンプレート」★★({n} / {m} の文字がそのまま残る)。
| # | n |
m |
★戻り★ |
|---|---|---|---|
| ★1★ | 1 | 0 | ★"{n}日連続で完了率が△以下"★ |
| ★2★ | 1 | 1 | ★"{n}日連続で出発失敗"★ |
| ★3★ | 2 | 0 | ★"{n}日連続で完了率が△以下"★ |
| ★4★ | ★2★ | ★1★ | ★★"{n}日連続で△以下(うち{m}日は出発失敗)"★★(★実データの施設5★) |
| ★5★ | 2 | 2 | ★"{n}日連続で出発失敗"★ |
| ★6★ | ★4★ | ★3★ | ★★"{n}日連続で△以下(うち{m}日は出発失敗)"★★(★実データの施設6★) |
| ★7★ | 6 | 0 | ★"{n}日連続で完了率が△以下"★(★実データの施設9★) |
| ★8★ | 4 | 4 | ★"{n}日連続で出発失敗"★ |
lib/i18n.ts に足すキーと1文字も違ってはいけない★★(全角括弧・「うち」・「は」を含む)。 ★★違うと translate() が未登録キーとして日本語のまま返す(fail-silent。D-111 項8)★★n >= 1 かつ 0 <= m <= n★(★n <= 0 では呼ばれない。呼び出し側が行を作らない★)lib/db/__tests__/report-mapping.test.ts に追記★。★実装より先に書き、落ちることを確認する★lib/db/report-mapping.ts に置く★★(★lib/db/clean-log-mapping.ts は触らない★ — 戻り型を変えないため。禁止事項8)problemRunDates() は内部で foldProblemDaysByDate() を呼ぶ★(★独自に遡って数えない★。 ★★数え方の規則が2箇所になると D-109 の問題が再発する★★)n と m の大小比較だけ)。★「2日以上」等をここに書かないこと★tsc = 0 / lint = 0 / npm test 全パス★git diff --stat が lib/db/report-mapping.ts と lib/db/__tests__/report-mapping.test.ts の2ファイル★git commit -m "feat: 指示書W 段階3 連続の内訳とsummaryテンプレートを作る"★対象★: reportPeriodKeysFromDates(dates) → ReportPeriodKey[] (★入力は YYYY-MM-DD の並び。重複していてよい★)
| # | 入力 | ★期待★ |
|---|---|---|
| ★1★ | [] |
★[]★ |
| ★2★ | ["2026-09-10"] |
★月次 {period:"monthly", year:2026, month:9, week:0} と 週次 {period:"weekly", year:2026, month:9, week: weekOfMonth("2026-09-10").week} の2件★(★★週番号を手計算で書かず weekOfMonth() を呼んで期待値にすること★★) |
| ★3★ | 同じ週の2日 | ★2件(重複しない)★ |
| ★4★ | 同じ月・別の週の2日 | ★月次1件 + 週次2件 = 3件★ |
| ★5★ | ★★月跨ぎ(["2026-08-31","2026-09-01"])★★ |
★★月次2件 + 週次2件 = 4件★★(★D-071。同じ日曜〜土曜でも月境界で別の週になる★) |
| ★6★ | 年跨ぎ(["2025-12-31","2026-01-01"]) |
★年が分かれて4件★ |
| ★7★ | 並び順 | ★★year → month → week の昇順(week:0 の月次が先頭)★★。★決定的であること★ |
| ★8★ | ★年次(month:0, week:0)★ |
★★作らない★★(P1では読まない。D-072 項4) |
lib/db/report-mapping.ts に置く★。★重複判定は既存の periodMapKey()(:31)を使う★weekOfMonth() に必ず通すこと★★ — ★★月跨ぎ週の帰属を知っているのは weekOfMonth() だけ★★(D-071 / CLAUDE.md §4.7)。★自分で「1日から数えて7日ごと」と書かない★todayJst() を呼ばないこと★(★当期の特別扱いをしない。決定2★)tsc = 0 / lint = 0 / npm test 全パス★ / ★8ケースがテストになっている★git commit -m "feat: 指示書W 段階4 清掃ログの日付から期間キーを列挙する"ContinuousProblem[] を組み立てる★(1コミット)★★「連続0日は行を作らない」「ロボット0台の施設も出す」「L1 も返す」は、DB を読む continuousProblems() の中に書くとテストできません★★(禁止事項17)。 ★★組み立てを純粋関数にして、段階6 は「DBから材料を集めてこの関数を呼ぶだけ」にすること★★。
★対象★:
export function continuousProblemsInPeriod(
entries: readonly ConsecutiveInput[], // ★過去の行のみ・全施設・全期間★
consecutiveById: ReadonlyMap<number, number>, // computeConsecutiveProblemDays() の戻り
facilityNameById: ReadonlyMap<number, string>, // ★ロボット0台の施設も入っている★
range: { from: string; to: string }
): ContinuousProblem[]
| # | 入力 | ★期待★ |
|---|---|---|
| ★1★ | entries = [] |
★[]★ |
| ★2★ | ★期間内にその施設の清掃ログが無い★ | ★★行を作らない★★ |
| ★3★ | ★連続0日(期間内の最終日が問題なし)★ | ★★行を作らない★★(★escalation_level を as で作らない・?? "L1" にしない★) |
| ★4★ | ★n=1(L1)★ |
★★行を作る★★・escalation_level="L1"(★絞り込みは lib/reports/query.ts の仕事。決定4★) |
| ★5★ | n=2 / n=3 / n=6 |
★"L2" / "L3" / "L3"★(★judgeEscalationLevel() 経由。閾値を直書きしない★) |
| ★6★ | ★★facilityNameById にロボット0台の施設が入っている★★ |
★★その施設の行が出る★★(決定5) |
| ★7★ | ★entries にあるが facilityNameById に無い施設 id★ |
★★行を作らない★★(★名前を空文字や String(id) で捏造しない★。facilityPerformance() の buildItem() が null を返すのと同じ作法) |
| ★8★ | ★混在ラン(3日中1日が未実施)★ | ★summary = 混在テンプレート / launch_failed_days = 1★ |
| ★9★ | ★全日が出発失敗★ | ★summary = 出発失敗テンプレート / launch_failed_days = n★ |
| ★10★ | ★全日が完了率低下★ | ★summary = 完了率テンプレート / launch_failed_days = 0★ |
| ★11★ | 通常ケース | ★last_problem_on = ★期間内最終エントリの日付(YYYY-MM-DD)★★ |
| ★12★ | ★完了率ちょうど 70.0 が最終日★ | ★★問題あり(行が出る)・launch_failed_days に数えない★★(D-109 決定3) |
| ★13★ | ★同一日に複数台(1台だけ問題)★ | ★★その日は1日★★(D-109 決定2) |
| ★14★ | ★★ランが期間の前から続いている(前月から連続4日、期間内は2日)★★ | ★★consecutive_problem_days = 4 / ランの日付も期間外の2日を含む / {m} も4日ぶんで数える★★(★これが「全期間の行を渡す」理由★) |
| ★15★ | 複数施設 | ★★戻りの順序が決定的★★(facilityNameById の並び順で作る。★並べ替えは lib/reports/query.ts の仕事★) |
★組み立ての順序(★この順で★)★:
facilityNameById を回す(★entries を回さない★ — 名前の無い施設を作らないため)days = consecutiveProblemDaysAtPeriodEnd(entries, consecutiveById, facilityId, range) / ★days <= 0 なら continue★level = judgeEscalationLevel(days) / ★level === null なら continue★(決定6。★キャスト禁止★)latest = lastEntryInPeriod(entries, facilityId, range) / ★無ければ continue★ (★2 で 0 を弾いているので通常は起きない。それでも ! や as を使わない★)facilityRows = entries.filter(e => e.facility_id === facilityId)(★全期間★)runDates = problemRunDates(facilityRows, latest.scheduled_at.slice(0, 10), days)m = launchFailedDaysInRun(facilityRows, runDates)summary = continuousProblemSummary(days, m)push({ facility_id, facility_name, consecutive_problem_days: days, escalation_level: level, last_problem_on: latest.scheduled_at.slice(0, 10), summary, launch_failed_days: m })tsc = 0 / lint = 0 / npm test 全パス★ / ★15ケースがテストになっている★as / !(non-null assertion)/ ?? "L1" を1つも使っていない★★git commit -m "feat: 指示書W 段階5 1期間ぶんの連続問題施設を組み立てる"prismaSource.continuousProblems()★(1コミット)facilityPerformance() をひな形にする★)★lib/db/index.ts:1115-1117 の notImplemented("continuousProblems") を置き換える★:
async continuousProblems() {
const { getPrisma } = await import("./prisma");
const prisma = await getPrisma();
// ★robots を join しない(ContinuousProblem に serial_number は無い。決定5)★
// ロボット0台の施設を除外すると、機体を撤去・交換中の施設の
// エスカレーションが画面から消える(fail-silent)
const facilityRows = await prisma.facility.findMany({ where: { deleted_at: null } });
const facilityNameById = new Map<number, string>(
facilityRows.map((f) => [Number(f.id), f.facility_name])
);
const [timetables, histories] = await Promise.all([
prisma.cleanTimetable.findMany(),
prisma.cleanStatusHistory.findMany(),
]);
const mainRows = timetables.filter((r) => r.precheck === CLEAN_TIMETABLE_PRECHECK.main);
const historyByTimetableId = latestHistoryByTimetableId(histories);
// ★`now` は1回だけ作って使い回す(行ごとに作ると境界の行の扱いがぶれる)★
// ★★`isFutureSchedule()` を外さないこと — 外すと未来7日ぶんが全部「出発失敗」になり
// 全施設が常時L3になる(D-106 / D-107)★★
const now = new Date();
const entries: ConsecutiveInput[] = mainRows
.filter((row) => !isFutureSchedule(row.start_at, now))
.map((row) => ({ /* cleanLogs() / facilityPerformance() と同じ作り方 */ }));
const consecutiveById = computeConsecutiveProblemDays(entries);
// ★確定期間も当期も詳細から算出する(対応するサマリが無い。D-102 項4 / 決定2)★
// 期間キーはデータの日付から列挙する。todayJst() による当期の場合分けは要らない
const rows: ContinuousProblemRow[] = [];
for (const key of reportPeriodKeysFromDates(entries.map((e) => e.scheduled_at.slice(0, 10)))) {
const range = periodRange(key.period, key.year, key.month, key.week);
if (!range) {
// ★自分が weekOfMonth() で作ったキーなので解けないのは実装の誤り(fail-loud)★
// 既存2箇所(reportKpis / facilityPerformance)と同じ作法にそろえる
throw new Error(`[db] continuousProblems: 期間範囲を算出できませんでした(${JSON.stringify(key)})`);
}
for (const item of continuousProblemsInPeriod(entries, consecutiveById, facilityNameById, range)) {
rows.push({ ...key, item });
}
}
return rows;
}
★★注意★★:
ConsecutiveInput の作り方は ★facilityPerformance()(lib/db/index.ts:938-947)と1文字同じ★ にすること★ — ★★2画面で連続日数が食い違わないため(D-103)★★prisma.robotErrorReport は読まない★(★summary にエラー種別を出さないため。D-077 / 確定1★)lib/db/index.ts の import に新関数4本を足す★★(lint の未使用/不足で気づける)notImplemented() が他で使われているなら残す。使われなくなったら未使用で lint が落ちるので消す★lib/reports/query.ts の仕事)sortContinuousProblems() の仕事)tsc = 0 / lint = 0 / npm test 全パス(★この段階でテストは増えない★)★USE_FIXTURES=false で db.continuousProblems() が例外を投げずに配列を返す★★(★件数の検証は段階8★)git diff --stat が lib/db/index.ts の1ファイルだけ★git commit -m "feat: 指示書W 段階6 continuousProblems()を実DBから作る"lib/i18n.ts に3キーを足す★★段階3 の定数と1文字も違わないこと★★(★translate() は未登録キーを日本語のまま返す。 fail-silent で気づけない。D-111 項8★)。
| キー(日本語原文) | ★EN★ |
|---|---|
"{n}日連続で完了率が△以下" |
"Completion rate △ or below for {n} consecutive days" |
"{n}日連続で出発失敗" |
"Launch failed for {n} consecutive days" |
"{n}日連続で△以下(うち{m}日は出発失敗)" |
"△ or below for {n} consecutive days ({m} with launch failure)" |
★英文は実装者判断で改善してよいが、★★{n} / {m} を必ず1回ずつ含めること★★★。 ★既存の "{n}日以上連続": "{n}+ consecutive days"(lib/i18n.ts:175)の語彙に寄せる★。
ReportsScreen.tsx:391-394 を直す★現状(★t() を通していない=EN で和文が残る★)★:
<span className={styles.contSummary}>{item.summary}</span>
★直したあと(例。★{m} は値があるときだけ置換する★)★:
// ★サーバはテンプレート文字列を返す(決定3。方式A)。数値はここで差し込む★
// `String.replace` は文字列指定だと最初の1件だけ置換する。各テンプレートに
// `{n}` `{m}` は1回ずつしか出ないのでこれで足りる。
// ★`launch_failed_days` は任意項目(fixtures を凍結しているため)。
// 値が無いときは置換しない(0 を捏造しない)★
const summaryText = (() => {
const base = t(item.summary).replace("{n}", String(item.consecutive_problem_days));
return item.launch_failed_days === undefined
? base
: base.replace("{m}", String(item.launch_failed_days));
})();
:391-393 の既存コメント(「モックの『完了率 {n}%』ではなく summary を出す★(指示書F 0-1 の判断)」)も 書き換えること★★ — ★★D-026 の説明を出す、という理由はもう成り立たない(D-111 / 本書)★★fixtures の summary は完成文でプレースホルダを含まない★★ので、replace は何もしない。 ★EN 表示では和文のまま出るが、それは想定内★(D-111 項5)ReportsScreen.tsx:364 の副題(「2日以上連続で△以下または出発失敗の施設」)は本書では直しません★★ (★申し送り③★)。★ついでに直さないこと(差分が増えてレビューしづらくなる)★tsc = 0 / lint = 0 / npm test 全パス★git diff --stat が lib/i18n.ts と components/features/reports/ReportsScreen.tsx の2ファイル★git commit -m "feat: 指示書W 段階7 連続問題施設のsummaryをi18n対応にする"★★.env.local を書き換えず、コマンドラインで env を渡すこと★★(★戻し忘れと他セッションへの影響を防ぐ★。 D-108 項8)。★★一時スクリプトには成功時にも必ず process.exit(0) を書くこと★★(★D-111 項9。 main().catch(...) だけだと Prisma の接続が残り、★実際に4時間39分ハングした★★)。
USE_FIXTURES=false DATABASE_URL="mysql://root@localhost:3306/dwatch_local" npx tsx -e "
import { db } from './lib/db';
import { ESCALATION_THRESHOLDS } from './lib/config';
const rows = await db.continuousProblems();
const two = rows.filter(r => r.item.consecutive_problem_days >= ESCALATION_THRESHOLDS.level2Days);
console.log('全行', rows.length, '2日以上', two.length);
console.log(JSON.stringify(rows.map(r => [r.period, r.year, r.month, r.week,
r.item.facility_id, r.item.consecutive_problem_days, r.item.escalation_level,
r.item.last_problem_on, r.item.launch_failed_days, r.item.summary])));
const logs = await db.cleanLogs();
const pre = await db.prechecks();
console.log('cleanLogs', logs.length, 'prechecks', pre.length);
const fp = await db.facilityPerformance();
console.log(JSON.stringify(fp.map(r => [r.period, r.year, r.month, r.week,
r.item.facility_id, r.item.consecutive_problem_days, r.item.status])));
process.exit(0); // ★★必ず書くこと★★
"
★npx tsx でエイリアス(@/)が解決できない場合★は ★dev サーバ+curl に切り替えてよい★ (/api/reports/continuous-problems?period=weekly&year=...)。★★その場合も下の期待値をすべて実測すること★★。
★★期待値(★作成時点の実測。★着手直前に段階0 で自分が控えた値を基準にすること★★)★★:
| # | 項目 | ★期待★ |
|---|---|---|
| ★1★ | ★2日以上の件数★ | ★★当週 4件 / 当月 4件 / 確定6期間はすべて0件★★ |
| ★2★ | ★施設5★ | ★★2日 / L2 / launch_failed_days = 1 / summary = "{n}日連続で△以下(うち{m}日は出発失敗)"★★ |
| ★3★ | ★施設6★ | ★★4日 / L3 / launch_failed_days = 3 / summary = 混在テンプレート★★ |
| ★4★ | ★施設7★ | ★4日 / L3★。★launch_failed_days は ★0 か 4 のどちらか★(★混在は施設5・6 の2件だけ★)。★実測して記録すること★ |
| ★5★ | ★施設9★ | ★★6日 / L3 / launch_failed_days = 0 / summary = "{n}日連続で完了率が△以下"★★(★5日連続で△のあと最終日だけ 9.9% で ×。★★「△以下」に畳むので1本の連続になる★★) |
| ★6★ | ★L1(1日)★ | ★★施設8・施設10 が escalation_level: "L1" で行として返る★★(★データ層は L1 も返す。決定4★) |
| ★7★ | ★確定期間の行★ | ★★期間キー自体は存在する(8月ぶんの週次・月次)が、連続0日なので ContinuousProblem が0件★★ |
| ★8★ | ★cleanLogs() / prechecks()★ |
★★259 / 27(★1件も変わっていないこと★)★★ |
| ★9★ | ★★facilityPerformance() の全出力★★ |
★★段階0 で控えた内容と完全一致★★(★段階2 のリファクタが値を変えていないこと★) |
| ★10★ | ★summary に数値が埋まっていないこと★ |
★★戻り値に {n} {m} の文字がそのまま残っている★★(★埋まっていたら決定3 違反★) |
| ★11★ | ★last_problem_on★ |
★YYYY-MM-DD(時刻を含まない)★。★その日が問題日であること(施設5 なら 70.0 の日)★ |
| ★12★ | ★launch_failed_days <= consecutive_problem_days★ |
★★全行で成立★★ |
| 画面 | 期待 |
|---|---|
★/reports★ |
★★連続問題施設カードが 500 → 200、当週で 4件★★(★limit 既定10 なので「他N件を表示」は出ない★)。★KPI・施設別・エラー内訳が回帰していないこと★ |
★/reports(先週・先月に切替)★ |
★★「該当施設なし」(0件)が出て、エラーにならない★★ |
★/detail-log★ |
★200 / 259件★ |
★/dashboard / /alerts★ |
★200 / 9 / 9 / 27件★ |
/facilities / /schedules / /ds-logs / /ds-alerts |
★200。回帰していないこと★ |
next dev は .env.local を共有する別セッションに影響します(D-108 項8)★★ — ★USE_FIXTURES=false npm run start -- -p <別ポート> のように環境変数を都度渡す★USE_FIXTURES=true での確認★launch_failed_days が無くても画面が壊れないこと★★(★{m} の置換をスキップする分岐★).env.local が着手前と同じ(USE_FIXTURES=true)であることを確認した★★git status に lib/ docs/ components/ d-watch-api.yaml 以外の差分が無いことを確認した★ (★README.md / docs/code-map.md / docs/handoff/管理画面_設計検討_20260910.md / .claude/worktrees/** が★着手前と同じ状態のまま★であること。禁止事項2)docs/decisions.md に追記(★D-113 の見込み。★着手直前に実測した番号を使うこと★)★見出しの形式は既存に揃える★: ### D-113 \[確定]` 指示書W(連続問題施設を実DBから作る)を実行した`
★最低限入れる項目★:
| # | 項目 |
|---|---|
| 1 | ★★continuousProblems() を実装し、監視側の DataSource が全部埋まったこと★★(★残るは点検記録2本(D-111 項6)と Pudu Cloud 待ちの GET /clean-logs/{id}/areas(501)★) |
| 2 | ★★確定2(C案): 混在ランは「{n}日連続で△以下(うち{m}日は出発失敗)」★★。★★D-111 項2 の決定4(出発失敗を優先)を差し替えたことを明記する★★(★却下理由: 直近に清掃を実施していても「出発失敗」と書くことになり事実と食い違う / (b) は出発失敗を隠す★)。★実データに2件実在(施設5・施設6)★ |
| 3 | ★★確定3: △ と × は書き分けず「△以下」に畳む★★(★施設9 は5日連続で△のあと最終日だけ 9.9% で ×★。★モック d-watch-mock.jsx:713 の副題と同じ粒度★) |
| 4 | ★決定1: API契約を先に直した★(d-watch-api.yaml の summary の description / example が D-026 の説明になっていた)。★launch_failed_days を任意項目として足した理由(fixtures を凍結するため)★ |
| 5 | ★★決定2: 確定期間も詳細テーブルから算出した★★(★対応するサマリが無い(D-102 項4)★ / ★D-102 に反しない(禁じているのは「サマリがあるのに再計算すること」)★ / ★当期だけにすると先週・先月でこのカードだけ常に空になり、施設別パフォーマンス表と不整合になる★) |
| 6 | ★決定3: i18n はテンプレート方式(方式A)★。★サーバはテンプレートのまま返し、画面が {n} {m} を差し込む★。★ReportsScreen.tsx:394 が t() を通していなかったことも直した★ |
| 7 | ★決定4: L1 もデータ層は返す★(★「2日以上」は lib/reports/query.ts:236 に実装済み。データ層でも絞ると規則が2箇所になる(D-103)★) |
| 8 | ★★決定5: ロボット0台の施設を除外しない★★(★facilityPerformance() が除外しているのは serial_number を出すためで、ContinuousProblem には無い★ / ★除外すると機体を撤去・交換中の施設のエスカレーションが消える(fail-silent)★) |
| 9 | ★決定6: 連続0日は行を作らない★(★judgeEscalationLevel() が null を返す。as キャストや ?? "L1" で握り潰さない★) |
| 10 | ★★共有した既存関数と、新設した6本★★(★lastEntryInPeriod() / problemRunDates() / launchFailedDaysInRun() / continuousProblemSummary() / reportPeriodKeysFromDates() / continuousProblemsInPeriod()。名前は実装に合わせる★) |
| 11 | ★★型を拡張しなかった理由★★(★ScheduleDayResult と foldProblemDaysByDate() の戻り型は toEqual 比較のテストが依存している / consecutiveProblemDaysAtPeriodEnd() のシグネチャも同様★) |
| 12 | ★★ランは期間をまたぐので、ランの抽出には「その施設の全期間の行」を渡すこと★★(★踏むと {m} が過小になる★) |
| 13 | ★段階8 の実測値(★2日以上が当週4件・当月4件・確定6期間0件 / 施設5=2日(m=1) / 施設6=4日(m=3) / 施設7=4日(m=実測値) / 施設9=6日(m=0) / L1 は施設8・施設10 / cleanLogs 259 / prechecks 27 / テスト数★) |
| 14 | ★申し送り(下記「申し送り」節をそのまま移す)★ |
docs/handoff.md に追記(★追記34★)## 2026-09-1X(追記34)★(★番号は実測すること★。★別担当が先に使っていたら35★)docs/handoff.md:6 付近)も更新すること★{n} / {m})で返し、画面が数値を差し込む」★★foldProblemDaysByDate() の戻り型を拡張すると toEqual のテストが全部落ちる★ / ★i18n のキーは1文字違うと fail-silent★)★DataSource は全部埋まった。残りは点検記録2本」★★docs/decisions.md と docs/handoff.md のみ)★★
git commit -m "docs: 指示書W完了(D-113)を記録する"docs/decisions.md には別担当の D-111 が未コミットのまま入っています★★ — ★★git add docs/decisions.md は D-111 ごと巻き込みます★★。 ★着手時に D-111 が既にコミット済みか確認し、★★未コミットなら、その担当が先にコミットするのを待つか、 巻き込むことを承知のうえでコミットメッセージに明記すること★★★README.md(作成時点で M)と docs/code-map.md / docs/handoff/管理画面_設計検討_20260910.md (作成時点で ??)は別作業のもの。コミットに含めないこと★★(禁止事項2)git pull --rebase の要否を確認し、★競合したら自分の追記だけを残して番号を取り直すこと★★★git push origin develop★README.md / docs/code-map.md / docs/handoff/管理画面_設計検討_20260910.md / .claude/worktrees/** に差分が無いことを最後に確認すること★★★D-113 に列挙して残すこと★。
| # | 論点 | 状態 |
|---|---|---|
| ★①★ | ★★全件取得をやめて SQL に絞り込みを降ろす件★★(★ユーザー提起 2026-09-11★)— 現状 cleanLogs() / facilityPerformance() / continuousProblems() はいずれも clean_timetables / clean_status_histories を全期間・全件読む(★DataSource が引数を取らない契約★。lib/db/index.ts:216-218 / D-090 項15)。★年間13万行(docs/spec.md:45)では重い(D-092 項3-3)★ |
★★DataSource の契約変更なので全メソッドに波及する★★。★テストDBで実測してから専用の指示書で行う★。★★第7回で取り下げた索引の依頼は、これを実施するなら再依頼が要る★★ |
| ★②★ | ★.contSummary(components/features/reports/ReportsScreen.module.css:251-257)に省略記号の指定が無く、長い文は折り返して行が伸びる★ — ★施設名(.contFacility)は nowrap で縮まない★ |
★★C案の文は22文字前後で境界★★(例:「4日連続で△以下(うち3日は出発失敗)」)。★text-overflow: ellipsis を足すか、行の高さが伸びるのを許容するかの判断が要る★ |
| ★③★ | ★ReportsScreen.tsx:364 の副題「2日以上連続で△以下または出発失敗の施設」が ★t() 未適用★ かつ ★「2」がハードコード★(ESCALATION_THRESHOLDS.level2Days から読んでいない)★ |
★本書では直さない(差分を段階7 の1箇所に限るため)★。★★閾値を変えると副題だけ嘘になる★★ |
| ★④★ | ★★当期は同じ全件スキャンが2〜3回重なる★★ — reportKpis() / facilityPerformance() / continuousProblems() がそれぞれ独立に全件を読む(★/reports を1回開くと3回★) |
★①と一体で見直す★ |
| ★⑤★ | ★periodRange() が解けないと一覧APIが 500★(★本書の continuousProblems() も既存2箇所と同じく throw する★) |
★指示書V 申し送り⑤ と同じ。「その行だけ落とす」に変えるか要検討★ |
| ★⑥★ | ★★consecutiveProblemDaysAtPeriodEnd() は「期末時点の連続」であって「期間中の最長連続」ではない★★(A案。D-103)— ★期間の途中で5日連続していても、期末の日が問題なしなら 0★ |
★★施設別パフォーマンスと同じ規則なので意図した挙動★★。★確定期間が0件になる主因でもある★。定義を変えたくなったら D-103 から見直す |
| ★⑦★ | ★論理削除された施設(facilities.deleted_at != null)の清掃ログは、施設名が引けないので行が作られない(fail-silent)★ |
★facilityPerformance() の buildItem() と同じ作法にそろえてある★。★方針として妥当かの確認が要る(指示書V 申し送り⑥ と同種)★ |
| # | 条件 |
|---|---|
| ★1★ | ★d-watch-api.yaml の summary の description / example が事実ベースの3種に書き換わり、launch_failed_days(任意項目)が足されている★ |
| ★2★ | ★lib/types/index.ts が YAML に追従している(launch_failed_days?: number)★ |
| ★3★ | ★★prismaSource.continuousProblems() が notImplemented でなくなり、USE_FIXTURES=false で 200 が返る★★ |
| ★4★ | ★確定期間(先週・先月)も詳細テーブルから算出しており、期間キーがデータの日付から列挙されている(todayJst() による当期の場合分けが無い)★ |
| ★5★ | ★summary が★テンプレート文字列★で返り、画面が {n} {m} を差し込んでいる。★lib/i18n.ts に3キーの EN 対訳がある★★ |
| ★6★ | ★★混在ランが C案の文面になっている(実データの施設5・施設6 で確認)★★ |
| ★7★ | ★△ と × を書き分けていない(施設9 が1本の連続として出る)★ |
| ★8★ | ★L1 の行がデータ層から返り、API 応答には含まれない(絞り込みは lib/reports/query.ts のまま)★ |
| ★9★ | ★ロボット0台の施設が除外されていない(prisma.facility.findMany に include: { robots: ... } が無い)★ |
| ★10★ | ★連続0日の行が作られていない。★as / ! / ?? "L1" を使っていない★★ |
| ★11★ | ★computeConsecutiveProblemDays() / foldProblemDaysByDate() / consecutiveProblemDaysAtPeriodEnd() / isFutureSchedule() / weekOfMonth() を共有しており、同等の処理を別実装していない★ |
| ★12★ | ★★ScheduleDayResult / foldProblemDaysByDate() の戻り型と consecutiveProblemDaysAtPeriodEnd() のシグネチャが変わっていない★★ |
| ★13★ | ★期待値表(段階2 の8 / 段階3 の 10+8+8 / 段階4 の8 / 段階5 の15)がすべてテストになっている★ |
| ★14★ | ★npx tsc --noEmit = 0 / npm run lint = 0 / npm test 全パス(★テスト数は着手前より増えている★)★ |
| ★15★ | ★段階8 の実測: 2日以上が★当週4件・当月4件・確定6期間0件★ / 施設5=2日(m=1) / 施設6=4日(m=3) / 施設9=6日(m=0) / L1 は施設8・施設10★ |
| ★16★ | ★★cleanLogs() = 259件 / prechecks() = 27件 / facilityPerformance() の全出力が段階0 と完全一致★★ |
| ★17★ | ★prisma/ / lib/fixtures/ / lib/judge/ / lib/reports/query.ts / app/api/ に差分が無い★ |
| ★18★ | ★README.md / docs/code-map.md / docs/handoff/管理画面_設計検討_20260910.md / .claude/worktrees/** に触っていない★ |
| ★19★ | ★docs/decisions.md(★D-113★)と docs/handoff.md(追記34)に記録した★ |
| ★20★ | ★8コミット(段階1 / 2 / 3 / 4 / 5 / 6 / 7 / 9)で push した★ |
| ★21★ | ★.env.local が USE_FIXTURES=true に戻っている★ |
| ★22★ | ★★DB に1行も書いていない★★ |
| 項目 | 内容 |
|---|---|
| 状態 | ★完了★ |
| 実行日 | 2026-09-11 |
| 決定番号 | ★D-116★(着手時点の最終番号はD-115。指示書Y完了後に実行したため見込みのD-113ではない) |
| コミット | e24e177(1) / 985ce60(2) / e136256(3) / 1aba267(4) / f73b6b7(5) / 2974653(6) / b214449(7) / 本コミット(9) |
| 実測値 | tsc0 / lint0 / npm test 45ファイル1023テスト / 当週・当月とも2日以上4件(施設5=2日m1/施設6=4日m3/施設7=4日m0/施設9=6日m0) / L1が施設8・10 / facilityPerformance()全出力が段階0と完全一致。詳細は docs/decisions.md の D-116 / docs/handoff.md 追記36 を参照 |