# 指示書BI — 予定なしの行にもアドバイスを出す（2026-11-04）

**担当**: Cursor。監督（Claude Code）がレビューする。
**前提**: develop の `4bb639e` 以降（指示書BH＝並べ替え・予定あり／予定なしの絞り込み・文言「予定なし」が入った版）。
★作業の前に必ず読むこと★: `app/api/clean-logs/[id]/advice/route.ts`（今のアドバイス API）、`lib/db/unscheduled-clean-log-source.ts`・`lib/db/clean-log-mapping.ts`（予定なしの行の作り方）、`lib/api/path-params.ts` の `parsePathErrorId()`（文字列 ID の検証の手本）。
★行番号はすべて `4bb639e` 時点の実物で確かめた値★（監督側の書き直し役が開いて確認。誤記5件・`logApiError` の参照0件は監督が数え直した）（作業中にずれたら、書いてある文言で探す）。

---

## 0. ユーザーの指示（原文）と回答

- 「清掃詳細ログにあった、アドバイスを出す仕組みが消えています」
  - 監督の調査: 消えていない。予定のある △× の行は今も ▼ で開く。★予定なしの行（予定外タスク。`CleanLogItem.unscheduled = true`）は ▼ を出さない★（指示書BG §2-1 の決定）。
    ステージングの △× 335件のうち 295件が予定なしで、既定の画面の1ページ目では 50行中5行しか ▼ が出ないため、消えたように見えた
- 「予定外のものはアドバイスを出せないですか」
  - 監督の問い「予定なしの行にもアドバイスの ▼ を出しますか」への回答: ★「出す」★。実装は ★Cursor★

## 1. 何を変えるか（全体像）

| 項目 | 今 | ★変更後★ |
|---|---|---|
| 予定なしの △× の行の ▼ | 出さない（開けない） | ★出す（開ける）★ |
| 開いたときのアドバイス | — | ★予定ありの行と同じ形（`CleanLogAdviceResponse`）★。新しい API から取る |
| 開いたときの「エリア別詳細を見る →」 | — | ★予定なしの行では出さない★（エリア別詳細は予定の id が要る。今の方針のまま） |
| 予定なしの ◎○ の行 | 開けない | 開けない（予定ありと同じく △× だけ） |
| 対象の画面 | — | ★清掃詳細ログとダッシュボードの小さい表の両方★（同じ表の部品 `CleanLogTable` を使っている） |

**予定なしの行に出るアドバイス**（`matchAdviceRules()` の式からの帰結。変えない）:
予定なしの行は必ず出発済み（`departed = true`）で、事前チェックの4列は null。そのため ★R1（出発失敗）・B2・B3（事前チェック）は出ない★。
出うるのは ★B1（△）・R2（完了率10%未満）・R3／R4（施設×日付の連続日数）★と L1〜L3。
△ なら B1、× なら R2 が必ず当たるので、予定なしの △× を開いて「該当するアドバイスはありません」になることは無い。
R3・R4 は ★その施設のその日の結果★（予定ありの清掃も合わせた連続日数。CLAUDE.md §4.5「予定外のタスクも、その施設のその日の結果に入れる」）で出る — 仕様どおり。

## 2. API

### 2-1. 新しい入口

`GET /clean-logs/unscheduled/{report_id}/advice`

- `{report_id}` ＝ `CleanLogItem.report_id`（`clean_task_reports.report_id`。★`varchar(20)` の文字列★。値は19桁の数字の文字列で、安全な整数を超える）
- 返すもの: ★`GET /clean-logs/{id}/advice` と同じ `CleanLogAdviceResponse`★（200）
- 404（`not_found`・「指定された清掃ログが見つかりません」。既存と同じ文言）:
  - その `report_id` の行が無い
  - ★予定のあるレポート★（`clean_timetable_id` が 0 でも NULL でもない）— 予定のある行は `/clean-logs/{id}/advice` で引く
  - ★削除済みの施設・清掃エリアの行★（`FACILITY_SECTION_VISIBLE_WHERE`。一覧に出ない行。D-182）
- 422（`validation_failed`）: `report_id` が空・20文字を超える（下の 2-2）
- 500: 取得失敗・アドバイスの設定が読めない（既存と同じ。表示上限の設定が読めないときは既定値で続ける — 既存と同じ非対称を崩さない）
- ★なぜこの形か★:
  - `/clean-logs/{id}/advice` の `{id}` は整数の契約（`PathId`・`parsePathId()`。安全な整数まで）で、19桁の `report_id` を通せない
  - パス名の `unscheduled` は `CleanLogItem.unscheduled`・`?schedule=unscheduled` とそろう
  - Next の app ルートは同じ階層の静的なフォルダを動的な `[id]` より先に当てる（`app/api/clean-logs/export/` と `[id]/` が並んでいる前例がある）。
    ★`app/api/clean-logs/[report_id]/` のように clean-logs 直下に名前の違う動的フォルダを作ると、Next がビルドで止まる★（同じ階層で名前の違う動的セグメント）。必ず静的な `unscheduled/` を挟む
  - 認証・IP 制限（`proxy.ts`）は `/api/` 配下すべてにかかるので、設定の追加は要らない

### 2-2. パスの検証（`lib/api/path-params.ts`・`lib/config/index.ts`）

- `lib/config/index.ts` に `PATH_REPORT_ID_MAX_LENGTH = 20`（`clean_task_reports.report_id` が `varchar(20)`）。消費者は `parsePathReportId()` だけ
- `parsePathReportId(value: string): QueryParseResult<string>` を ★`parsePathErrorId()`（`lib/api/path-params.ts:89-97`）と同じ作法★で足す:
  空文字と20文字超だけを 422（message「report_idの形式が不正です」、details `{ report_id: ["20文字以内で指定してください"] }`）。★文字種は絞らない★。★`Number()` / `BigInt()` を通さない★。★入力値をエコーしない★

### 2-3. データ層（`lib/db/`）— ★予定なしの1行だけを取る読み口を新しく作る★

