> **【注記】** 2026-09-03、TeamWorks 提供の実DDL（`clean_watch_20260902.sql`）と手書き `prisma/schema.prisma` の答え合わせのために作成した記録です（`prisma db pull` による方法）。
> 同じ照合を別方法（DDL文の直接パース）で行った `ddl-diff-parse.md` と対になる相互検証の記録です。
> **当時の記録であり、現行方針ではありません。** 以降の決定は `docs/decisions.md` と `docs/handoff.md` を参照してください。

# 30 — 実DDL と 手書き schema.prisma の答え合わせ（prisma db pull）

- 実施日: 2026-09-03
- 入力DDL: `C:\Users\daich\Downloads\clean_watch_20260902.sql`（46,390 bytes / `CREATE TABLE` 39個）
  - MySQL dump 10.13, Distrib 8.0.41 (Linux x86_64) / 元DB名 `clean_watch`
- 手書き版: `C:\Users\daich\dev\d-watch\prisma\schema.prisma`（28モデル / md5 `a1d48b7927c2b93171d3394986abbdf1`）
- 生成版: `<scratchpad>\ddl\pulled.prisma`（39モデル / 703行）
- 比較スクリプト: `<scratchpad>\ddl\compare.py` → 生出力 `diff.txt` / 構造化 `diff.json`

## 0. 作業条件（守った制約）

| 制約 | 実施内容 | 確認 |
|---|---|---|
| `prisma/schema.prisma` を上書きしない | `db pull --print` で標準出力へ出し、`--schema` にはscratchpadのスタブを指定 | 実行後 md5 変化なし / `git status` 空 |
| ローカルDB `dwatch_local` を壊さない | DDL は新規 `dwatch_ddl` へ投入 | `dwatch_local` のテーブル数 28 のまま |
| `.env.local` を書き換えない | `--url` フラグで接続先を一時的に上書き | ファイル無変更 |
| リポジトリへ書き込まない | 作業ファイルはすべて scratchpad 配下 | `git status` 空 |
| 手書き版を修正しない | 差分抽出のみ。修正は行っていない | — |

**Laravel 基盤10テーブルは比較対象外として除外した**:
`users` / `sessions` / `jobs` / `migrations` / `cache` / `cache_locks` / `failed_jobs` / `job_batches` / `password_reset_tokens` / `personal_access_tokens`
→ 業務テーブルは DDL 側 **29個**、手書き側 **28モデル**。

---

## 1. DDL 読み込みの結果

**成功。エラー・警告は0件。**

### 1-1. 事前確認した内容

| 確認項目 | 結果 |
|---|---|
| 文字コード | `/*!50503 SET NAMES utf8mb4 */`。各テーブル `DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci` |
| `CREATE DATABASE` 文 | **無し** |
| `USE` 文 | **無し**（→ 意図しないDBに入る危険は無いが、投入先をコマンドラインで明示する必要がある） |
| `SET` 文 | `TIME_ZONE='+00:00'` / `SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` / `SQL_NOTES=0` |
| 外部キーチェック無効化 | あり（`FOREIGN_KEY_CHECKS=0`, `UNIQUE_CHECKS=0`）。バージョンガード付きコメント形式 |
| `INSERT` / `VIEW` / `TRIGGER` / `PROCEDURE` | **無し**（純粋なスキーマのみ。データは含まれない） |
| `DROP TABLE IF EXISTS` | 各テーブルの前に有り |

`USE` 文が無いため、投入先DBは接続時に明示した。

### 1-2. 実行コマンドと結果

```
mysql -u root -e "CREATE DATABASE dwatch_ddl CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -u root --default-character-set=utf8mb4 dwatch_ddl < clean_watch_20260902.sql
```

- 終了コード 0 / 標準エラー出力なし
- `information_schema` 上のテーブル数: `dwatch_ddl` = **39**（DDLの `CREATE TABLE` 数と一致）
- `dwatch_local` = 28（投入前後で不変）
- サーバは MySQL 8.4.9、dump 元は 8.0.41。バージョン差による失敗は発生しなかった。

### 1-3. db pull

Prisma 7 では `datasource` ブロックに `url` を書けない（`P1012`）。
最初にスタブへ `url = env(...)` を書いて1度失敗したため、`url` を削って `--url` フラグで渡す形に変更し成功した。
この失敗はスタブ側の書き方の問題で、DDL/DBには影響していない。

