/**
* 設定値(閾値の外出し)
*
* CLAUDE.md 7. 禁止事項:
* ❌ 閾値(90/70/10、猶予30分、L2=2日/L3=3日)のハードコード
*
* 判定ロジックや画面から直接数値を書かず、必ずここを参照してください。
* 将来的にはシステム管理画面の「運用設定」から変更できるようにします(F-10)。
*/
/**
* ◎○△× の判定閾値(`lib/judge/grade.ts` の `judgeGrade()` が唯一の消費者)
*
* ★★境界は「上のランクに入る」★★(★D-139★。ユーザー指示 2026-09-16
* 「◯、×は90%以上のように境はふくめてください」)。★D-020b(境界は下のランクに落ちる)は破棄★。
* ★★× の境界だけは「未満」のまま★★(同日「10%未満を×としてください」で確定)。
*/
export const GRADE_THRESHOLDS = {
/** 完了率がこの値「以上」で ◎(90.0 は ◎。D-139) */
excellent: 90,
/** 完了率がこの値「以上」で ○(70.0 は ○。D-139) */
good: 70,
/** 完了率がこの値を「下回る」と ×(10.0 は △ のまま。D-139 でも変わらない) */
poor: 10,
} as const;
/**
* 1回ごとの完了率の式の部品(`lib/judge/kpi.ts` の `runCompletionRate()` が唯一の消費者)。
*
* ★出所★: TeamWorks 第10回 N-3 の回答(`docs/handoff/TeamWorks回答_第10回_20261002.md:73`)—
* 「清掃1回毎にclean_area / task_area * 100 (100より大きい値は100, 小数点第2位以下切捨て)で計算」。
* ★サマリ(`*_summaries.clean_ratio`)と同じ式に当期を揃えるため★(D-155 項1)。
*
* ★★確定(第11回 P-2=A「今の式のまま」。D-158)★★ — 旧: 暫定(D-155。ユーザー判断「一旦B」)。
*/
export const RUN_COMPLETION_RATE = {
/** 1回ごとの完了率の上限(%)。「100より大きい値は100」 */
cap: 100,
/** 残す小数の桁数。「小数点第2位以下切捨て」= 小数第1位まで残して切り捨て */
decimals: 1,
} as const;
export const ESCALATION_THRESHOLDS = {
/** 2日連続で L2 */
level2Days: 2,
/** 3日以上連続で L3 */
level3Days: 3,
} as const;
/**
* 完了率の「急落」判定(ユーザー指示 2026-09-15 / 指示書AI)
*
* ★比(%)で見る。差(ポイント)ではない★ — ユーザー確定(2026-09-15「比で」)。
* (前回の完了率 − 当日の完了率) ÷ 前回の完了率 ≧ dropRatio
* = 完了率が前回の (1 - dropRatio) 倍を下回ったら急落。
*
* ★差で見ると、元から完了率が低い施設を拾えない★ —
* 前日30% → 当日18% は「12ポイント低下」だが「40%低下」。
* ユーザーは後者を拾う側を選んだ。
*
* ★「前回」はカレンダー前日ではなく、前回のスケジュール存在日★
* (D-026 / `CLAUDE.md` §4.5 の「連続」の数え方に揃えた。指示書AI 決定A)。
*/
export const COMPLETION_DROP = {
/** 前回比でこの割合以上下がったら「急落」(0.2 = 20%) */
dropRatio: 0.2,
} as const;
/**
* 要レスキュー店舗の判定(ユーザー指示 2026-09-15 / 指示書AI 段階3)
*
* ★ユーザー原文★: 「定義は出発失敗3日異常、連続問題レベル3、完了率急落前日比OO%、未実施3日連続」
* → ★「出発失敗」と「未実施」は現在のコードでは同じ判定★(`notExecutedFromReport()`。
* 旧 `notExecutedFromHistory()`。★`clean_task_reports` の行が無い=未実施★。指示書AQ §1-1)。
* 実開始時刻がDBに無いため分けられない(指示書AI §1-1。TeamWorks 第9回 11章で照会中)。
* ★回答が来たら4つ目の条件を足す★。
*
* ★★`ESCALATION_THRESHOLDS.level3Days` を流用しないこと★★ —
* たまたま両方 3 だが、意味が違う。あちらは「何日連続の**問題**で L3 にするか」、
* こちらは「何日連続の**未実施**で要レスキューにするか」。
* 片方を変えたときにもう片方まで動くと、★変えた人が意図しない条件まで変わる★。
*
* ★単位は「スケジュール存在日」★(D-026)。★カレンダー日ではない★ —
* 清掃予定のない日は連続を途切れさせない。
*/
export const RESCUE_THRESHOLDS = {
/** 未実施がこの日数以上連続したら要レスキュー(R-1) */
notExecutedDays: 3,
} as const;
/**
* 要レスキュー店舗を★画面で最初に何件見せるか★(指示書AJ 段階1。旧 `RESCUE_LIST_LIMIT`)
*
* ★★これは API が返す件数の上限ではない★★ — ★上限は設けない★
* (ユーザー指示 2026-09-15:「上限は設けずに、表示は5件、もっと表示のようなのもので全件」)。
* ★これは画面で最初に見せる件数であり、「もっと表示」で全件が開く★。
*
* ★★API 側で切らない理由★★: 切ると「切られたこと」に運用担当が気づけないため
* (指示書AJ §1-2。★実データで7件中2件が黙って消えていた★ — 消えた4件は
* すべて急落単独で、急落は「前日まで正常」なので連続日数が浅く、
* 連続日数の降順+上限5件の組み合わせで構造的に一覧から落ちていた)。
* ★件数の表示(`items.length`)も切ったあとの数になっていた★ので、
* 7件該当していても「5件」と出ていた(例外もテスト失敗も出ない)。
*
* ★★件数が青天井にならない理由★★: ★要レスキューは施設ごとに最大1行★なので、
* 施設数(P1 の想定で200件程度)が自然な上限になる。★ページ送りは要らない★。
*
* ★★`PAGINATION.defaultPerPage` を流用しないこと★★ — あちらは一覧APIの1ページの件数で、
* こちらは ★ダッシュボードの1ブロックに最初に並べる件数★。意味が違う。
*
* ★5 にした理由★: 同じ画面の「清掃詳細ログ(コンパクト版)」が5件
* (`/api/dashboard` の `problem_logs` の先頭5件を画面が出す。D-159)なので揃えた。
*
* ★消費者★: `lib/dashboard/rescue-visibility.ts` の `rescueVisibility()` のみ
* (画面はその純粋関数を通す。★`RescueRequiredCard.tsx` が直接読まない★ —
* 「何件見せるか」の判定をテストできる場所に置くため。`vitest.config.ts` の
* `include` は `lib/**` と `app/**` なので、画面コンポーネントのテストは走らない)。
*/
export const RESCUE_INITIAL_VISIBLE = 5 as const;
/**
* 連続日数の「表示上限」(ユーザー指示 2026-09-13 / 指示書AF)
*
* ★これは表示のための上限であって、判定の閾値ではない★
* L1〜L3(`lib/judge/escalation.ts`)と R1〜R4(`lib/judge/advice.ts`)は
* ★上限をかける前の生値★で判定する(`CLAUDE.md` §4.3 の「表示用に丸めた値で判定しない」と同じ思想)。
*
* ★`defaultMax` はシステム管理画面から変更できる★(保存先は
* `data/settings/display-limits.json`。`lib/settings/display-limits.ts`)。
* ★保存が無い・壊れている・範囲外のときはこの既定値に倒れる★。
*
* ★既定値を 30 にした理由★:
* R4(3日以上でオンサイト訪問。`CLAUDE.md` §4.5)の10倍で、エスカレーションの判断が
* 上限に触れて歪まない。「1か月ぶん」として運用担当が直感的に読める長さで、
* 月次レポートの粒度(§4.7)とも揃う。30日を超えた連続は、日数の正確さより
* 「訪問したか」が問題になる段階であり、31日と45日の区別が意思決定を変えない。
*
* ★`minMax` を `level3Days` から導く理由★:
* 上限が L3 を下回ると、「2日以上」と表示されているのにバッジが `Lv3` という
* 説明のつかない画面になる。★`3` と直書きしないこと★(§7 ハードコード禁止)。
* L3 の日数が将来変われば、下限も自動で追従する。
*
* ★`maxMax` を 365 にした理由★:
* 上限が無いと、画面から巨大な値を保存することで全期間走査に戻る抜け穴になる
* (指示書AC 段階6 がこの値をクエリの読み込み範囲に使う)。実データの保持は2年で、
* 365 はその半分・1年ぶん。1年を超える連続を1日単位で表示する運用上の意味が無い。
*
* ★単位は「スケジュール存在日」★(D-026)。★カレンダー日ではない★。
*/
export const CONSECUTIVE_DAYS_DISPLAY = {
/** 既定の表示上限。★これを超えたら「{n}日以上」と表示する★ */
defaultMax: 30,
/** 設定できる下限。★L3 の日数を下回らせない★ */
minMax: ESCALATION_THRESHOLDS.level3Days,
/** 設定できる上限 */
maxMax: 365,
} as const;
/**
* 連続日数を数えるために「表示期間より前」を何日ぶん余分に読むか(指示書AC 段階6)
*
* ★★連続日数の単位は「スケジュール存在日」であって「カレンダー日」ではない★★(D-026 /
* `CLAUDE.md` §4.5)。清掃予定のない日は連続を途切れさせないので、
* ★「30スケジュール存在日ぶん」を SQL で直接絞ることはできない★
* (それを厳密にやるには施設ごとのスケジュール本数を数えるクエリが要る)。
*
* ★★そこで安全側に倒し、カレンダー日の遡り幅を上限より**広く**取る★★ —
* 「★どんなに疎な施設でも7カレンダー日に1回は清掃予定がある★」という前提を置き、
* ★1スケジュール存在日 = 最大7カレンダー日★ として換算する。
* ★★この前提が崩れる(例: 月1回しか清掃しない施設が出る)と、遡りが足りず
* 連続日数が実際より小さく出ます★★(★例外もテスト失敗も出ない★)。
* ★そのときはここを増やすこと★。★★呼び出し側で係数を直書きしないこと★★(§7)。
*
* ★★`plusDays` を 0 にしないこと★★ — `capConsecutiveDays()` は
* ★`rawDays > maxDays` のときだけ★ `capped: true` にする
* (`lib/format/consecutive-days.ts`)。上限ちょうどまでしか読まないと
* `rawDays` が上限を超えられず、★「30日以上」が二度と画面に出なくなる★
* (指示書AF §C-3 申し送り2 / 指示書AC 段階6 期待値表 #7)。
*
* ★遡り幅の上限値そのもの(30日)はここに持たない★ —
* ★★システム管理画面で変更された保存値★★を `loadConsecutiveDaysMax()`
* (`lib/settings/display-limits.ts`)から読む(指示書AC 段階6)。
*/
export const CONSECUTIVE_LOOKBACK = {
/** 1スケジュール存在日を何カレンダー日として遡るか(★安全係数。1以上★) */
calendarDaysPerScheduleDay: 7,
/** 表示上限に足す日数。★`capped` を成立させるために必ず1以上★ */
plusDays: 1,
} as const;
/**
* 清掃失敗(未実施)と判定するまでの猶予(分)。D-025: 暫定値。
*
* ★★未使用(D-091 項1 / D-173)★★ — 出発失敗は「予定+猶予」では判定しない(本清掃のレポートの行の有無と、
* 結果待ちの `RESULT_WAIT_MINUTES`)。`CLAUDE.md` §7 の「猶予30分は設定値として外出し」に従って値だけ残している(指示書BB A14)
*/
export const LAUNCH_GRACE_MINUTES = 30;
/**
* ★結果待ち★の長さ(分)。★予定の終了(無ければ開始)からこの分数のあいだは、
* レポートが無くても「結果待ち」とし、出発失敗に数えない★(清掃中も含む)。
* ユーザー判断 2026-10-07(「レポートは清掃後30分より遅れて届くことがある」→ B。待つ時間は一旦2時間)。D-173。
* ★TeamWorks にレポートの最大の遅れを聞いて見直す★。
* ★`LAUNCH_GRACE_MINUTES` とは別物。流用しないこと★(D-091 項1 はこの件に限りユーザー判断で上書き)。
* ★消費者★: `isResultPending()`(`lib/clean-logs/result-pending.ts`)だけ
*/
export const RESULT_WAIT_MINUTES = 120;
/**
* ダッシュボード(F-01)の「直近の清掃」の選び方(D-159 / 指示書AV)
* ★`LAUNCH_GRACE_MINUTES` とは別物。流用しないこと★ — あちらは出発失敗の猶予(D-091 項1 で使わないと決めた)。
* こちらは「どの予定を見せるか」だけに効き、1行の判定(出発・◎○△×・L1〜L3)には効かない。
* ★消費者★: `app/api/dashboard/route.ts` と `components/features/dashboard/DashboardScreen.tsx`(見出しの日数)だけ
*/
export const DASHBOARD_LATEST_RUN = {
/** 何日前の予定まで遡るか(予定の開始で数える。★ちょうどは含む★) */
lookbackDays: 7,
// ★旧 `settleMinutes`(終了+30分たつまで候補にしない)は D-173 で削除した★(ユーザー判断 2026-10-07。表2 C6)—
// ★候補は「予定の終了 ≤ 現在」★。終わってからレポートが届くまでは、行の旗 `result_pending`
// (`RESULT_WAIT_MINUTES`・`isResultPending()`)で「結果待ち」と出す。
} as const;
/**
* DB の DATETIME 列に入っている「壁時計値」のタイムゾーン(分)
*
* ★TeamWorks 第8回 A2(2026-09-11 受領)★: 「JSTです」
* `@@time_zone = SYSTEM` / `@@system_time_zone = JST`。
* システムが生成する日付時刻は JST で生成し、PUDU から返る UNIX 時間も JST に補正して格納する。
*
* ★なぜ設定値にするのか★: `CLAUDE.md` §7(閾値のハードコード禁止)。
* DB のタイムゾーンが変われば、直す場所はここ1箇所になる。
*
* ★消費者★: `lib/format/date.ts` の `fromDbJst()` / `toDbJst()` のみ。
* ★表示整形(`toJstIso()`)は「JST の ISO 文字列を作る」別の責務なので、この値を使わない★
*/
export const DB_DATETIME_OFFSET_MINUTES = 9 * 60;
/**
* 事前チェックのタイミング(分前)
*
* ★これは **DFA 側の設定値**(何分前にロボット状態をチェックしたいか)であって、
* `clean_timetables.precheck` を読み替えるための対応表ではない★(指示書X 段階4。
* D-091 項4 / D-093 項2 の予定が TeamWorks 第7回回答 A5-2 で実現し、
* `precheck` は「本清掃開始の何分前か」を表す分数そのものになった。D-112)。
* 実際に何分前のチェックだったかは **DB の `precheck` が正**(D-105 項13-2 の解決)。
*
* ★消費者★: システム管理画面の表示(`components/features/system/SystemScreen.tsx`)/
* シードの `start_at` と `precheck` の生成(`scripts/seed-local-db.ts`)/
* fixtures(`lib/fixtures/prechecks.ts` / `lib/fixtures/error-reports.ts` / `lib/fixtures/clean-logs.ts` の
* `status_1h_minutes` / `status_30m_minutes` の既定値)。
*
* ★★詳細ログの `status_1h` / `status_30m` の列の割り当てには**もう使わない**★★(D-178。ユーザー判断 2026-10-16)—
* 列は DB の `precheck` を分数の大きい順に並べた**順番**で決める(`lib/db/precheck-mapping.ts` の
* `statusColumnsFromPrecheckRows()`)。★旧: この値と一致する分数だけを列に入れていたため、
* DB が 60/10 になると10分前の結果が黙って落ちた★。
*
* ★事前チェックは1清掃につき2回で固定と確定している★(ユーザー回答 2026-09-11
* 「これは2回だけです、3回以上にはしません」)。この配列を3要素にしても詳細ログの列は増えない
* (列は2つ。DB に3つ以上あれば開始に近い2つを残し、捨てた分数は `data.anomaly` に出す。D-178)。
*/
export const PRECHECK_MINUTES_BEFORE = [60, 30] as const;
/**
* `clean_timetables.precheck` の値の種類(TeamWorks第7回回答 A5-2 / A5-5。2026-09-11受領。D-112)
*
* ★`0` = 本清掃だけが特別な値★。それ以外は「本清掃開始の何分前か」を表す数値そのもの
* (分数そのもの。`1` が入っていたら1分前、`60` なら60分前。A5-3)。
* コード表(旧 `rounds`。0/1/2 の回次コード値)は TeamWorks 第7回回答により不要になり、
* 指示書X で削除した — 新旧の値域は混在しない(A5-4)ので、対応表を引く処理も持たない。
* 変換は `lib/db/precheck-mapping.ts` の `precheckMinutesFromCode()` 1箇所だけ。
*/
export const CLEAN_TIMETABLE_PRECHECK = {
/** 本清掃の行 */
main: 0,
} as const;
/**
* `clean_task_reports.clean_timetable_id` / `robot_error_reports.clean_timetable_id` の「予定外のタスク」を表す値
* (TeamWorks 回答 2026-10-27「clean_timetable_id が0以外は予定/実行済タスクになりますし, 0は予定外タスクになります」)。
*
* ★NULL も予定外として扱う★(ステージングには NULL の行が残っている。2026-10-27 実測・先方投入)。
* 判定は `lib/db/timetable-link.ts` の `scheduledTimetableId()` 1本だけ。★`=== null` / `{ not: null }` を直書きしない★
*/
export const CLEAN_TIMETABLE_ID_UNSCHEDULED = 0;
/**
* `clean_status_histories.clean_error` / `clean_statuses.clean_error` のコード値
*
* ★★2026-09-30 以降、出発失敗(未実施)の判定にも「清掃結果」列にも使っていない★★
* (指示書AQ 決定2 / 決定3 / §1-1 / §1-2)— 清掃結果の取得元が `clean_task_reports` に
* 変わり、★そちらには `clean_error` 列が無い★。未実施は ★「本清掃に対応する
* `clean_task_reports` の行が無い」★ で判定し(`notExecutedFromReport()`)、
* 「清掃結果」列は ★`clean_task_reports.status` の表示名★ を出す(`formatCleanTaskStatus()`)。
* ★`status` には別の表(`CLEAN_TASK_STATUS_LABELS`)がある★(第10回 N-2。D-155 項2)。
* ★このコード表で `status` を読み替えないこと★(別のテーブルの別のコード表)。
* ★それでも削除しない★ — `clean_statuses` 側で今後使う可能性があるため(指示書AQ §1-2)。
* ★以下の段落は、この切替より前の経緯として残してある★。
*
* ★出所(原文)★:
* `docs/handoff/analysis/table-definitions-02-clean-error.md:213`(`clean_status_histories`
* のカラム6「結果」)/ 同 `:181`(`clean_statuses` のカラム7「エラー」)—
* ★両者は同じコード表で、論理カラム名だけが違う(履歴側は「結果」、`clean_statuses`
* 側は「エラー」。原文ママ)★。Prisma 上の型は
* `clean_error Int @default(0) @db.UnsignedTinyInt`(`prisma/schema.prisma:495` が履歴側 /
* `:454` が `clean_statuses` 側)。
*
* ★★`1`(異常(清掃未実施))だけが出発失敗(未実施)★★(ユーザー回答 2026-09-10。
* 「出発失敗(未実施)は `clean_error = 1` のみ」)。★旧: 判定は
* `notExecutedFromHistory()`(`lib/db/clean-log-mapping.ts`)1箇所に閉じ込めてあった★。
* ★いまの判定は `notExecutedFromReport()` で、`clean_error` を読まない★(上の段落)。
*
* ★`2`(セキュリティ有効)/ `3`(D-S制御盤異常)/ `4`(清掃ロボット異常)は
* 「未実施とは違う扱い」★(同回答)。★これらを出発失敗やエラー種別7分類に
* 読み替えないこと★(D-077 / `CLAUDE.md` §4.8)。
* ★旧: 詳細ログの「清掃結果」列とCSVに `formatCleanResult()`(`lib/format/clean-result.ts`)で
* 出していた★(★D-146 項2★ / 指示書AO 段階2・段階3)。★いまは `formatCleanTaskStatus()` が
* `clean_task_reports.status` の表示名(`CLEAN_TASK_STATUS_LABELS`)を出し、このコード表は通さない★
* (指示書AQ §1-2 / 追補 5-2・D-155 項2)。
* ★ラベルは原文ママのまま出し、`t()` に通さない★(このコード表を再び使うときの約束)。
*
* ★`unsigned tinyint` なので 0〜255 が入りうる★ — ★コード表に無い値(5以上)は
* 数字のまま出す約束だった★(表示名の関数 `cleanErrorLabel()` と `CLEAN_ERROR_LABELS` は 2026-10-19 に削除。
* 本番で未使用だったため。指示書BB A14)。★★「その他」「不明」という語を
* 使わないこと★★(D-089 項3 / D-104 項2。破棄済み D-067 の「未知の値をその他に
* 寄せる」方針と紛らわしい)。前例は `MAKER_NAMES[maker_id] ?? String(maker_id)`。
*
* ★1箇所に閉じ込める理由★: `lib/db/`(DB→API)と `scripts/seed-local-db.ts`
* (fixtures→DB)と `lib/fixtures/`(`departed` からの導出)の3者が同じ値を必要とする
* (`ROBOT_STATUS_STANDBY` と同じ形)。★TeamWorks の回答で分類が変わっても
* ここ1箇所の変更で済むようにするため、`0/1/2/3/4` をコードに直書きしないこと★
* (`CLAUDE.md` §7)。
*/
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;
/**
* `clean_task_reports.status`(清掃結果)のコード値(★第10回 N-2 で受領。D-155 項2★)。
*
* ★出所(原文)★: `docs/handoff/TeamWorks回答_第10回_20261002.md:63` —
* 「0 清掃開始前 / 1 清掃中 2 清掃中断 3 清掃キャンセル 4 清掃正常終了 5 清掃異常終了」。
*
* ★◎○△× の判定・未実施の判定には使わない。稼働率の分子(正常終了の回数。`isCompletedTask()`)にだけ使う(第11回 P-6=A)★
* (D-155 項2 / 指示書AS D12)— 中断・異常終了でも完了率で ◎○△× を出す。
* ★未実施(出発失敗)の判定も `status` ではなく「レポートの行が無い」で行う★(`notExecutedFromReport()`)。
* ★★`CLEAN_ERROR`(と旧 `CLEAN_ERROR_LABELS`。2026-10-19 削除)とは別のテーブルの別のコード表★★
* (あちらは `clean_status_histories.clean_error`)。★互いに読み替えないこと★。
*/
export const CLEAN_TASK_STATUS = {
/** 0: 清掃開始前 */
notStarted: 0,
/** 1: 清掃中 */
cleaning: 1,
/** 2: 清掃中断 */
interrupted: 2,
/** 3: 清掃キャンセル */
cancelled: 3,
/** 4: 清掃正常終了 */
completed: 4,
/** 5: 清掃異常終了 */
endedAbnormally: 5,
/**
* 6: 清掃キャンセル(★ユーザー指示 2026-10-30「6はキャンセルだそうです」「6の名称も3と同じ清掃キャンセルにしてください」★)。
* ★第10回 N-2 の表には無い値★(Pudu の値。第11回 P-4 の時点では先方も意味が分からなかった。D-158)。
* ★表示名は 3 と同じだが、コード値は別のまま★(DB の値は読み替えない)
*/
cancelledByRobot: 6,
} as const;
/**
* `clean_task_reports.status` → 表示名(★第10回 N-2 の原文ママ★)。
*
* ★★原文から1文字も変えないこと★★(言い換え・要約の禁止。旧 `CLEAN_ERROR_LABELS`(2026-10-19 削除)と同じ約束)。
* ★英語は `lib/i18n.ts` で訳す(仮。ユーザーが後でまとめて見直す)★。
* ★6 だけは N-2 の原文に無く、ユーザー指示(2026-10-30)で 3 と同じ「清掃キャンセル」★。
* ★表に無い値(7 以上など)は `cleanTaskStatusLabel()` が「不明(7)」のように返す★(ユーザー指示 2026-10-30
* 「7以上は不明(7)となるようにしてください」)。★数字は必ず残す★ — 「不明」だけに潰すと値が分からなくなる
* (D-089 項3 / D-104 項2 の趣旨。旧: 数字だけ `String(status)`)。
*/
export const CLEAN_TASK_STATUS_LABELS: Readonly<Record<number, string>> = {
[CLEAN_TASK_STATUS.notStarted]: "清掃開始前",
[CLEAN_TASK_STATUS.cleaning]: "清掃中",
[CLEAN_TASK_STATUS.interrupted]: "清掃中断",
[CLEAN_TASK_STATUS.cancelled]: "清掃キャンセル",
[CLEAN_TASK_STATUS.completed]: "清掃正常終了",
[CLEAN_TASK_STATUS.endedAbnormally]: "清掃異常終了",
[CLEAN_TASK_STATUS.cancelledByRobot]: "清掃キャンセル",
};
/**
* 清掃詳細ログの「清掃結果」列の並べ替え順位(指示書BH §2-2)。
* ★表示の並びだけ★ — ◎○△× の判定には使わない。
*
* | 順位 | 対象 |
* |---|---|
* | 1 | 出発失敗(`task_status` null・結果待ちでない)→ `CLEAN_TASK_STATUS_SORT_RANK_NOT_DEPARTED` |
* | 2 | 5 清掃異常終了 |
* | 3 | 2 清掃中断 |
* | 4 | 3・6 清掃キャンセル(同じ順位) |
* | 5 | 表に無い値(不明(n))→ `CLEAN_TASK_STATUS_SORT_RANK_UNKNOWN` |
* | 6 | 0 清掃開始前 |
* | 7 | 1 清掃中 |
* | 8 | 4 清掃正常終了 |
*
* 結果待ちは順位の外で一番下に固定(`sortCleanLogs()`)。
*/
export const CLEAN_TASK_STATUS_SORT_RANK_NOT_DEPARTED = 1;
export const CLEAN_TASK_STATUS_SORT_RANK_UNKNOWN = 5;
export const CLEAN_TASK_STATUS_SORT_RANK: Readonly<Record<number, number>> = {
[CLEAN_TASK_STATUS.endedAbnormally]: 2,
[CLEAN_TASK_STATUS.interrupted]: 3,
[CLEAN_TASK_STATUS.cancelled]: 4,
[CLEAN_TASK_STATUS.cancelledByRobot]: 4,
[CLEAN_TASK_STATUS.notStarted]: 6,
[CLEAN_TASK_STATUS.cleaning]: 7,
[CLEAN_TASK_STATUS.completed]: 8,
};
/**
* 表に無い `clean_task_reports.status` の表示名の型(★`{id}` に生値が入る★)。
* ★英語の訳はエラー一覧の「不明({id})」と共有する★(`lib/i18n.ts`。画面は `t()` してから `{id}` を置き換える)。
*/
export const CLEAN_TASK_STATUS_UNKNOWN_TEMPLATE = "不明({id})";
/** `status` が表(`CLEAN_TASK_STATUS_LABELS`)に載っているか */
export function isKnownCleanTaskStatus(status: number): boolean {
return Object.prototype.hasOwnProperty.call(CLEAN_TASK_STATUS_LABELS, status);
}
/**
* `clean_task_reports.status` の表示名を引く。★表に無い値は「不明(7)」★(上の docstring)。
* ★判定には使わない★(表示だけ。D-155 項2)。
*/
export function cleanTaskStatusLabel(status: number): string {
if (isKnownCleanTaskStatus(status)) return CLEAN_TASK_STATUS_LABELS[status];
return CLEAN_TASK_STATUS_UNKNOWN_TEMPLATE.replace("{id}", String(status));
}
/**
* 事前チェック時点でエラーが記録されていなかった場合に `status_1h` / `status_30m` へ入れる値(D-098)
*
* ★TeamWorks から受領した `robots.status` コード表の `'0' => 'Standby.'` の**原文**★
* (`docs/handoff/TeamWorks回答_第2回_20260903.md:56`。★末尾のピリオド込みが原文★)。
* ★当方が作った文言ではない★ので、D-077(生値をそのまま出す/推測で変換しない)に反しない。
*
* ★注意★: これは「その行の `robots.status` を実際に読んで得た値」ではない。
* `robot_error_reports` に該当行が無い(=エラーが記録されていない)ことを
* 「待機(standby)」と読み替えている(D-065 Q2)。実データで `robots.status` を
* 直接引けるようになったら、そちらの生値に差し替えること。
*
* ★1箇所に閉じ込める理由★: `lib/db/index.ts`(DB→API)と
* `scripts/seed-local-db.ts`(fixtures→DB の逆判定)の両方が同じ値を必要とするため
* (`lib/format/raw-label.ts` の `EMPTY_VALUE_LABEL` と同じ形)。
*/
export const ROBOT_STATUS_STANDBY = "Standby.";
/** 一覧APIの共通デフォルト */
export const PAGINATION = {
defaultPerPage: 50,
maxPerPage: 200,
/**
* ★ページ番号ボタンを省略せずに全部並べる上限★
*
* これを超えたら先頭・末尾・現在の前後だけを出し、間を「…」で省略する。
* ★★奇数かつ 7 以上であること★★(`buildPageItems()` が左右対称に切り出すため。
* テストで固定している)。
*
* ★なぜ設定値にするのか★: `CLAUDE.md` §7(閾値のハードコード禁止)。
* 画面幅の都合で並べる数を変えたくなったとき、直す場所をここ1箇所にする。
*
* ★消費者★: `lib/pagination/page-items.ts` の `buildPageItems()` のみ。
*/
maxVisiblePages: 7,
} as const;
/**
* 清掃詳細ログの日付範囲の上限(日)
*
* ★2年(ユーザー指示 2026-09-15 / 指示書AI 段階7)★ —
* 「詳細ログは2年分見れるように」。★730 ではなく 731★ は、2年のあいだに
* うるう日が入りうるため(両端を含む日数で数えている。`inclusiveDayCount()`)。
*
* ★★これは「上限」であって「既定」ではない★★ — 画面を開いたときの初期表示は
* 今日1日のまま(`DetailLogScreen.tsx` の `useState(todayJst())`)。★変えないこと★。
*
* ★上限がかかるのは `date_from` と `date_to` の**両方**が指定されたときだけ★
* (`lib/clean-logs/query.ts`)。片方だけ・両方無しは検証しない=上限なし
* (ユーザー指示: 「未指定で開いた場合や、店舗指定のみの場合は上限無し」)。
*
* ★★申し送り★★: 索引が入るまで、2年を選ぶと重い(本番想定で約25万行)。
* CSVは別途 `MAX_CSV_ROWS` で歯止めをかけている。
*/
export const MAX_DATE_RANGE_DAYS = 731;
/**
* CSVエクスポートの行数上限(ユーザー指示 2026-09-15 / 指示書AI 段階8)
*
* ★ユーザー原文★:
* 「あとCSVで出力できる期間の限界が来たらこの期間だとCSV出力できませんとわかるようにしてください」
*
* ★★日数ではなく行数で決めている★★ — 同じ2年でも、1店舗なら約1,300行、
* 全店舗(施設200件)なら約25万行で、★200倍違う★(指示書AI 10-1)。
* ★日数で切ると、1店舗の2年ぶん(軽い)まで巻き添えで禁止することになる★。
* ★ユーザーは「店舗指定のみなら上限なし」と明示している★ので、行数で決めれば自然に両立する。
*
* ★★この値は暫定です。実データと索引が入ったら測り直すこと★★ —
* 現在の根拠は「31日・502行で0.73秒」という★実測1点からの外挿にすぎない★。
* ★DB側は塊ごとの全表走査になるので、行数に対して二乗で効く★
* (`clean_task_reports.clean_timetable_id` に索引が無いため — `prisma/schema.prisma` の
* `CleanTaskReport` の `@@index` に無い(構造)。★旧取得元 `clean_status_histories` も同じで、
* そちらは TeamWorks 第9回 1-2 で依頼中★。★上の 0.73秒は旧取得元で測った値★)。
* 20,000行なら 20塊 × 20,000行 = 40万行の走査で収まる見込み。
*
* ★CSVの大きさの目安★: 1行あたり 111 バイト(実測。BOM・CRLF・10列)。
* 20,000行 ≒ 2.2MB。
*
* ★★「上限」は含む★★ — ちょうど 20,000 行は出せて、20,001 行から 422
* (`app/api/clean-logs/export/route.ts`。境界の両側をテストで固定してある)。
*/
export const MAX_CSV_ROWS = 20_000;
/**
* レポートAPI(`/reports/*`)の `year` が取りうる範囲(`d-watch-api.yaml` の `Year`)
*
* ★なぜ範囲が要るのか★:
* `month`(1〜12)・`week`(1〜6)には上下限があるのに `year` には無く、
* 「整数でありさえすれば通る」状態だった。実測(2026-09-14。develop `3e48d30`):
* - `period=monthly&year=-1&month=9` → ★200★(空のKPIが返る。紀元前が有効な期間になる)
* - `period=weekly&year=-1` / `year=0` / `year=1000000` → ★500★
* (`lib/format/period.ts` の `addDays` が `00-1-09-05` / `0000-09-02` のような
* 4桁でない日付文字列で RangeError を投げる)
* - `year=99999999999999999999` → ★500★
* (Prisma が `Unable to fit value into a 64-bit signed integer for field 'year'`。
* SQLは発行されないが、★攻撃者が選んだ値がサーバログに載る★)
* いずれも「入力の誤り」であって内部エラーではないので、422で弾く。
*
* ★下限を 2000 にした理由★:
* 1. ★4桁の西暦でなければ期間の日付文字列(`YYYY-MM-DD`)が壊れる★
* (上の RangeError)。1000未満は構造的に扱えない。
* 2. D-Watch の実データ保持期間は2年(指示書AC)。サービス開始(2026)より前の
* 年に行が存在することはない。2000 は「桁が壊れない安全域」の中で、
* 過去データの取り込みがあっても困らない十分に手前の丸い値。
*
* ★上限を 2100 にした理由★:
* 1. サマリ4テーブルの `year` は ★`UnsignedSmallInt`(0〜65535)★
* (`prisma/schema.prisma` の `customer_summaries` 等)。65535 を超える値は
* DBに存在しえないので、受け取る意味がない。
* 2. 4桁の制約から実際の天井は 9999 だが、★通す値は狭いほどよい★。
* 保持2年の運用に対して 2100 は約75年ぶんの余裕があり、
* システムの寿命を超える。カレンダーの限界に合わせる必要はない。
*
* ★消費者★: `lib/reports/query.ts` の `parseReportQuery()` のみ。
* (`month` / `week` の下限・上限は同ファイルの `MONTH_MIN` 等に置かれている。
* ここに出したのは `CLAUDE.md` §7「閾値のハードコード禁止」に寄せたため)
*/
export const REPORT_YEAR = {
/** 受け付ける最小の西暦 */
min: 2000,
/** 受け付ける最大の西暦 */
max: 2100,
} as const;
/**
* `where: { ... : { in: [...] } }` に一度に渡す要素数の上限(指示書AC ★§E-5★)
*
* ★なぜ要るのか★:
* `cleanLogs()` は本清掃の id 配列で `clean_task_reports`(旧: `clean_status_histories`。
* 指示書AQ 決定2 / 決定3) / `robot_error_reports` / 事前チェック行を引き直します(段階2 D-4)。
* ★日付範囲の上限は2年(`MAX_DATE_RANGE_DAYS` = 731。指示書AI 段階7 で 31 から広げた)で、
* 本番想定は約360行/日(`docs/spec.md:45`)なので、`in` の要素数は
* ★約26万★ になりえます★(31日なら約11,000だった)。★だからこそ分割が要ります★。
*
* ★★超えたら分割する(`facility_id` + 日付で絞り直す案は採らない)★★ —
* `clean_task_reports` / `robot_error_reports` には `facility_id` の索引があるので
* 「施設+日付でも絞る」ほうが索引は効きます。★しかしその条件は清掃ログ本体の
* 絞り込みと**別経路で行が選ばれる**ことになり、両者がずれたときに
* ★行が静かに落ちます★(`clean_task_reports.facility_id` が
* 紐づく `clean_timetables.facility_id` と必ず一致する保証を DFA は持っていない。
* `CLAUDE.md` §7「仕様が不明な箇所を推測で実装しない」)。
* ★★分割なら結果は分割しない場合と必ず同じになる★★ので、こちらを採りました。
*
* ★値を 1,000 にした理由★:
* 1. ★Prisma(mysql)の `maxBindValues` は 65,535★ で、超えると Prisma が
* ★自分でクエリを分割します★(指示書AC 実測: 65,536要素 → 3クエリ)。
* ★その自動分割に乗ると分割の単位をこちらで制御できない★ので、
* ★十分下回る値★を自分で決める。
* 2. ★小さすぎるとクエリ回数(往復)が増える★ — 11,000要素なら 1,000 で ★11回★、
* 100 なら 110回。
* 3. ★大きすぎると1本の SQL 文が長くなる★(`max_allowed_packet`)。
* ★本番(MariaDB 10.11)で実測して調整してよい★。★その場合はここ1箇所を直す★
* (`CLAUDE.md` §7 ハードコード禁止)。
*
* ★★施設IDの `in`(施設名の解決結果)には**当てはめないこと**★★ —
* 施設マスタは実測12件で、部分一致で全施設が当たっても12要素。
* ★`clean_timetable_id` の `in` とは桁が違う★(指示書AC §E-5)。
*
* ★消費者★: `lib/db/chunked.ts` の `chunkForInClause()` のみ。
*/
export const DB_IN_CLAUSE_MAX_ITEMS = 1000 as const;
/** 施設別パフォーマンスの良好/注意/要改善の境界(暫定値) */
export const FACILITY_STATUS_THRESHOLDS = {
/** 稼働率がこの値以上なら良好 */
good: 90,
/** 稼働率がこの値以上なら注意、未満は要改善 */
warning: 75,
} as const;
/**
* 施設別パフォーマンス表(F-04 / 指示書F 5-7)のセル色分け用の閾値
*
* ★判定ロジック(◎○△×・L1〜L3)の閾値とは別物★
* `GRADE_THRESHOLDS` / `ESCALATION_THRESHOLDS` は「その清掃をどう評価するか」の
* 判定基準だが、ここにあるのは**表のセルを赤くするかどうか**という表示上の目安。
* 混ぜると判定基準を変えたつもりが表の色だけ変わる(逆も起きる)ので分けている。
*
* `FACILITY_STATUS_THRESHOLDS` はエラー率用ではない(あちらは稼働率の良好/注意/要改善)。
* モック(d-watch-mock.jsx:807)の `> 8 → ng` に対応する設定値が既存に無かったため新設した。
*/
export const FACILITY_TABLE_THRESHOLDS = {
/** エラー率がこの値を「超える」と要注意色(ng)にする。8.0 は薄字のまま */
errorRateWarning: 8,
} as const;
/** ダッシュボード(F-01)・レポート(F-04)の成功率カードに表示する目標値(%) */
export const SUCCESS_RATE_TARGET = 95 as const;
/**
* D-S制御盤の備考(F-06 の「備考」列)に入力できる最大文字数
*
* 運用担当の手入力メモなので、長文の申し送りではなく一行メモを想定した暫定値。
* 表示側は1行に収める前提で、これを超える内容は別の記録(点検記録)に書く運用にする。
*
* ★保存先がメインDBではなく DFA 独自の JSON ストア(`lib/settings/ds-device-notes.ts`)★
* なのでカラム長という物理的な制約は無い。それでも上限を設けるのは、
* 上限が無いと1件の備考でストアのファイル全体が肥大し、
* 全件読み込み→書き戻しという保存方式ごと遅くなるため。
*/
export const DEVICE_NOTE_MAX_LENGTH = 200 as const;
/**
* アラート(事前チェック)の備考(`PATCH /prechecks/{id}/status` の `note`)に入力できる最大文字数
* (指示書AT §3-6)。
*
* ★`d-watch-api.yaml` の `maxLength: 400` と一致させること★(片方だけ変えない)。
* 保存先はメインDBの `robot_error_reports.comment`(`text`)なので列の長さの制約は無いが、
* 画面は1行メモとして表示するため上限を設ける。文字数は ★トリム前★ で数える
* (`DEVICE_NOTE_MAX_LENGTH` と同じ理由。画面の入力欄が数えるのもトリム前)。
*/
export const PRECHECK_NOTE_MAX_LENGTH = 400 as const;
/**
* D-Sec アラートとして出す `robot_error_reports.error_type` の値(指示書AX D1。D-180)。
* ★完全一致(JS の `===`)で比べる★ — SQL の `IN` は写し(DB の照合順序は大小文字・末尾の空白を区別しないため)。
* ★`SECURITY_ON` は入れない★(ユーザー判断 2026-10-19。D-091 項3「セキュリティが無効であっても異常ではない」)。
* ★`NOT_STARTED`(清掃開始失敗)も入れない★(ロボット側の出発失敗。同日のユーザー判断)。
* 値の出所: TeamWorks 第10回 N-5(`D-S_CONTROL_FAILURE` = "no regular report from the D-S control.")。
*/
export const D_SEC_ALERT_ERROR_TYPES: readonly string[] = ["D-S_CONTROL_FAILURE"];
/**
* エラー一覧の「エラー内容」(`error_lists` の id)を選ぶと★備考(`note`)が必須になる★選択肢の id(指示書BD §4。D-184)。
* ★131 = 決定稿の「OTHER ERROR / その他(該当なし)」★(スプレッドシート「PUDUエラー一覧_20261016」の「PUDUエラー 決定稿」シート)。
* ★ID は基本固定だが、追加・変更はありうる★(ユーザー回答④ 2026-10-27)— 変わったらここだけを直す。
* 消費者: `ErrorListItem.note_required` の組み立てと、`PATCH /errors/{error_id}/status` の検証(指示書BD §3-3 手順4)。
*/
export const ERROR_LIST_NOTE_REQUIRED_IDS: readonly number[] = [131];
/**
* エラー内容(`error_list_id`)を★選べない★行の `robot_error_reports.error_type`(ユーザー判断 2026-10-27。指示書BD §4。D-184)。
* ★完全一致(JS の `===`)で比べる★(`D_SEC_ALERT_ERROR_TYPES` と同じ)。
* 制御盤(`D-S_CONTROL_FAILURE`)・セキュリティ(`SECURITY_ON`)の行は選べない。
* ★`NOT_STARTED`(清掃開始失敗)は入れない — 選べる★(同日のユーザー判断)。
* 消費者: `ErrorReportItem.error_list_selectable` を決める純粋関数1つ(画面は `error_type` を見ない。`CLAUDE.md` §9.6)。
*/
export const ERROR_LIST_UNSELECTABLE_ERROR_TYPES: readonly string[] = ["D-S_CONTROL_FAILURE", "SECURITY_ON"];
/**
* 対応アドバイスの文言(`PATCH /settings/advice`)の入力上限
*
* ★`DEVICE_NOTE_MAX_LENGTH` と**まったく同じ理由**で必要★
* 保存先は `lib/settings/advice.ts` の JSON ストアで、`DEVICE_NOTE_MAX_LENGTH` の
* 備考ストアと同じ「★全件読み込み→書き戻し★」方式(`lib/settings/advice.ts:126-139`)。
* 上限が無いと1件の編集でファイル全体が肥大し、★保存方式ごと遅くなる★。
* 加えてここに入れた値は `GET /settings/advice` と `/clean-logs/{id}/advice` で
* ★そのまま画面に返る★ので、表の行が壊れる形にもなる。
*
* ★値の根拠★: `DEFAULT_ADVICE_RULES`(`lib/config/advice-rules.ts`)の実物を数えた実測
* (2026-09-13)に余裕を掛けた暫定値。★実測を超えて足りなくなったら上げてよい★が、
* ★上げた理由をここに書き足すこと★。
*
* | 定数 | 上限 | 既定値の実測(最長) |
* |---|---|---|
* | `ADVICE_TITLE_MAX_LENGTH` | 100 | ★14文字★(R3「2日連続で完了率低下(R3)」) |
* | `ADVICE_TEXT_MAX_LENGTH` | 400 | ★62文字★(R4) |
* | `ADVICE_ACTION_MAX_LENGTH` | 100 | ★19文字★(B3「Wi-Fiルーターの再起動を現場に依頼」) |
* | `ADVICE_ACTIONS_MAX_ITEMS` | 10 | ★3件★ |
*
* ★`actions` の**中身**は許可リストにしない★ — 運用担当の自由入力のメモであり、
* 書ける内容を DFA 側で決められない(`CLAUDE.md` §7)。縛るのは★長さと件数だけ★。
*
* ★消費者★: `lib/settings/advice-validation.ts` の `validateAdviceFields()` のみ。
*/
export const ADVICE_TITLE_MAX_LENGTH = 100 as const;
/** 推定原因の本文。→ `ADVICE_TITLE_MAX_LENGTH` のコメントに理由と根拠をまとめてある */
export const ADVICE_TEXT_MAX_LENGTH = 400 as const;
/** 推奨アクション1件あたりの文字数。→ 同上 */
export const ADVICE_ACTION_MAX_LENGTH = 100 as const;
/** 推奨アクションの件数。→ 同上 */
export const ADVICE_ACTIONS_MAX_ITEMS = 10 as const;
/**
* パスパラメータ `{id}` が取りうる最大値(`/clean-logs/{id}/advice` 等)
*
* `Number.MAX_SAFE_INTEGER` を超える整数は、`Number.parseInt()` を通した時点で
* 元の桁が失われる(例: 末尾1桁が変わる)。その値で `find(l => l.id === logId)` を
* すると「送った値とは別のIDに一致する」事故が起きうるので、桁が落ちる入力は
* そもそも受け取らずに422で弾く。
*
* ★上限の根拠は「DBのカラム幅」ではなく「JavaScript 側の桁落ち」★
* 業務テーブルのIDは `prisma/schema.prisma` を見るとほぼ全てが
* ★`BigInt @id @default(autoincrement()) @db.UnsignedBigInt`★
* (例: `model Facility`(123行目) / `model Control`(201行目))で、
* ★`UnsignedBigInt` の上限 18446744073709551615 は `Number.MAX_SAFE_INTEGER`
* (9007199254740991)を大きく超える★。つまり「カラム幅の方が狭いから届かない」
* という話ではなく、★先に限界が来るのは JavaScript の `number` の方★。
* さらに `RobotErrorReport.id` は `String @db.VarChar(50)`(311行目)、
* `CleanTaskReport.report_id` は `String @db.VarChar(20)`(510行目)で
* ★整数ですらない★(`Int` を使っているのは Laravel の `migrations` テーブルだけ)。
* だからここは安全整数を上限にする。★カラム幅が変わってもここを直す必要は無い★
* (直す必要が出るとしたら、IDを `number` で扱うのをやめる時)。
*
* ★なぜ設定値にするのか★: `CLAUDE.md` §7(閾値のハードコード禁止)。
* ★消費者★: `lib/api/path-params.ts` の `parsePathId()` のみ。
*/
// 周囲の定数と違い `as const` を付けない(不揃いに見えるが意図的)。
// `Number.MAX_SAFE_INTEGER` はリテラルではなく組み込みプロパティの参照なので、
// ★`as const` を付けるとコンパイルエラー(TS1355)になる★。
// リテラルでない以上リテラル型にも狭まらないため、付けても得るものは無い。
export const PATH_ID_MAX = Number.MAX_SAFE_INTEGER;
/**
* パスパラメータ `{serial_number}` の最大文字数(`/ds-devices/{serial_number}/note`)
*
* 200 ではなく 100 なのは、D-S制御盤のシリアルが入る `controls.serial_number` が
* ★`varchar(100)`★(`prisma/schema.prisma` の `model Control`)だから。
* DBに入りえない長さの入力を、実在チェック(`devices.some(...)`)まで運ぶ意味が無い。
*
* ★**文字種は絞らない**★(`CLAUDE.md` §7「仕様が不明な箇所を推測で実装しない」)。
* 実データのシリアルの形式は TeamWorks のデータ収集処理が決めるもので、
* DFA側で確定していない(fixtures は `8111R5628060011` 形式、
* `d-watch-api.yaml` の example は `DS-001` 形式で、既に2種類ある)。
* 長さ上限だけならどちらの形式でも弾かれない。
*
* ★消費者★: `lib/api/path-params.ts` の `parsePathSerialNumber()` のみ。
*/
export const PATH_SERIAL_NUMBER_MAX_LENGTH = 100 as const;
/**
* パスパラメータ `{id}` の最大文字数(`PATCH /prechecks/{id}/status`。指示書AT 段階1)
*
* ★`robot_error_reports.error_id` が `varchar(50)`★(`prisma/schema.prisma` の `model RobotErrorReport`)。
* DBに入りえない長さの入力を実在確認まで運ばない(`PATH_SERIAL_NUMBER_MAX_LENGTH` と同じ作法)。
* ★文字種は絞らない★ — `error_id` の形式は TeamWorks のデータ収集処理が決めるもので、
* DFA側で確定していない(`CLAUDE.md` §7)。
*
* ★消費者★: `lib/api/path-params.ts` の `parsePathErrorId()` のみ。
*/
export const PATH_ERROR_ID_MAX_LENGTH = 50 as const;
/**
* パスパラメータ `{report_id}` の最大文字数(`GET /clean-logs/unscheduled/{report_id}/advice`。指示書BI 段階1)
*
* ★`clean_task_reports.report_id` が `varchar(20)`★(`prisma/schema.prisma` の `CleanTaskReport`)。
* DBに入りえない長さの入力を実在確認まで運ばない(`PATH_ERROR_ID_MAX_LENGTH` と同じ作法)。
* ★文字種は絞らない★ — 形式は TeamWorks のデータ収集処理が決めるもので、
* DFA側で確定していない(`CLAUDE.md` §7)。
*
* ★消費者★: `lib/api/path-params.ts` の `parsePathReportId()` のみ。
*/
export const PATH_REPORT_ID_MAX_LENGTH = 20 as const;
/**
* devices.maker_id(DB。tinyint 0〜255)→ RobotSummary.maker(API文字列)の対応表
*
* makers テーブルは提供されない(D-057 A-1: 「メーカー情報は設定ファイルで代替する想定」)ため、
* 番号の割り当ては DFA 側の暫定決定(指示書M 確認1 / D-085)。実データの maker_id を受領したら差し替える。
*
* ★未知の番号(ここに無いキー)は呼び出し側で `String(maker_id)` をそのまま返し、
* "不明" 等に潰さないこと★(D-077 と同じ考え方。気づかれない誤変換より、
* 見慣れない値が画面に出るほうが「対応表に足す必要がある」と気づける)
*/
export const MAKER_NAMES: Record<number, string> = {
1: "PUDU",
2: "Keenon",
};
/**
* 対応機種レジストリ(指示書N / D-087)
*
* P1 で画面に出る機種は PUDU CC1 のみ(CLAUDE.md §1「ロボットメーカーは PUDU のみ」)。
* 将来の候補として PUDU MT1 / Keenon C30 / Keenon C40 がある(2026-09-09 ユーザー情報:
* 「pudu製品は現状CC1のみ、将来的にMT1も表示するかもしれません。keenon製品はC30とC40を
* 将来的にやる可能性があります。なので現状はCC1のみ表示されるように、他の3機種は追加が
* 必要になったときに迅速に追加できるように土台だけ置いておいてください」)。
*
* ★対応を開始するときは supported を true にするだけ★
* ★ここに無い機種も画面からは消さず「未対応」バッジを付けて表示する★(D-087)。
* 隠す(フィルタする)と、実データに未登録機種が来たときロボットが画面から静かに
* 消え、例外もテスト失敗も出ないまま誰も気づけなくなる。D-077(エラー種別を変換しない)
* と `MAKER_NAMES`(未知の番号を "不明" 等に潰さない)と同じ思想。
* ★機種をコメントアウトで表すのではなくフラグにしている★ —
* コメントアウトは型チェックもテストも通らず、有効化して初めて綴りの誤りに気づく。
* フラグなら「既知だが未対応」と「完全に未知」を区別でき、有効化も1文字で済む。
*/
export const ROBOT_MODELS = [
{ maker: "PUDU", model: "CC1", supported: true },
{ maker: "PUDU", model: "MT1", supported: false },
{ maker: "Keenon", model: "C30", supported: false },
{ maker: "Keenon", model: "C40", supported: false },
] as const;
/**
* facilities.contract(10桁文字列)の桁位置定義(D-080)
*
* 1桁目(index 0)=D-Security、2桁目(index 1)=D-Watch。契約済み(有効)なら "1"。
* ★桁位置の数字をこのファイル以外に直書きしないこと★(`lib/format/contract.ts` が唯一の消費者)
*/
export const CONTRACT_DIGITS = {
dSecurity: 0,
dWatch: 1,
} as const;
/**
* `customers.group_id` の「グループなし」を表すセンチネル値(D-011)
*
* DB は `BigInt @default(0)` の **NOT NULL**(`prisma/schema.prisma` の `model Customer`)で、
* ★0 = ホールディングスに属さない★ を表す。TeamWorks がインデックス効率を理由に
* NULL ではなくセンチネル値方式を採ったため(D-011)。
*
* ★API には 0 を漏らさない★(D-011「レスポンスでは 0 → `null` に変換して返す」)。
* ★この数字を他所に直書きしないこと★ — 唯一の消費者は
* `lib/format/group-id.ts` の `groupIdFromDb()`。
* ルートや fixtures に `=== 0` を散らすと、1箇所直し忘れたときに
* ★「グループ0」という存在しないホールディングスが画面に出る★(指示書AK 落とし穴3)。
*/
export const GROUP_ID_NONE = 0 as const;
/**
* 事前チェックのロボット状態(`status_1h` / `status_30m` / `robot_status`)の色分け。
*
* ★実DB上の値の形式は未確定です★(D-077 項3)。
* `robots.status` の46値は動作フェーズのみで、ハンドルアップ・オフラインを表現していません。
* 下の日本語6値は ★fixtures 由来の暫定値★ で、実データからは来ません。
*
* ★ここに無い値はすべてグレー(`c.txD`)にフォールバックします★(`lib/theme-status.ts`)―
* 例外もテスト失敗も出ないので、実データ接続後は
* ★実際に出現する値を突き合わせて、この表に追記してください★。
*
* ★`ROBOT_STATUS_STANDBY`(`Standby.`)はあえてこの表に足していません(指示書R 段階2 の判断)★
* 1. `Standby.` は「`robots.status` を読んで得た値」ではなく、
* `robot_error_reports` に行が無いことの読み替えです(`ROBOT_STATUS_STANDBY` のコメント参照)。
* 実際には読んでいない状態に「正常(緑)」を主張するより、
* 未知=グレーのままにしておくほうが事実に忠実です。
* 2. この表の使い方は上記のとおり「★実データで実際に出現した値を追記する★」であり、
* 当方が当てている代替値を先回りで登録する場所ではありません。
* 3. 生値方針(D-077)の帰結として ★色分けがグレーになること自体が確認項目★です
* (指示書R 完了条件6 / 確認3 項3)。ここで色を足すと、その確認が成り立たなくなります。
* ★実データの `robots.status` に `Standby.` が出ることを確認できたら、1行足すだけで済みます★
* (色を足すのは「値の変換」ではなく「表示の装飾」なので D-077 には触れません)。
*/
export const ROBOT_STATUS_TONES: Readonly<Record<string, "ok" | "pr" | "ng" | "ac">> = {
待機中: "ok",
充電中: "pr",
ハンドルアップ: "ng",
緊急停止: "ng",
オフライン: "ng",
セキュリティ有効: "ac",
};
/**
* 面積の表示単位(ユーザー指示 2026-09-16「単位は㎡でよいです」/ 指示書AN)
*
* ★★これは DFA の判断であって、TeamWorks の確認ではない★★ —
* ★DFA側の資料に「㎡」とあるが、それはDFAが書いたもの★。
* ★TeamWorks からの回答は受け取っていない★(第9回 5章で照会中)。
* ★実データは task_area が全行 1000 ちょうどで、桁から推定することもできない★。
*
* ★★回答が違っていたら、全画面が間違った単位を主張することになる★★。
* ★だから1箇所に置いている。直すのはここ1行で済む★。
* ★★画面やAPIに "㎡" を直書きしないこと。★★
*/
export const AREA_UNIT = "㎡" as const;
/**
* 清掃面積の画面で、内訳を何件まで出すか(指示書AN)
*
* ★施設数が上限★(施設ごとに最大1行)。★「もっと表示」で全件開く★のは
* 要レスキュー(D-137)と同じ作法。
*/
export const CLEAN_AREA_INITIAL_VISIBLE = 10;