- `DataSource`（`lib/db/index.ts`）に ★`unscheduledCleanLog(reportId: string): Promise<CleanLogItem | null>`★ を足す（前例: `errorReportById()` / `precheckItemById()`。条件外は null → ルートが 404）
- ★`CleanLogDbQuery` に `report_id` を足さない★（必須フィールドの作法なので、本番5ルート・十数か所のテストに波及し、`mainTimetableWhere()`・`cleanLogRunMeta` にも条件が要る）
- ★`db.cleanLogs({ id: null, date_from: null, ... })` で全件を読んでから探さない★（予定のある行の全件読みに戻る。指示書AC で潰した問題）
- **Prisma 版**（この順で）:
  1. `clean_task_reports` を ★`where: { ...UNSCHEDULED_TIMETABLE_ID_WHERE, ...FACILITY_SECTION_VISIBLE_WHERE, report_id: reportId }`★ で読む（`findMany`）。
     ★展開して並べる。`AND` で包まない★ — 同じ階層の `OR` は `UNSCHEDULED_TIMETABLE_ID_WHERE` の1つだけ（`FACILITY_SECTION_VISIBLE_WHERE` はリレーション条件、`report_id` はスカラー）。
     既存の `unscheduledCleanLogWhere()`（`lib/db/unscheduled-clean-log-source.ts:39-49`）・`loadConsecutiveInputs()` の3本目（`lib/db/consecutive-source.ts:590-598`）と同じ展開の形にする。
     ★`unscheduledCleanLogWhere()` に `CleanLogDbQuery` を作って渡す形にもしない★（必須フィールドが多い）。
     ★`include`（施設名・清掃エリアのロボット）と並びは `loadUnscheduledCleanLogRows()`（同ファイル :120-142）と同じものを使う★ — `where` を引数で受ける関数に切り出すなどして、`include` の定義を2か所に書き写さない
  2. `unscheduledCleanLogRowsFromReports()` を通す（★予定のあるレポートはここで落ちて `[]` → null → 404★。`unscheduledCleanLogItem()` に直接渡すと例外になり 500 になる）
  3. ★手順2の結果から `r.report.report_id === reportId`（JS の `===`）の行を `find` する★（`UnscheduledCleanLogRow` では `report_id` は `report` の中）。★先頭の1行だけを見ない★。無ければ null。
     DB の照合順序が大小文字・末尾の空白を同一視しうるので、SQL で当たった行をここで弾く（`errorReportById()` の D-4 と同じ）
  4. 連続日数: ★`loadConsecutiveInputs(prisma, now, { date_from: d, date_to: d, facility_ids: [その行の facility_id] })`★（`d` は ★その行の JST の日付＝`started_at_iso` の先頭10文字★。`Date` や UTC の日付を使わない）→ `consecutiveProblemDaysByFacilityDate()` → `.get(facilityDateKey(facility_id, started_at_iso)) ?? 0`。
     連続日数は「その日まで」で数え、施設ごとに独立しているので、★表示上限以下なら一覧と同じ生値になる★（上限を超えると生値は経路で違いうるが、表示「{上限}日以上」と L3 は一致する。既存の3経路と同じ許容範囲。`lib/db/consecutive-source.ts:491-495`）。
     ★指示書BB の U4（単一ID の連続日数の入力は全期間・全施設のまま）は、既存の `/clean-logs/{id}/advice`・`/{id}/areas` についての決定。新しい入口は行の日付と施設が先に分かるので絞る（U4 と食い違わない）★
  5. `unscheduledCleanLogItem(report, context, consecutive)` で1行を作る（一覧と同じ関数）。
     ★置き場所★: 手順1〜5は `prismaSource.unscheduledCleanLog()`（`lib/db/index.ts` の中）に書き、★手順1の読み込みだけ★を `lib/db/unscheduled-clean-log-source.ts` の関数に置く。
     ログは一覧と同じく `reportSectionRobotCount(row.section_id, row.robot_count, new Set())` で出す（`lib/db/index.ts:946` の★非公開関数★なので、`unscheduled-clean-log-source.ts` 側からは呼べない。1リクエストに1つの `seen`）
- **fixtures 版**: `unscheduledCleanLogs.find((r) => r.report_id === reportId) ?? null`（`lib/fixtures/clean-logs.ts:383`。Prisma 版の手順3と同じく `===` で比べる）。
  ★可視性（削除済み）の絞り込みは書かない。架空のフィルタを作らない★ — fixtures の清掃ログには削除済みの施設・清掃エリアが無く（`lib/db/visibility.ts:8`「fixtures に削除の概念が無い」）、一覧の `filterFixtureCleanLogs()` も可視性を見ない。
  ★`isVisibleFacilitySection()` を呼んだり、施設の fixtures から `deleted_at` を引いたりしない★。★削除済みの 404 は Prisma のスタブ（表A-db の A-9・表A-route の A-9′）だけで確かめる★
- ★1行の値は、データ層の一覧（`db.cleanLogs()`）の同じ `row_key` の行と完全に一致すること★（連続日数・`escalation_level` まで。ルートの `capCleanLogs()` が付ける `consecutive_problem_days_capped` は比べる対象に含めない。表A-db の A-5・A-6 で縛る）

### 2-4. ルートと共通化

- 新規 `app/api/clean-logs/unscheduled/[report_id]/advice/route.ts`: `parsePathReportId()` → `db.unscheduledCleanLog()`（例外は 500）→ null なら 404 → 下の共通関数
- ★`[id]/advice/route.ts` の「1行が決まった後」（`find()` の後：ルール判定・ログ・設定の読み込み・表示上限・応答。今の :78-174）を共通関数に切り出し、両方のルートから呼ぶ★（新規 `lib/clean-logs/advice-response.ts`。名前は案）。
  `lib/judge/` には置かない（設定の読み込みとログを含むので純粋関数ではない。CLAUDE.md §9.4）。
  ★`[id]/advice` の挙動・ログ・文言は1文字も変えない★（既存のテストが変えずに通ること）。
  ★判定（`matchAdviceRules()`・`judgeEscalationLevel()`）とログには生値、`capConsecutiveDays()` は応答の値だけ★（今の順序を崩さない）