```
npx prisma db pull --print \
  --schema="<scratchpad>/ddl/stub.prisma" \
  --url="mysql://root@localhost:3306/dwatch_ddl"
```

終了コード 0。生成結果 39モデル。

---

## 2. テーブル単位の差分

**差分 1件。**

| 種別 | テーブル | 内容 |
|---|---|---|
| 手書きに無い | `clean_link_schedules` | 定義書に存在しなかった新テーブル。**手書き版に未実装** |
| 生成版に無い | （無し） | 手書き版の28モデルはすべて実DDLに存在した |

業務テーブル 29 のうち 28 は名前レベルで一致。`@@map` の物理名も全件一致した。

### 2-1. `clean_link_schedules` の全カラム（新テーブル）

`link_schedules` と**構造が完全に同一**（カラム名・型・既定値・NULL許容・FK先すべて）。
唯一の違いは制約名の接頭辞（`clean_link_schedules_section_id_foreign`）と、`link_schedules` 側に `AUTO_INCREMENT=10` が入っている点（データ有無の差）。

| # | カラム | DDL型 | NULL | 既定値 | Prisma |
|---|---|---|---|---|---|
| 1 | `id` | `bigint unsigned` AUTO_INCREMENT | NOT NULL | — | `BigInt @id @default(autoincrement()) @db.UnsignedBigInt` |
| 2 | `section_id` | `bigint unsigned` | NOT NULL | — | `BigInt @db.UnsignedBigInt` |
| 3 | `type` | `tinyint unsigned` | NOT NULL | `1` | `Int @default(1) @db.UnsignedTinyInt` |
| 4 | `weeks` | `varchar(7)` | NOT NULL | — | `String @db.VarChar(7)` |
| 5 | `intervals` | `varchar(120)` | NOT NULL | — | `String @db.VarChar(120)` |
| 6 | `inactive` | `tinyint unsigned` | NOT NULL | `0` | `Int @default(0) @db.UnsignedTinyInt` |
| 7 | `created_at` | `timestamp` | NOT NULL | `CURRENT_TIMESTAMP` | `DateTime @default(now()) @db.Timestamp(0)` |
| 8 | `updated_at` | `timestamp` | NOT NULL | `CURRENT_TIMESTAMP` **ON UPDATE** `CURRENT_TIMESTAMP` | `DateTime @default(now()) @db.Timestamp(0)`（`@updatedAt` 相当） |
| 9 | `deleted_at` | `timestamp` | NULL | `NULL` | `DateTime? @db.Timestamp(0)` |

- 主キー: `PRIMARY KEY (id)`
- インデックス: `KEY clean_link_schedules_section_id_foreign (section_id)`
- 外部キー: `section_id → sections(id)`（`ON DELETE` / `ON UPDATE` 指定なし = `NoAction`）
- ユニーク制約: なし

**`link_schedules` と `clean_link_schedules` が両方存在し中身が同一**という点は、どちらを参照すべきか TeamWorks に要確認（旧テーブルの残置か、用途別の並存か判断できない）。

---

## 3. カラム単位の差分（テーブルごと）

カラムの**有無**の差分は 14件（生成版にあって手書きに無い 3 / 手書きにあって実DDLに無い 11）。
うち 3件は**カラム名の綴り違い（リネーム）**で、実質は「6件のリネーム片側」＋「8件の実在しないカラム」。

業務カラム総数: 手書き 311 / 生成版 312。

### 3-1. 生成版にあって手書き版に無いカラム（3件）

| テーブル | カラム | 型 | NULL | 備考 |
|---|---|---|---|---|
| `robot_error_reports` | `responded_at` | `DateTime? @db.DateTime(0)` | 可 | 手書きは `responsed_at`（下記リネーム） |
| `control_error_histories` | `responded_at` | `DateTime? @db.DateTime(0)` | 可 | 手書きは `responsed_at`（下記リネーム） |
| `control_reports` | `serial_number` | `String @db.VarChar(100)` | 不可 | 手書きは `sireal_number`（下記リネーム） |

### 3-2. 手書き版にあって実DDLに無いカラム（11件）

| テーブル | カラム | 手書きの型 | 判定 |
|---|---|---|---|
| `robot_error_reports` | `responsed_at` | `DateTime? @db.DateTime(0)` | **リネーム** → `responded_at` |
| `control_error_histories` | `responsed_at` | `DateTime? @db.DateTime(0)` | **リネーム** → `responded_at` |
| `control_reports` | `sireal_number` | `String @db.VarChar(100)` | **リネーム** → `serial_number` |
| `clean_schedules` | `started_at` | `DateTime? @db.DateTime(0)` | **実DDLに存在しない** |
| `clean_schedules` | `ended_at` | `DateTime? @db.DateTime(0)` | **実DDLに存在しない** |
| `clean_statuses` | `deleted_at` | `DateTime? @db.Timestamp(0)` | **実DDLに存在しない**（論理削除なし） |
| `clean_timetable_histories` | `inactive` | `Int @default(0) @db.UnsignedTinyInt` | **実DDLに存在しない** |
| `robots` | `task_id` | `String? @db.VarChar(20)` | **実DDLに存在しない**（`robot_task_versions` 側にある） |
| `robots` | `version` | `String? @db.VarChar(20)` | **実DDLに存在しない**（`robot_task_versions` 側にある） |
| `section_robot_statuses` | `indicatived_at` | `DateTime? @db.Timestamp(0)` | **実DDLに存在しない** |
| `section_robot_statuses` | `deleted_at` | `DateTime? @db.Timestamp(0)` | **実DDLに存在しない** |

### 3-3. カラム名リネーム3件の DDL 原文（裏取り）

```sql
-- robot_error_reports
`responded_at` datetime DEFAULT NULL,
-- control_error_histories
`responded_at` datetime DEFAULT NULL,
-- control_reports
`serial_number` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
```

**`responsed_at` は定義書の typo であり、実カラム名は `responded_at`（d が入る）で確定。**
`control_reports.sireal_number` も定義書の typo で、実カラム名は `serial_number`。

---

## 4. 型・既定値・null許容の差分

### 4-1. 型（Prisma型）の差分 — 1件

| テーブル.カラム | 手書き | 生成版 | DDL原文 |
|---|---|---|---|
| `customer_contacts.contact_type` | `Int @db.UnsignedTinyInt` | **`BigInt @db.UnsignedBigInt`** | `bigint unsigned NOT NULL` |

**同名カラムなのに `facility_contacts.contact_type` は `tinyint unsigned`（`Int @db.UnsignedTinyInt`）**で、
実DDL上で顧客側と施設側の型が食い違っている。手書き版は両方 tinyint に揃えていたため顧客側が外れた。

### 4-2. `@db.*` 属性（桁数・符号）の差分 — 4件

| テーブル.カラム | 手書き | 生成版 | DDL原文 |
|---|---|---|---|
| `customer_contacts.contact_type` | `UnsignedTinyInt` | `UnsignedBigInt` | `bigint unsigned` |
| `facility_contacts.email` | `VarChar(100)` | **`VarChar(255)`** | `varchar(255)` |
| `robot_error_reports.serial_number` | `VarChar(50)` | **`VarChar(20)`** | `varchar(20)` |
| `robot_error_reports.product_code` | `VarChar(50)` | **`VarChar(20)`** | `varchar(20)` |

`customer_contacts.email` は `varchar(100)` のまま（一致）。`facility_contacts.email` のみ 255。

### 4-3. NULL 許容の差分 — 1件

| テーブル.カラム | 手書き | 生成版 | DDL原文 |
|---|---|---|---|
| `robot_error_reports.errored_at` | `DateTime`（必須） | **`DateTime?`（NULL可）** | `datetime DEFAULT NULL` |

同テーブルの `responded_at` / `resolved_at` / `product_code` などはいずれも手書きも NULL 可で一致。

### 4-4. 既定値の差分 — 15件

#### (a) 実装に影響する既定値差分（9件）