- ★共通関数の形★: `adviceResponse(log: CleanLogItem, ctx: { route: string; logIds: { clean_log_id: number } | { report_id: string } }): Promise<NextResponse>`（名前は案）。
  - ★共通関数のファイルに経路名の定数を持たない★。`logApiError`（`loadAdviceRules()` の失敗）・`judge.advice`・`judge.escalation` の3か所すべてで `ctx.route` を使う
  - `judge.advice` には `...ctx.logIds` を広げる（予定ありは `clean_log_id` だけ、予定なしは `report_id` だけを出す）。`judge.escalation` には今どおり ID を足さない
  - ★`[id]` ルート★: 今のファイル内の `ROUTE`（`"/api/clean-logs/[id]/advice"`。:25）と、パスを解析した `logId` をそのまま渡す（`{ route: ROUTE, logIds: { clean_log_id: logId } }`）。`log.id` から作らない（:99-100 のコメントの作法）
  - ★新しいルート★: ファイル内に `const ROUTE = "/api/clean-logs/unscheduled/[report_id]/advice"` を置く。全体を `withRequestId(request, …)` で包む。
    `db.unscheduledCleanLog()` の例外は、その `ROUTE` で `logApiError({ route: ROUTE, method: "GET", body, status: 500, err })` して標準の形の 500（「データの取得に失敗しました」）にする。
    共通関数には `{ route: ROUTE, logIds: { report_id: <パスの report_id> } }` を渡す（`log.report_id` は型の上で null を含む。null になるのは不変条件違反なので、渡すのは `parsePathReportId()` を通したパスの値。手順3の `===` で一致を確かめた値と同じ）
  - `[id]` 側の既存テストは経路名（`route`）を見ていない（`app/api/clean-logs/[id]/__tests__/route.test.ts:925-946` は `method`・`status` だけ）。`[id]` の経路名が変わっていないことは、`ROUTE` を渡していることをコードレビューで確かめる
- ログ: `JudgeAdviceLogEvent`（`lib/logger/events.ts:139-`）に ★`report_id?: string`★ を足し、予定なしの行では `clean_log_id` を出さず `report_id` を出す（`clean_log_id` は number 型）。
  `docs/logging-design.md` を直す:
  - 「記録可（数値・enum・ID）」（:204）に `report_id` を足す
  - :253（`matchAdviceRules` / `judgeEscalationLevel` の呼び出し元）・:289（`judge.advice` / `judge.escalation` を出すルート）の表: `[id]/advice` だけを挙げている行に、新しいルート `app/api/clean-logs/unscheduled/[report_id]/advice/route.ts`（共通関数 `lib/clean-logs/advice-response.ts` 経由）を足す
  - :487 のルート一覧に新しい `route.ts` を足す
- `lib/logger/events.ts` の `SettingsStoreCorruptLogEvent` の説明（:377-379）の「`/api/settings/advice` と `/api/clean-logs/[id]/advice` の2ルートから共有」を、
  「`/api/settings/advice` と、アドバイスの共通関数を通る `/api/clean-logs/[id]/advice`・`/api/clean-logs/unscheduled/[report_id]/advice` の3ルートから共有」に直す

### 2-5. `d-watch-api.yaml`（★先に直す。CLAUDE.md §6-1★）

- 新しい path `/clean-logs/unscheduled/{report_id}/advice` を `/clean-logs/{id}/advice`（:703）の直後に足す。パラメータはインラインの `string`・`maxLength: 20`（`/errors/{error_id}/status`（:1074）と同じ書き方）。200・404（`NotFound`）・422（`ValidationError`）・500
- ★200 の形を `components/schemas/CleanLogAdviceResponse` に切り出し、既存の `/clean-logs/{id}/advice` と新しい入口の両方から `$ref`★（写すと片方だけ直す事故が起きる。型名は `lib/types/index.ts` の `CleanLogAdviceResponse` とそろう。CLAUDE.md §9.2）。中身は今のインラインの定義をそのまま移す
- `/clean-logs/{id}/advice` の説明（:709）の「予定外タスクの行（`id: null`）には無い」→「予定なしの行は `/clean-logs/unscheduled/{report_id}/advice`」
- `CleanLogItem` の `id`・`report_id`・`unscheduled` の説明: 「予定外の行のアドバイス・エリア別詳細は取得できない（画面は行を開かない）」（:2332）→「アドバイスは `/clean-logs/unscheduled/{report_id}/advice` で取れる（画面は △× の行を開く）。エリア別詳細は取れない」
- ★ついでに誤記を直す★（`clean_task_reports` に `id` 列は無い。主キーは `report_id`。`prisma/schema.prisma:551`）。★次の5か所すべて★:
  - `d-watch-api.yaml` の `CleanLogItem`:
    - `id` の説明（:2331）の「`clean_task_reports.id` は19桁」→「`clean_task_reports.report_id` は19桁」
    - `row_key` の説明（:2338）の「`"rp-<clean_task_reports.id>"`」→「`"rp-<clean_task_reports.report_id>"`」
    - `report_id` の説明（:2344）の「`clean_task_reports.id`」→「`clean_task_reports.report_id`」
  - `lib/types/index.ts` の `CleanLogItem`:
    - `id` の docstring（:237）の「`clean_task_reports.id`」→「`clean_task_reports.report_id`」
    - `report_id` の docstring（:248）の「`clean_task_reports.id`」→「`clean_task_reports.report_id`」
  - 終わったら `grep -rn "clean_task_reports\.id" d-watch-api.yaml lib app components` が★0件★になることを確かめて、その出力を報告する（直す前は上の5件だけ。監督が 2026-11-04 に確認）
- `/clean-logs/{id}/areas` の「予定外タスクの行からは開かない」は★変えない★

## 3. 画面

- `lib/format/clean-result.ts` の `cleanLogRowView()`:
  - `expandable`: `!row.unscheduled && (△|×)`（:139）→ ★`△|×`★（結果待ちは今までどおり false）
  - ★戻り値に `areaDetail: boolean` を足す★ ＝ `expandable && !row.unscheduled`（「エリア別詳細を見る →」を出すか。結果待ちの分岐（:121-130）も false を返す。1行の見え方はこの関数の1か所で決める作法。`CleanLogTable.tsx:324`）
  - 型 `CleanLogRowView` の説明（:99-103「予定外タスクの行も開けない」）を直す
- 新規 ★`lib/clean-logs/advice-url.ts`★ の純粋関数 `cleanLogAdviceUrl(row: Pick<CleanLogItem, "id" | "unscheduled" | "report_id">): string | null`:
  - 予定あり（`unscheduled === false`）で `id` が数値 → `/api/clean-logs/${id}/advice`
  - 予定なし（`unscheduled === true`）で `report_id` が文字列 → `/api/clean-logs/unscheduled/${encodeURIComponent(report_id)}/advice`（★文字列のまま。`Number()` を通さない。`row_key` を分解しない★）
  - それ以外（予定ありなのに `id` が null、予定なしなのに `report_id` が null。API の不変条件では起きない）→ null（開かない）