| テーブル.カラム | 手書き | 生成版 | DDL原文 |
|---|---|---|---|
| `clean_timetables.precheck` | `@default(9)` | **`@default(0)`** | `tinyint unsigned NOT NULL DEFAULT '0'` |
| `clean_timetables.parent_id` | `@default(0)` | **既定値なし** | `bigint unsigned NOT NULL`（DEFAULT句なし） |
| `robots.battery` | `@default(100)` | **`@default(0)`** | `tinyint unsigned NOT NULL DEFAULT '0'` |
| `robots.device_id` | `@default(0)` | **既定値なし** | `bigint unsigned NOT NULL`（DEFAULT句なし） |
| `clean_task_reports.created_at` | `@default(now())` | **既定値なし** | `timestamp NOT NULL`（DEFAULT句なし） |
| `clean_task_reports.updated_at` | `@default(now())` | **既定値なし** | `timestamp NOT NULL`（DEFAULT句なし） |
| `security_status_histories.reported_at` | `@default(now())` | **既定値なし** | `timestamp NOT NULL`（DEFAULT句なし） |
| `clean_status_histories.id` | `@default(autoincrement())` | **既定値なし** | `bigint unsigned NOT NULL`（AUTO_INCREMENT なし） |
| `clean_timetable_histories.id` | `@default(autoincrement())` | **既定値なし** | `bigint unsigned NOT NULL`（AUTO_INCREMENT なし） |

**履歴系2テーブルの `id` は AUTO_INCREMENT ではない**（採番はアプリ側 or 元テーブルの id を引き継ぐ設計）。
`clean_task_reports` の `created_at` / `updated_at` と `security_status_histories.reported_at` は
**DEFAULT句が無い `timestamp NOT NULL`** なので、INSERT 時に明示的に値を入れる必要がある。

#### (b) 数値リテラル表記のみの差分（6件・値は同一）

| テーブル.カラム | 手書き | 生成版 |
|---|---|---|
| `clean_statuses.task_area` | `@default(0)` | `@default(0.000000)` |
| `clean_statuses.clean_area` | `@default(0)` | `@default(0.000000)` |
| `clean_status_histories.task_area` | `@default(0)` | `@default(0.000000)` |
| `clean_status_histories.clean_area` | `@default(0)` | `@default(0.000000)` |
| `clean_task_reports.task_area` | `@default(0)` | `@default(0.000000)` |
| `clean_task_reports.clean_area` | `@default(0)` | `@default(0.000000)` |

DDL は `decimal(12,6) unsigned NOT NULL DEFAULT '0.000000'`。
**数値としては同一だがリテラル表記が異なるため差分として計上**した。生成版の表記に合わせるのが正。

### 4-5. 主キー / ユニークの差分 — 4件

| テーブル.カラム | 手書き | 生成版 | DDL原文 |
|---|---|---|---|
| `robot_error_reports.id` | `@id` | **`@id` なし** | `PRIMARY KEY` が**存在しない** |
| `robot_error_reports.error_id` | 制約なし | **`@unique`** | `UNIQUE KEY robot_error_reports_report_id_unique (error_id)` |
| `clean_status_histories.id` | `@id` | **`@unique`** | `PRIMARY KEY` なし / `UNIQUE KEY clean_status_histories_id_unique (id)` |
| `clean_timetable_histories.id` | `@id`（+autoincrement） | `@id`（autoincrement なし） | `PRIMARY KEY (id)` のみ |

**`robot_error_reports` には PRIMARY KEY が一つも無い。**
Prisma はモデルの一意識別子として `error_id @unique` を採用しており、
`id`（varchar(50)）は**ただのカラム**になっている。
つまり Prisma Client からは `findUnique({ where: { id } })` が使えず、`error_id` でしか一意引きできない。

**`clean_status_histories` も PRIMARY KEY が無く、`id` は UNIQUE KEY のみ。**
手書き版のコメント「定義書原文のまま: キー種別が他テーブルの pk ではなく uk（unique key）と記載されている」は
実DDLと合っていた（定義書の記述は正しかった）が、Prisma 属性は `@id` ではなく `@unique` が正。

### 4-6. `@updatedAt`（Prisma固有・introspectionでは生成されない）

`db pull` は `@updatedAt` を出力しないため §4-4 の機械比較には出てこないが、
DDL の `ON UPDATE CURRENT_TIMESTAMP` の有無で検証すると以下の不整合がある。

| テーブル.カラム | 手書き | DDL の `ON UPDATE CURRENT_TIMESTAMP` | 判定 |
|---|---|---|---|
| `clean_task_reports.updated_at` | `@updatedAt` | **無し**（DEFAULT句すら無い） | **誤り** |
| `facility_contacts.updated_at` | `@updatedAt` | **無し**（`DEFAULT CURRENT_TIMESTAMP` のみ） | **誤り** |
| `section_robot_statuses.updated_at` | `@updatedAt` | **無し**（`DEFAULT CURRENT_TIMESTAMP` のみ） | **誤り** |
| `security_statuses.reported_at` | 指定なし | **有り** | **付け漏れ** |
| 上記以外の `updated_at` 18件 | `@updatedAt` | 有り | 一致 |

---

## 5. リレーション（外部キー）の差分

**差分 3件。すべて「手書き版が実在しない外部キーを張っていた」方向。**
生成版にあって手書きに無い外部キーは **0件**（新テーブル `clean_link_schedules` の1本を除く）。

| テーブル | 手書きが張っていたFK | 実DDL |
|---|---|---|
| `clean_statuses` | `clean_timetable_id → clean_timetables(id)` | **FK なし**（`clean_timetable_id bigint unsigned NOT NULL` だがインデックスも無い） |
| `clean_status_histories` | `clean_timetable_id → clean_timetables(id)` | **FK なし**（同上） |
| `robot_task_versions` | `clean_schedule_id → clean_schedules(id)` | **FK なし**（`KEY` も無い。FKは `robot_id → robots(id)` の1本だけ） |

### 5-1. 特に確認を依頼された項目の結果

| 確認項目 | 結果 |
|---|---|
| `devices.maker_id` の型 | `tinyint unsigned NOT NULL`（DEFAULT句なし）= `Int @db.UnsignedTinyInt`。**手書き版と一致** |
| `makers` への外部キー | **存在しない。`makers` テーブル自体がDDLに含まれていない。**`devices` の制約は `PRIMARY KEY (id)` のみ。TeamWorks回答（A-1）どおりで、手書き版の判断は正しかった |
| `controls.facility_id` の外部キー | **存在する**: `CONSTRAINT controls_facility_id_foreign FOREIGN KEY (facility_id) REFERENCES facilities (id)`。`section_id → sections(id)` も存在。手書き版と一致 |

### 5-2. 業務テーブルの外部キー一覧（実DDL・全件）

すべて `ON DELETE` / `ON UPDATE` 指定なし → Prisma は `onDelete: NoAction, onUpdate: NoAction` を生成。

| テーブル | 外部キー |
|---|---|
| `customers` | `group_id → groups(id)` |
| `facilities` | `customer_id → customers(id)` |
| `sections` | `facility_id → facilities(id)` |
| `controls` | `facility_id → facilities(id)`, `section_id → sections(id)` |
| `robots` | `facility_id`, `section_id`, `device_id → devices(id)` |
| `robot_task_versions` | `robot_id → robots(id)` のみ |
| `robot_instruct_histories` | `robot_id → robots(id)` |
| `robot_error_reports` | `facility_id`, `section_id`, `robot_id` |
| `section_robot_statuses` | `facility_id`, `section_id` |
| `control_reports` | `facility_id`, `section_id`, `control_id → controls(id)` |
| `control_error_histories` | `facility_id`, `section_id`, `control_id` |
| `security_statuses` | `facility_id`, `section_id` |
| `security_status_histories` | `facility_id`, `section_id` |
| `clean_statuses` | `facility_id`, `section_id`（`clean_timetable_id` は無し） |
| `clean_status_histories` | `facility_id`, `section_id`（`clean_timetable_id` は無し） |
| `clean_task_reports` | `facility_id`, `section_id`, `robot_id`（`clean_timetable_id` は無し） |
| `clean_schedules` | `facility_id`, `section_id` |
| `clean_timetables` | `facility_id`, `section_id`, `clean_schedule_id → clean_schedules(id)` |
| `clean_timetable_histories` | `facility_id`, `section_id`, `clean_schedule_id` |
| `link_schedules` | `section_id → sections(id)` |
| `clean_link_schedules` | `section_id → sections(id)` |
| `customer_summaries` / `customer_error_summaries` / `customer_contacts` | `customer_id → customers(id)` |
| `facility_summaries` / `facility_error_summaries` / `facility_contacts` | `facility_id → facilities(id)` |
| `devices` / `groups` | なし |

### 5-3. インデックスの差分 — 6件