- `DetailLogScreen.tsx` と `DashboardScreen.tsx` の `toggleRow()`（★2画面に同じ処理がある。両方直す★。`DetailLogScreen.tsx:265-` / `DashboardScreen.tsx:114-`）:
  先頭の `const logId = row.id; if (logId === null) return;` を ★`const url = cleanLogAdviceUrl(row); if (url === null) return;`★ に替え、取得を `apiFetch<CleanLogAdviceResponse>(url)` にする。先頭のコメント（「予定外タスクの行（`id: null`）は開かない」。:266 / :115）を直す。
  ★開いている行・キャッシュ・読み込み中・失敗は今までどおり `row_key` で持つ★（予定ありは `tt-…`、予定なしは `rp-…` で重ならない。キーを `id` に戻さない）
- `CleanLogTable.tsx`: 開いた中身の「エリア別詳細を見る →」ボタン（:477-487）を ★`view.areaDetail` が true のときだけ★描く。予定なしの行では何も代わりに出さない。
  `expandedKey` の説明（:23-26「予定外の行は開かないので…」）と :327 のコメントを直す
- ★コメント3か所も直す★（ボタンを隠すと事実と合わなくなる）:
  - `CleanLogTable.tsx:477`（`areaBtn` のボタンの上のコメント）「エリア別詳細はアドバイスの有無・取得の成否によらず出す」→「エリア別詳細は予定のある行だけ出す（`view.areaDetail`。予定の id が要る）。アドバイスの有無・取得の成否にはよらない（取得は別の API。指示書BC 2-1）」
  - `DetailLogScreen.tsx:290` と `DashboardScreen.tsx:139`（`toggleRow()` の catch。2画面とも）の「行は開いたまま。エリア別詳細のボタンは出る」→「行は開いたまま。予定のある行ならエリア別詳細のボタンは出る」
  - `openAreas()` のコメント（`DetailLogScreen.tsx:309`・`DashboardScreen.tsx:158`「予定外タスクの行（`id: null`）からは開かない」）は正しいままなので★変えない★
- ★`openAreas()` の `if (row.id === null) return;`（2画面。:310 / :159）は残す★（ボタンを隠したあとも保険。`AreaTarget.logId` は number なので外すと tsc が止まる）
- ★画面で `matchAdviceRules()` を呼んで作らない★（判定は API から受け取る。CLAUDE.md §9.6。文言も運用設定でサーバー側にある）
- `lib/types/index.ts`: `CleanLogItem.unscheduled`（:255「▼（アドバイス・エリア別詳細）を出さない」）・`id`（:235-240）の説明を「アドバイスは出す（△×）・エリア別詳細は出さない」に直す。
  あわせて `id`（:237）・`report_id`（:248）の `clean_task_reports.id` → `clean_task_reports.report_id`（§2-5 の誤記の修正を参照）

## 4. 期待値表（★監督が確定。テストはこれをそのまま使う★）

**表P — `parsePathReportId()`（`lib/api/__tests__/path-params.test.ts` か既存の置き場所に足す）**

| # | 入力 | 結果 |
|---|---|---|
| P-1 | `""` | 422。details `{ report_id: ["20文字以内で指定してください"] }`（`toEqual` で丸ごと） |
| P-2 | `"9200000000000000002"`（19桁） | ok・★文字列のまま★ `"9200000000000000002"` |
| P-3 | 20文字（`"1".repeat(20)`） | ok |
| P-4 | 21文字（`"1".repeat(21)`） | 422（P-1 と同じ details）。本文に入力値が出ない |
| P-5 | `"abc"` | ok（文字種は絞らない。ルートでは 404 になる） |

**表U — `cleanLogAdviceUrl()`（新規 `lib/clean-logs/__tests__/advice-url.test.ts`）**

| # | 行 | 結果 |
|---|---|---|
| U-1 | `{ id: 54, unscheduled: false, report_id: null }` | `"/api/clean-logs/54/advice"` |
| U-2 | `{ id: null, unscheduled: true, report_id: "9200000000000000002" }` | `"/api/clean-logs/unscheduled/9200000000000000002/advice"` |
| U-3 | `{ id: null, unscheduled: true, report_id: "a/b c" }` | `"/api/clean-logs/unscheduled/a%2Fb%20c/advice"`（`encodeURIComponent`） |
| U-4 | `{ id: null, unscheduled: false, report_id: null }` | `null` |
| U-5 | `{ id: null, unscheduled: true, report_id: null }` | `null` |

**表V — `cleanLogRowView()`（`lib/format/__tests__/clean-result.test.ts`。入力の `F`・`R`・`S`・`W` は同ファイル :138-155 の定義）**

| # | 行（入力） | `expandable` | `areaDetail` |
|---|---|---|---|
| V-1 | 予定あり・×（既存 C2-2 の `F`。:169） | true | true |
| V-2 | 予定あり・◎（既存 C2-3 の `S`。:180） | false | false |
| V-3 | 予定なし・×（★新しいテスト★。入力 `{ ...S, task_status: 6, completion_rate: 0, grade: "×", unscheduled: true }`。fixtures の `rp-9200000000000000002` と同じ形） | ★true★ | ★false★ |
| V-4 | 予定なし・△（★:191-202 の既存テストを書き換える★。入力 `{ ...W, unscheduled: true }` はそのまま） | ★true（false から変える）★ | false |
| V-5 | 予定なし・◎（★新しいテスト★。入力 `{ ...S, unscheduled: true }`） | false | false |
| V-6 | 結果待ち（予定あり。既存 C2-1 の `R`。:157） | false | false |

- V-3 の期待値（戻り値全体を `toEqual` で丸ごと）: `{ departed: "ok", resultLabel: "清掃キャンセル", unknownStatus: null, completionRate: 0, gradeShown: true, expandable: true, areaDetail: false }`
  （`task_status` 6 は 3 と同じ「清掃キャンセル」。`lib/format/clean-result.ts:14`）。★入力に `F`（`departed: false`）を使わない★ — 予定外は未実施にならないので、予定なしで出発失敗の行は存在しない