| テーブル | 差分 | 方向 |
|---|---|---|
| `clean_timetable_histories` | `[clean_schedule_id]` / `[start_at]` / `[end_at]` | 生成版のみ（手書きに欠落） |
| `control_reports` | `[control_id]` | 生成版のみ（手書きに欠落） |
| `link_schedules` | `[section_id]` | 生成版のみ（手書きに欠落） |
| `robot_task_versions` | `[clean_schedule_id]` | **手書きのみ**（実DDLに無い） |

補足: `clean_timetable_histories` のインデックス名は `..._started_at_index` / `..._ended_at_index` だが
対象カラムは `start_at` / `end_at`（DDL側の命名が実カラム名とずれている）。

---

## 6. 手書き版が正しかった点

**業務テーブル28のうち、比較項目すべてが完全一致したのは 12 テーブル。**

| # | テーブル | カラム数 |
|---|---|---|
| 1 | `groups` | 6 |
| 2 | `devices` | 8 |
| 3 | `customers` | 13 |
| 4 | `facilities` | 18 |
| 5 | `sections` | 9 |
| 6 | `controls` | 12 |
| 7 | `robot_instruct_histories` | 4 |
| 8 | `security_statuses` | 7 |
| 9 | `customer_summaries` | 16 |
| 10 | `facility_summaries` | 16 |
| 11 | `customer_error_summaries` | 10 |
| 12 | `facility_error_summaries` | 10 |

（カラムの有無・Prisma型・`@db.*`・NULL許容・既定値・主キー・ユニーク・インデックス・外部キーのすべてが一致。
`@updatedAt` のみ introspection では検証不能なため §4-6 で別途確認済み。`security_statuses.reported_at` の付け漏れ1件を除き整合。）

### 6-1. 判断に迷った箇所が正解だった例

| 箇所 | 手書き版の判断 | 実DDL | 判定 |
|---|---|---|---|
| `clean_statuses.clean_status` | `Int @db.TinyInt`（**符号あり** tinyint。A-2の回答で -1 があるため） | `tinyint NOT NULL DEFAULT '0'`（unsigned なし） | **正解** |
| `clean_status_histories.clean_status` | `Int @db.TinyInt`（符号あり） | `tinyint NOT NULL DEFAULT '0'` | **正解** |
| `customers.prefecture_cd` | `Int @db.TinyInt`（**符号あり**。facilities 側とは型が違うと明記） | `tinyint NOT NULL` | **正解** |
| `facilities.prefecture_cd` | `Int @db.UnsignedTinyInt`（符号なし） | `tinyint unsigned NOT NULL` | **正解**（両テーブルの型差も正しく再現） |
| `devices.maker_id` | `Int @db.UnsignedTinyInt` / `makers` へのFKは張らない | `tinyint unsigned NOT NULL` / FKなし・`makers`テーブル自体なし | **正解** |
| `facilities.shop_id` | `String @default("0") @db.VarChar(20)`（varchar なので文字列 "0" と判断） | `varchar(20) NOT NULL DEFAULT '0'` | **正解** |
| `facilities.contract` | `String @default("0000000000") @db.VarChar(10)` | `varchar(10) NOT NULL DEFAULT '0000000000'` | **正解** |
| `clean_schedules.start_time` / `end_time` | `String @db.VarChar(4)` | **`varchar(4)` NOT NULL** | **正解**（時刻を4桁文字列で持つ設計） |
| `clean_task_reports` の主キー | `report_id String @id @db.VarChar(20)` | `PRIMARY KEY (report_id)` | **正解**（id ではなく report_id が PK） |
| `robots.status` | `Int @db.SmallInt`（符号あり smallint） | `smallint NOT NULL DEFAULT '0'` | **正解** |
| `section_robot_statuses.robot_status` | `Int @db.SmallInt` | `smallint NOT NULL DEFAULT '0'` | **正解** |
| `customer_error_summaries.error_type` | `String @db.VarChar(100)` | `varchar(100) NOT NULL` | **正解** |
| `facility_error_summaries.error_type` | `String @db.VarChar(100)` | `varchar(100) NOT NULL` | **正解** |
| `customer_summaries` / `facility_summaries` の面積 | `Decimal @db.Decimal(14, 6)`（**12,6 ではなく 14,6**） | `decimal(14,6) unsigned NOT NULL` | **正解** |
| 同 比率カラム4種 | `operation_ratio` / `launch_ratio` / `clean_ratio` / `error_ratio` すべて `Int @db.UnsignedTinyInt`・**既定値なし** | `tinyint unsigned NOT NULL`（DEFAULT句なし） | **正解**（整数パーセント。小数は持たない） |
| `control_error_histories.occurred_at` | `DateTime @default(now()) @db.DateTime(0)` | `datetime NOT NULL DEFAULT CURRENT_TIMESTAMP` | **正解**（timestamp ではなく datetime に DEFAULT） |
| `robot_error_reports.error_detail` / `comment` | `String @db.Text` / `String? @db.Text` | `text NOT NULL` / `text`（NULL可） | **正解** |
| 命名規約 | フィールド名＝物理カラム名（snake_case）、`@@map` で物理テーブル名 | — | **正解**。`db pull` 出力との差分比較が単純な突き合わせで済んだ |
| `clean_status_histories.id` のキー種別 | 「pk ではなく uk と定義書に記載」とコメントで留保 | `PRIMARY KEY` なし / `UNIQUE KEY` のみ | **定義書の記述は正しかった**（Prisma属性の選択のみ要修正） |

`makers` テーブル、`controls` の facility FK、`clean_schedules.end_time` の型、
サマリの比率・面積の型と精度、error_type の型は、**すべて手書き版が実DDLと一致していた**。

---

## 7. 実装に影響する差分の優先順位

### P1 — 直さないと動かない / データが取れない（8件）

| # | 差分 | 影響 |
|---|---|---|
| 1 | `robot_error_reports.responsed_at` → **`responded_at`** | 存在しないカラム参照。エラー詳細画面・対応日時の取得が全滅 |
| 2 | `control_error_histories.responsed_at` → **`responded_at`** | 同上（制御機エラー履歴） |
| 3 | `control_reports.sireal_number` → **`serial_number`** | 存在しないカラム参照 |
| 4 | `robot_error_reports` に **PRIMARY KEY が無い** → `id` の `@id` を外し `error_id @unique` を一意識別子にする | `@id` を付けたままだと Prisma のスキーマ検証自体は通るが、実DBと不一致。`findUnique({id})` は使えない。API の主キー設計を `error_id` 基準に見直す必要がある |
| 5 | `clean_status_histories.id` は `@id` ではなく **`@unique`**（PRIMARY KEY 無し） | 同上。履歴の一意引きの前提が変わる |
| 6 | `customer_contacts.contact_type` が **`BigInt @db.UnsignedBigInt`**（facility 側は tinyint） | Int と BigInt で TypeScript の型が変わる（`number` vs `bigint`）。両contactを共通の型で扱うコードは破綻する |
| 7 | 手書きが張っていた**実在しない外部キー 3本**（`clean_statuses.clean_timetable_id`, `clean_status_histories.clean_timetable_id`, `robot_task_versions.clean_schedule_id`） | `include` / `select` でリレーション取得を書くと実DBにFKが無いため設計前提が崩れる。JOIN は手書きで行う必要がある |
| 8 | `clean_link_schedules` が**未実装** | 新テーブル。参照要否を TeamWorks に確認のうえモデル追加 |

### P2 — 実行時エラーやデータ欠損につながる（7件）

| # | 差分 | 影響 |
|---|---|---|
| 9 | `robot_error_reports.errored_at` が **NULL 可** | 必須前提のコードで null 参照。発生日時が無いレコードを表示側で扱う必要がある |
| 10 | `robot_error_reports.serial_number` `VarChar(50)` → **`VarChar(20)`** | 21文字以上の書き込みで切り詰め/エラー |
| 11 | `robot_error_reports.product_code` `VarChar(50)` → **`VarChar(20)`** | 同上 |
| 12 | `clean_task_reports.created_at` / `updated_at` が **DEFAULT句なしの `timestamp NOT NULL`** | INSERT時に必ず値を入れる必要がある。`@default(now())` 前提の書き込みは失敗する |
| 13 | `security_status_histories.reported_at` が **DEFAULT句なし** | 同上 |
| 14 | `clean_status_histories.id` / `clean_timetable_histories.id` が **AUTO_INCREMENT でない** | 履歴INSERT時に id を自前で決める必要がある |
| 15 | `@updatedAt` の不整合3件（`clean_task_reports` / `facility_contacts` / `section_robot_statuses`）＋ 付け漏れ1件（`security_statuses.reported_at`） | DBが自動更新しないカラムを自動更新扱いにしている（逆も1件）。更新時刻がずれる |