- V-5 の期待値（同じく丸ごと）: `{ departed: "ok", resultLabel: "清掃正常終了", unknownStatus: null, completionRate: 95.2, gradeShown: true, expandable: false, areaDetail: false }`（`S` の `completion_rate` は 95.2・`task_status` は 4。:147-154）
- V-4: テスト名を「予定なしの行は △× なら開ける（アドバイスあり）。エリア別詳細は出さない」に直す。ほかの欄（`departed` "ok"・`resultLabel`「清掃中断」・`unknownStatus` null・`completionRate` 45・`gradeShown` true）と、:200-201 の `CLEAN_LOG_UNSCHEDULED_LABEL`・`translate` の2行は★そのまま★残す
- 既存の toEqual への `areaDetail` の足し方:
  - 「C2-1・C2-2・C2-3・C2-4（:158・:170・:181・:205）には `areaDetail` を足す（期待値の他の欄は変えない。C2-1 false / C2-2 true / C2-3 false / C2-4 true）」
  - 「:192 の予定なしのテストは V-4 として `expandable` を true に変え、`areaDetail: false` を足す（ほかの欄は変えない）」

**表A — `GET /clean-logs/unscheduled/{report_id}/advice`**（3つの表に分ける。番号は A-1〜A-4 が fixtures、A-5〜A-10 がデータ層、A-6′〜A-9′・A-11〜A-13 がルート）

**表A-fixtures（A-1〜A-4。段階4）** — `USE_FIXTURES=true`。予定なしの2行は `lib/fixtures/clean-logs.ts:383-420` の `unscheduledCleanLogs`。連続日数は固定値なので日付が変わっても同じ。

★置き場所は新規 `app/api/clean-logs/unscheduled/[report_id]/advice/__tests__/route.fixtures.test.ts`★。
- ファイルの冒頭で `vi.mock("@/lib/db/use-fixtures", () => ({ USE_FIXTURES: true }))` と明示的に固定する。前例は `app/api/errors/__tests__/route.fixtures.test.ts:18` と `app/api/errors/[error_id]/status/__tests__/route.fixtures.test.ts:24`
- モックを省いて環境変数に任せない — `lib/db/use-fixtures.ts:17` は環境の `USE_FIXTURES=false`（`.env.local`）で false に変わる
- `lib/logger` の `logEvent` は前例と同じく `vi.fn()` に差し替える。ルートの `GET` はファイル先頭で静的 import する
- ★既存の `app/api/clean-logs/__tests__/route.test.ts`・`app/api/clean-logs/[id]/__tests__/route.test.ts` には A-1〜A-4 を足さない★ — どちらも `:235` / `:173` で `USE_FIXTURES: false` に固定しており、`vi.mock` はファイル単位、`lib/db/index.ts:3197` の `db` は読み込み時に束縛されるので、同じファイルでは true にできない。そこに書くと Prisma 経路が走り、A-1・A-2 は 404 で落ちる（A-3・A-4 は偶然通る）
- fixtures の行の日付は今日からの相対なので、`vi.setSystemTime` で今を固定するか、日付を直書きしない（§6 罠10）

| # | report_id | 結果 |
|---|---|---|
| A-1 | `9200000000000000002`（施設11・×・完了率0・連続1） | 200。`escalation_level` `"L1"`・`consecutive_problem_days` 1・`advices` の `rule` は ★`["R2"]` だけ★・`max_level` 1 |
| A-2 | `9200000000000000001`（施設1・◎・95.9・連続0） | 200。`escalation_level` null・`advices` `[]`（API は ◎ でも返す。画面が開かないだけ） |
| A-3 | `123`（存在しない） | 404（`not_found`・「指定された清掃ログが見つかりません」） |
| A-4 | 21文字 | 422（details のキーは `report_id`） |

**Prisma のスタブ（表A-db・表A-route 共通）** — `app/api/clean-logs/__tests__/route.test.ts` の予定なしのスタブ・`unscheduledReport()` ヘルパー・OR に対応した `matchesWhere`（:144-180）を使う。★`[id]/__tests__/route.test.ts` のスタブ（:126-）は OR を無視するので、そちらに書くなら OR 対応を移すこと★。

- ★スタブの `matchesWhere` は `OR` だけを解釈し、`AND` は黙って無視する★。`buildData()` の予定ありのレポート（`report()` の行）には `section`・`report_id` が無い。そのため where の条件が外れると、`sortIncludedRobots()`（:222-233）が TypeError を出す。
  ★A-5〜A-10 が赤いときは、まず where が平らに展開されているか（§2-3 手順1）を確かめる。スタブ側を書き換えて通さない★（例外は下の `report_id` の照合順序の再現だけ）
- ★置き場所★: 既存の `describe("予定外タスクの行（指示書BG 表U1）")`（:1116-1266）の中に `it` として足す。`unscheduledReport()`・`RP1`・`RP2` はその describe の中のローカル定義（:1118-1147）なので、外の describe からは参照できない。外に新しい describe を作るなら、先にこの3つをファイルの最上位（`get()`（:384）の近く）へ移す（既存の U1 テストの中身は変えない）
- ★ルートの import★: 新しいルートの `GET` は、ファイル先頭で静的 import する（ファイル冒頭 :8-10 の作法。`it` の中で `await import()` しない）。例: `import { GET as GET_UNSCHEDULED_ADVICE } from "@/app/api/clean-logs/unscheduled/[report_id]/advice/route";`
- ★`report_id` の列だけ照合順序を再現する★（A-11 のため）: `matchesWhere` で `field === "report_id"` かつ `cond` が文字列のときだけ、`typeof value === "string" && value.replace(/ +$/, "").toLowerCase() === cond.replace(/ +$/, "").toLowerCase()` で比べる（ci＋末尾の空白を無視する PAD SPACE。`report()` の行のように `report_id` を持たない行は不一致）。ほかの列の比較（`value !== cond`）は変えない。
  前例は errors / prechecks の status のテストの `ciEqual`（`app/api/errors/[error_id]/status/__tests__/route.test.ts:93`。こちらは大小文字だけ。D-4）。★手順3を消したら A-11 が 200 になって赤くなること★
- ★`logApiError` を見る A-12 のため★、このファイルに `vi.mock("@/lib/logger/api", () => ({ logApiError: vi.fn() }))` を足す（`[id]/__tests__/route.test.ts:175` と同じ）。このファイルには今 `logApiError` を見るテストが無い（監督が 2026-11-04 に grep で0件を確認）ので、ファイル全体に効いても既存のテストは変わらない

**表A-db（A-5〜A-10。`db.unscheduledCleanLog()`。段階3）**