### P3 — 表示・既定値の意味が変わる（6件）

| # | 差分 | 影響 |
|---|---|---|
| 16 | `clean_timetables.precheck` 既定値 **9 → 0** | 事前チェック状態の初期値解釈が変わる。9 の意味（未設定？）は定義書由来なので TeamWorks 要確認 |
| 17 | `robots.battery` 既定値 **100 → 0** | 新規ロボットのバッテリー初期表示が 0% になる |
| 18 | `clean_timetables.parent_id` / `robots.device_id` の **既定値 0 が実DDLに無い** | INSERT時に明示指定が必要 |
| 19 | `facility_contacts.email` `VarChar(100)` → **`VarChar(255)`** | 入力バリデーションの上限を緩める（customer 側は 100 のまま） |
| 20 | 手書きにある**実在しないカラム 8件**（`clean_schedules.started_at`/`ended_at`, `clean_statuses.deleted_at`, `clean_timetable_histories.inactive`, `robots.task_id`/`version`, `section_robot_statuses.indicatived_at`/`deleted_at`） | 参照するとエラー。特に `clean_statuses` / `section_robot_statuses` は **論理削除カラムが無い**ので `deleted_at IS NULL` 条件は書けない。`robots.task_id`/`version` は `robot_task_versions` から取る |
| 21 | インデックス差分 6件 | 機能には影響しないが `@@index` を実DDLに合わせる（`robot_task_versions.clean_schedule_id` は実DDLにインデックスも無いため、この列での検索はフルスキャンになる点に注意） |

### P4 — 表記のみ（2件）

| # | 差分 | 影響 |
|---|---|---|
| 22 | Decimal 既定値のリテラル `0` → `0.000000`（6箇所） | 値は同一。生成版の表記に合わせる |
| 23 | `link_schedules` と `clean_link_schedules` の構造が完全同一 | どちらを使うか TeamWorks 要確認（機能影響は参照先の選択次第） |

---

## 8. 差分の総件数

| 区分 | 件数 | 内訳 |
|---|---|---|
| テーブル | **1** | 手書きに無い 1（`clean_link_schedules`）／生成版に無い 0 |
| カラム（有無） | **14** | 生成版のみ 3 ／ 手書きのみ 11（うち 3 はリネーム片側） |
| 型（Prisma型） | **1** | `customer_contacts.contact_type` |
| `@db.*` 属性 | **4** | contact_type / facility_contacts.email / serial_number / product_code |
| NULL 許容 | **1** | `robot_error_reports.errored_at` |
| 既定値 | **15** | 実質差分 9 ＋ リテラル表記のみ 6 |
| 主キー | **2** | `robot_error_reports.id` / `clean_status_histories.id` |
| ユニーク | **2** | `robot_error_reports.error_id` / `clean_status_histories.id` |
| インデックス | **6** | 生成版のみ 5 ／ 手書きのみ 1 |
| リレーション（FK） | **3** | すべて手書きのみ（実在しないFK） |
| （参考）`@updatedAt` | **4** | 誤り 3 ／ 付け漏れ 1。introspection では検証不能なため DDL の `ON UPDATE` で確認 |

- 完全一致したテーブル: **12 / 28**
- 手書き版の物理名（`@@map`）は 28/28 すべて一致

---

## 9. 生成物

| ファイル | 内容 |
|---|---|
| `pulled.prisma` | `db pull` 生成物（703行 / 39モデル）。**正とすべきスキーマ** |
| `pulled.raw` | 生成物の生出力（dotenv のログ2行を含む） |
| `schema.prisma.HANDWRITTEN.bak` | 比較時点の手書き版のコピー（md5 `a1d48b7927c2b93171d3394986abbdf1`） |
| `stub.prisma` | `db pull` に渡した最小スキーマ（generator + datasource のみ） |
| `compare.py` | モデル単位・フィールド単位の機械比較スクリプト |
| `diff.txt` | 比較の生出力 |
| `diff.json` | 比較結果の構造化データ |
| `pull.stderr.log` | `db pull` の標準エラー出力 |

投入先DB `dwatch_ddl` は残してある（不要になれば `DROP DATABASE dwatch_ddl;`）。
`dwatch_local` は一切変更していない。

**手書き版 `prisma/schema.prisma` は未修正。修正は別指示で行う。**