| # | 場面 | 結果 |
|---|---|---|
| A-5 | `db.cleanLogs({ id: null, date_from: null, date_to: null, facility_ids: null })` が返す予定なしの行（`unscheduled === true`。ルートの `capCleanLogs()` を通す前のデータ層の値）それぞれについて `db.unscheduledCleanLog(row.report_id!)` | ★その行と `toEqual` で一致★（`consecutive_problem_days`・`escalation_level` まで。数値を手で書き写さない）。★一覧の API（`GET /clean-logs`）の応答とは比べない★ — ルートの `capCleanLogs()`（`app/api/clean-logs/route.ts:107`）が各行に `consecutive_problem_days_capped: false` を付けるが、データ層の1行（`unscheduledCleanLogItem()`）にはこのキーが無く、`toEqual` が必ず失敗する（`toEqual` が無視するのは値が undefined のキーだけ）。どうしても API の応答と比べるなら `capCleanLogs([db の1行], maxDays)[0]` と比べる。この例には、連続日数が表示上限を超える行を入れない（上限を超えると生値は経路で違いうる。§2-3 の4） |
| A-6 | 施設1で 9/8〜9/10 が3日連続 × の例（既存 :1243-1249 と同じく `unscheduledReport(RP1, 1, FACILITY_1, "2026-09-10T21:30:00+09:00", 959, 1000)`。◎） | 行を返す。`grade` "◎"・`consecutive_problem_days` 3・`escalation_level` "L3" |
| A-7 | 予定のあるレポート（`unscheduledReport()` の第7引数に本清掃の id（例 `103n`）を渡した行）の `report_id` | null を返す（★例外にならない★） |
| A-8 | `clean_timetable_id` が `0n` の行と `null` の行 | どちらも行を返す（null でない。どちらも予定なし） |
| A-9 | 削除済みの施設の予定なしの行（U1-5（:1208-1223）と同じ作り方） | null を返す。＋★SQL の where に可視性と予定なしの条件が載っている★: `calls.cleanTaskReport.find((c) => "include" in c)` の `where` が `toMatchObject(FACILITY_SECTION_VISIBLE_WHERE)`（U1-5 の :1221-1222 と同じ形）、かつ `toMatchObject(UNSCHEDULED_TIMETABLE_ID_WHERE)`（`@/lib/db/timetable-link` から import）。AND で包むとこの toMatchObject が落ちるので、展開の形が担保される |
| A-10 | 連続日数の読み込みの範囲 | ★スタブに発行された where で確かめる（既存 #8「連続日数用のクエリは表示期間＋上限ぶんの遡りで絞る」:479-499 と同じ作法）★。行の JST の日付を `d` とする。<br>(a) 連続日数用の予定のクエリ ＝ `calls.cleanTimetable` のうち `select` を持ち `select.facility_id === true` のもの。その `where.start_at.lt.toISOString()` ＝ `` `${d の翌日}T00:00:00.000Z` ``。`where.start_at.gte` の日付部分（`toISOString().slice(0, 10)`）＝ `d` から `consecutiveLookbackCalendarDays(await loadConsecutiveDaysMax())` 日前。`where.facility_id` ＝ `{ in: [BigInt(その行の facility_id)] }`。<br>(b) 予定外の3本目 ＝ `calls.cleanTaskReport` のうち `select` を持ち `select.facility_id === true` のもの（その行を引く1本目は `include` 付き、予定の IN で引く2本目は `select` に `facility_id` が無いので区別できる。`lib/db/consecutive-source.ts:566-` の2本目）。その `where.started_at` と `where.facility_id` が (a) と同じ。<br>★`vi.mock("@/lib/db/consecutive-source")` や `vi.spyOn` で `loadConsecutiveInputs` を差し替えない★（`lib/db/index.ts` はモジュールの中で直接呼んでいるので spyOn では差し替えられない。vi.mock はファイル全体に効き、既存の #8 と一覧の連続日数のテストが壊れる）。★遡りの日数を数値で直書きしない★（保存された表示上限から導く） |

**表A-route（A-6′〜A-9′・A-11〜A-13。`GET /clean-logs/unscheduled/{report_id}/advice` をルート経由で。段階4）**

| # | 場面 | 結果 |
|---|---|---|
| A-6′ | A-6 と同じデータ | 200。`escalation_level` `"L3"`・`consecutive_problem_days` 3・`consecutive_problem_days_capped` false・`advices` の `rule` は ★ちょうど `["R3", "R4"]`★（`toEqual`。R3 も同時に出る — `lib/judge/advice.ts:82-90`。R4 だけではない。◎ なので R2・B1 は当たらず、出発済みなので R1、事前チェックが null なので B2・B3 も当たらない）・`max_level` 3。★この行は ◎（95.9）なので画面では ▼ が出ず開かない（表V の V-5）が、API は返す（A-2 と同じ）★ |
| A-7′ | A-7 と同じデータ | 404（`not_found`・「指定された清掃ログが見つかりません」）。★500 にならない★ |
| A-8′ | A-8 と同じデータ（0 の行と NULL の行） | どちらも 200 |
| A-9′ | A-9 と同じデータ（削除済みの施設の行） | 404（A-7′ と同じ本文） |
| A-11 | パスの `report_id` が `"9200000000000000002 "`（末尾に空白1つ・20文字。表P の P-3 と同じく 422 にならない長さ）。DB に入っている行は `"9200000000000000002"`（予定なし・生きている施設） | 404（`not_found`）。★さらに、スタブの `clean_task_reports` の findMany（`include` 付きの1本目）がその行を返していた（SQL では当たった）ことも確かめる★（スタブの `calls` は引数しか残さないので、`calls.cleanTaskReport.find((c) => "include" in c)` の `where` に対して、同ファイルの `matchesWhere(その行, where)` が true になることで確かめる）。これで 404 の理由が「SQL が空を返した」ではなく「§2-3 手順3の JS の `===` が弾いた」ことの証拠になる（上の照合順序の再現が前提） |
| A-12 | 新しい入口で `loadAdviceRules()` が例外（`__setAdviceStoreForTesting()` で壊れたストアを入れる。`[id]/__tests__/route.test.ts:925-946` の T3 が手本。`finally` で `null` に戻す） | 500・`{ error: { code: "internal_error", message: "データの取得に失敗しました", details: null } }`。`logApiError` が1回呼ばれ、第1引数が `{ route: "/api/clean-logs/unscheduled/[report_id]/advice", method: "GET", status: 500 }` に `toMatchObject` |
| A-13 | A-8′ の NULL の行（予定なし・生きている施設）で 200 のとき、`logEvent` の呼び出し | `judge.advice` の第2引数が `{ route: "/api/clean-logs/unscheduled/[report_id]/advice", report_id: <その行の report_id> }` を含み、★`clean_log_id` のキーを持たない★（`"clean_log_id" in payload` が false）。`judge.escalation` の `route` も同じ値 |

**既存（回帰）**: `GET /clean-logs/{id}/advice` のテスト（`app/api/clean-logs/[id]/__tests__/route.test.ts`）は★変えずに★全部通ること（共通関数への切り出しで挙動が変わっていない証拠）

## 5. 段階（この順で）

1. `d-watch-api.yaml`（2-5。誤記の5か所と grep の確認も）
2. `lib/config/index.ts`・`lib/api/path-params.ts`（2-2）＋表P
3. `lib/db/unscheduled-clean-log-source.ts`・`lib/db/index.ts`（2-3）＋★表A-db（A-5〜A-10）★。スタブの `report_id` の照合順序の再現もここで足す
4. `lib/clean-logs/advice-response.ts`（共通関数）・`[id]/advice/route.ts` の置き換え → ★既存の `[id]/advice` のテストが変えずに通ることを先に確かめる★ → 新しいルート＋★表A-fixtures（A-1〜A-4。新規 `app/api/clean-logs/unscheduled/[report_id]/advice/__tests__/route.fixtures.test.ts`）と表A-route（A-6′〜A-9′・A-11〜A-13。`app/api/clean-logs/__tests__/route.test.ts`）★。
   `lib/logger/events.ts`（`JudgeAdviceLogEvent` の `report_id`・`SettingsStoreCorruptLogEvent` の説明。§2-4）・`docs/logging-design.md`（§2-4 の3か所）
5. `lib/format/clean-result.ts`＋表V、`lib/clean-logs/advice-url.ts`＋表U、`CleanLogTable.tsx`・`DetailLogScreen.tsx`・`DashboardScreen.tsx`（コメント3か所も。§3）、`lib/types/index.ts` の説明（誤記の2か所も）
6. docs を新しい入口・新しい動きに合わせる:
   - `docs/code-map.md`: API 一覧（:932 の `/api/clean-logs/[id]/advice` の行の後。画面の呼び出し列つき）とファイル一覧（:4118 の後）に新しい入口を足す。
     :596 の「★『エリア別詳細を見る →』はアドバイスの有無・成否によらず出します★」を「★予定ありの行では、アドバイスの有無・成否によらず出します。予定なしの行には出しません★（指示書BI）」に直す
   - `docs/fixtures.md:123`: 「▼ を出さない（行を開かない）」を「△× の行（施設11 の ×）は ▼ でアドバイスが開く（`/api/clean-logs/unscheduled/{report_id}/advice`）。エリア別詳細を見る → は出ない。◎ の行（施設1）は ▼ なし」に替える。
     同じ行の「予定外（EN "Unscheduled"）」「CSV の『予定時刻』列も『予定外』」は、指示書BH §13 に合わせて「予定なし（EN "No schedule"）」に直す
   - `docs/spec.md:66`（F-02 の API）: `/clean-logs/unscheduled/{report_id}/advice` を足す
   - `docs/architecture.md:81` の後: `| \`app/api/clean-logs/unscheduled/[report_id]/advice/route.ts\` | 予定なしの行のアドバイス取得 | 実装済み |` の行を足す
   - `docs/logging-design.md` は段階4で直した（:204・:253・:289・:487）。ここで漏れが無いか見直す
7. tsc・lint（触ったファイル）・vitest 全件の件数と、段階ごとの変更ファイルを報告（★報告は日本語★）

## 6. 既知の罠

1. ★`expandable` だけを true にすると、2画面の `toggleRow()` の `if (logId === null) return;` で止まる★ — ▼ は出るのに押しても開かない。型エラーもテストの失敗も出ない（画面で押さないと分からない）
2. ★`toggleRow()` は2画面にある★。片方だけ直すと、詳細ログでは開くのにダッシュボードでは開かない。
   ★ダッシュボード側は fixtures の画面で予定なしの行を開けない（§7）。直したかどうかは grep でしか確かめられない★
3. ★「エリア別詳細を見る →」は今は無条件で出る★ — 隠さないと、予定なしの行で「押しても何も起きないボタン」になる（`openAreas()` が黙って return する）
4. ★`report_id` を数値にしない★（19桁は安全な整数を超える。`Number()` で末尾が丸まる）。`parsePathId()` を流用すると全件 422 になる
5. ★予定のある `report_id` を `unscheduledCleanLogItem()` に直接渡すと例外 → 500★。`unscheduledCleanLogRowsFromReports()` を通して 404 にする
6. ★`findUnique({ where: { report_id } })` だけで引くと、予定なしの条件と可視性（削除済み）が SQL から抜ける★
7. ★`UNSCHEDULED_TIMETABLE_ID_WHERE` はトップレベルの `OR`★。同じ階層に別の `OR` が来るときだけ `AND: [...]` で包む（前例: `findUnscheduledErrors()`。`lib/db/unscheduled-report-source.ts:95-96`）。
   ★今回は OR が1つなので包まない★。包むと、テストのスタブ（`app/api/clean-logs/__tests__/route.test.ts` の `matchesWhere`）が `AND` を解釈せず条件が全部消える
8. ★連続日数の範囲を null（全期間・全施設）で渡さない★（重い。行の日付と施設が先に分かるので絞る）。日付は JST の `started_at_iso` の先頭10文字
9. fixtures の予定なしの行の連続日数は固定値（0 と 1）。fixtures で一覧と一致しても Prisma 版が一致する保証にはならない → 表A-db の A-5 は Prisma のスタブで確かめる
10. fixtures の予定なしの行の日付は今日からの相対（`at(daysBack)`）。テストに日付を直書きしない
11. ★`app/api/clean-logs/[report_id]/` は作らない★（`[id]` と同じ階層に名前の違う動的フォルダ → ビルドで止まる）
12. ★判定とログには生値、`capConsecutiveDays()` は応答だけ★。共通関数に切り出すときも順序を崩さない。`loadAdviceRules()` の失敗は 500、`loadConsecutiveDaysMax()` の失敗は既定値で続行 — この非対称をそろえてしまわない
13. 一覧を出した時点と ▼ を開いた時点で「今」が違うので、同じ施設・同じ日の予定のある行が結果待ちから未実施に変わると、連続日数が一覧とずれうる（予定のある行のアドバイスにも同じ性質がある。今回直さない）
14. ★`clean_task_reports.id` は誤記★（YAML :2331・:2338・:2344、`lib/types/index.ts` :237・:248 の5か所。§2-5 で全部直す）。新しいパラメータの説明・関数コメントに写さない
15. ★一覧の API の応答には `capCleanLogs()` が `consecutive_problem_days_capped` を付ける。データ層の1行には無い★。A-5 で API の応答と `toEqual` で比べると全行が失敗する（データ層の `db.cleanLogs()` と比べる）
16. ★A-10 は `loadConsecutiveInputs` をモックして引数を見るのではなく、スタブの `calls` に残った where で確かめる★（`route.test.ts` の #8（:479-499）が手本）
17. ★手順3（`===` の再確認）を落としても、照合順序を再現しないスタブでは緑のまま★。A-11 はスタブ側で `report_id` の照合順序を再現して初めて意味を持つ

## 7. 画面で確かめること（fixtures。`USE_FIXTURES=true npm run build` → `USE_FIXTURES=true npx next start -p 3100`。★:3000 は触らない★。終わったら :3100 を止める）

- 清掃詳細ログ: 「全件」＋「予定なし」で2行。★× の行（施設11）に ▼ が出る★。
  押すと、見出し「⚠ レベル1 — 現場確認が必要」（`max_level` 1。`CleanLogTable.tsx` の `adviceHead()`、:58-78）と、カード「⚠ 完了率が極端に低い（R2）」の★1枚だけ★が出る（R1・B1 のカードは出ない）。
  ★画面には `escalation_level`（"L1"）を出す箇所が無いので、「L1」という文字は探さない。表示も足さない★。L1 であることは API の応答（表A-fixtures の A-1: `GET /api/clean-logs/unscheduled/9200000000000000002/advice` が `escalation_level: "L1"`・`max_level: 1`・`advices` の rule が `["R2"]` だけ）で確かめる。
  カードの題と見出しは運用設定・既定のルール（`lib/config/advice-rules.ts:49-62`）の文言で決まる。fixtures の既定値と違って見えるときは、文言の一致にこだわらず「見出しがレベル1」と「R2 のカード1枚」の2点を確かめる。
  「エリア別詳細を見る →」は★出ない★。◎ の行（施設1）は ▼ が出ない
- 予定ありの △× の行: 今までどおり ▼ で開き、アドバイスと「エリア別詳細を見る →」が出て、エリア別詳細も開く
- ダッシュボードの小さい表: fixtures の直近モードでは、通常の時間帯に予定なしの行は `problem_logs` に出ない（監督側の点検役が 2026-11-04 16:34 に確認。監督は再現していない。施設11 は今日 03:30 の結果待ちの行（`lib/fixtures/clean-logs.ts:367`）が直近に選ばれ、結果待ちは `problem_logs` に出ないため。予定なしの × が出るのは `?date=` を指定したときだけで、ダッシュボードの画面には日付を渡す手段が無い — `DashboardScreen.tsx:88`）。
  ★画面での確認はしない★。★fixtures に行を足さない、時刻もずらさない★（表A-fixtures の A-1・A-2 と、件数を縛る既存のテストが崩れる）。
  代わりに、§6 罠2 の確認として次のコマンドを実行し、出力を報告に貼る。
  `grep -n "cleanLogAdviceUrl\|logId\|/advice" components/features/dashboard/DashboardScreen.tsx components/features/detail-log/DetailLogScreen.tsx`
  期待する結果は次の3点。
  (a) 両ファイルとも `toggleRow()` の中に `cleanLogAdviceUrl(row)` と `apiFetch<CleanLogAdviceResponse>(url)` がある。
  (b) `toggleRow()` の中に `logId` と、`/api/clean-logs/${...}/advice` の直書きが残っていない（`AreaTarget.logId`・`openAreas()` の `logId: row.id`・`areaLogId` の行は `toggleRow()` の外なので残ってよい）。
  (c) `openAreas()` の `if (row.id === null) return;` は残っている（§3 のとおり）
- 開いた行を閉じて別の行を開く・予定ありと予定なしを交互に開く・同じ行を開き直す（2回目は取り直さない）が今までどおり動く
- ブラウザのネットワークで、予定なしの行は `/api/clean-logs/unscheduled/<report_id>/advice`、予定ありの行は `/api/clean-logs/<id>/advice` を呼んでいる
- JP/EN の切り替えで、開いた中身の文言とはみ出しが今までどおり

## 8. 共通ルール

- 期待値表（§4）を先にテストにする。数字・順番を変えない。合わないと思ったら実装を変えずに監督に報告
- `git add -A` 禁止。★コミットはしない（監督がまとめてコミットする）★
- 触らない: `docs/decisions.md`・`docs/handoff/指示書AY_*`・`docs/handoff/設計_制御盤ログの取り出し_*`・`components/features/errors/ErrorContentPicker.tsx`・`lib/errors/error-content-label.ts` とそのテスト・`prisma/`・`CLAUDE.md`
- DB に書き込まない。サーバー（:3000）を起動・停止しない
- 完了したら tsc・lint（触ったファイル）・vitest 全件の件数と、段階ごとの変更ファイルを報告する

## 9. 決定の記録（監督が `docs/decisions.md` に後で書く。Cursor は書かない）

- 予定なしの △× の行にもアドバイスの ▼ を出す（ユーザー回答「出す」。2026-11-04）。指示書BG §2-1 の「予定外の行は開かない」のうち、アドバイスの部分を取り消す。エリア別詳細は出さないまま
- 入口は `GET /clean-logs/unscheduled/{report_id}/advice`（`{id}` は整数の契約なので別の入口。`unscheduled` は既存の名前とそろう）
- データ層は予定なしの1行専用の読み口（`CleanLogDbQuery` に足すと波及が大きい）。連続日数は行の日付と施設に絞って読む（表示上限以下なら一覧と同じ値）
- 却下: 画面で `matchAdviceRules()` を呼ぶ案（§9.6 違反）、`row_key` を受ける1本の入口（予定のある行の入口が2本になり、`row_key` が API の入力になる）、`/clean-logs/{id}/advice?kind=report`（整数の契約を壊す）、`CleanLogDbQuery` に `report_id` を足す案
- 点検（2026-11-04）: 4観点で29件の指摘、反証で26件が本物（重複をまとめて約15項目）。棄却3件: U4 との食い違い（新しい入口には及ばない）、A-10 の観測方法についての2件（スタブの where で確かめる形に決めた）
