---
title: "サービスロボット導入シミュレーション Log#16 | 眠っている間に、配膳ロボットを比べる基準ができた"
slug: serving-robot-pass-by-standard
date: 2026-10-27
day: 384
meta_description: "Unity の3Dのファミレスを夜に4本同時で回し、3晩で200回。Mac が忙しいとロボットが詰まると分かり、本数を4に固定。すれ違いは止まらず通る形を標準にし、60回を比べる基準にした。"
tags: [サービスロボット導入シミュレーション, 配膳ロボット, Unity, シミュレーション, 夜の実行, 再現性, 台帳, Claude Code, codex]
series: "導入シミュレーション"
---

パソコンの中に作ったファミレスで、配膳ロボットを何台入れると店がどう変わるかを調べています。お客さんが来て、スタッフが案内し、ロボットが料理を運ぶ。Unity（ゲームを作る道具）で作った3Dの店を、混む2時間ぶん動かして、待ち時間などを数えます。前回までに、これを人が寝ている間に何十回も回す仕組みを作りました。今回は、その仕組みで実際に夜を3回回しました。

回し始める前に、思いがけないことが分かりました。**同じ店・同じ条件でも、Mac がほかの仕事で忙しいほど、ロボットが動けなくなる回数が増えます。** 1本だけで回すと中央値1回、4本を同時に回すと7回でした。お客さんの入店待ちには差が出ません。つまり、シミュレーションの数字の一部が、店の中のことではなく、パソコンの空き具合で変わっていました。そこで、夜は必ず同じ本数（4本）で回し、比べる相手も同じ回し方でそろえることにしました。

もう1つ決めたことがあります。実際の店の配膳ロボットは、通路でほかのロボットと会うと、速さを落として止まらずにすれ違います。シミュレーションでも同じ動きを入れるかどうかを、2晩分のデータで比べました。入れると、ロボットどうしが向かい合って動けなくなる時間の中央値が、422秒から64秒に縮みました。お客さんの待ちには、はっきりした差がありません。この動きを標準にして、3晩目に回した60回を、これから先の夜の結果を比べる基準（基準データ v4）にしました。

やったことの全体です。

| やったこと | なぜ | 結果 |
|---|---|---|
| 同時に回す本数で結果が変わるかを確かめた | 1本で回すと詰まりが少なく見える気配があったため | 動けなくなった回数がはっきり増える。夜は4本に固定した |
| 台帳に書く所を別の AI（codex）にレビューしてもらった | 台帳は壊すと戻せないため | 「見送り」と判定。今夜に関わる8つを直してから回した |
| 最初の夜（70回）を回した | 夜の仕組みを実物で通すため | 70回とも最後まで回った。1回だけ、店が空にならなかった |
| 店が空にならなかった回を調べた | 夜の数字を信じてよいか決めるため | 卓に着けないロボットが同じ皿を持ち続けていた。2回失敗したら人が運ぶ形にした |
| 2回目の夜（70回）を回した | 直したあとも結果が変わらないかを見るため | 60回とも前の夜の幅に入った |
| すれ違いの入と切を2晩分で比べた | 実物に合わせた動きを標準にしてよいか決めるため | 長く向かい合う止まりがはっきり減り、待ちは悪くならない。入を標準にした |
| 3回目の夜（60回）を回し、基準データ v4 にした | 標準を変えたので、比べる相手も作り直すため | 60回とも最後まで回った。次の夜からはこの60回と比べる |
| Claude Code への依頼を自動で渡すようにした | 人が依頼文を貼り付ける手間をなくすため | 3回目の夜は、人がコマンドを打たずに回った |

1つ、まだ答えの出ていない点があります。3回目の夜で、③の店ではロボットを1台から2台に増やしたとき、10種のうち6種で入店待ちが長くなりました。ロボットを増やせば待ちが短くなる、というサイトの試算とは逆の向きです。基準 v4 で初めて出た印なので、次の店に広げる中で追いかけます。

次は、同じ仕組みを別の店の形（④⑤⑥、そのあと②）に広げます。


## 作業ログ ― 同時に回す本数で、ロボットの詰まりが変わった

ここから下は作業ログです。効果のうち「見込める効果」と書いた所は、まだ測っていない見込みです。測った数字はそのまま載せています。

### busycheck：③1台・種1 を1本ずつ5回と、4本同時で8回

前回の終わりに、同じ回を1本だけで回すと「動けなくなった」が少なく見える気配がありました。夜に何本同時に回すかを決める前に、これを確かめました。条件は③の店・ロボット1台・乱数の種1で、すべて同じです。Unity のエディタは閉じて回しました。4本同時の回は、5回だと最後の1本が1本だけで回ってしまうので、2束の8回にしました（Claude Code の判断）。

| 回 | 1本ずつ：動けなくなった | 人のあきらめ | 入店待ち（分） | 4本同時：動けなくなった | 人のあきらめ | 入店待ち（分） |
|---|---|---|---|---|---|---|
| 1 | 5 | 38 | 2.330 | 3 | 36 | 1.653 |
| 2 | 3 | 31 | 2.293 | 6 | 40 | 2.003 |
| 3 | 1 | 23 | 1.584 | 8 | 51 | 1.642 |
| 4 | 1 | 35 | 2.311 | 5 | 33 | 1.929 |
| 5 | 1 | 34 | 2.193 | 9 | 35 | 2.824 |
| 6 | | | | 2 | 32 | 1.575 |
| 7 | | | | 11 | 51 | 3.046 |
| 8 | | | | 8 | 48 | 1.623 |
| 中央値 | 1 | 34 | 2.292 | 7 | 38 | 1.791 |

順位で比べると、動けなくなった回数ははっきり違いました（U=4／40。5回と8回なら6以下か34以上で、偶然では起きにくい差）。人のあきらめは増える向きでも言い切れず（U=8.5／40）、入店待ちは違いがありません（U=25／40。中央値は逆に1本ずつの方が長い）。台帳の全部の列を当たると、はっきり動いたのは、動けなくなった・助けに行った・ロボットが運んだ割合・店員が遅れて運んだ皿の4つだけでした。忙しい → ロボットが止まる → ロボットの分担が下がる → 店員が代わりに運ぶ、という流れで、客の待ちは店員が吸い取っています。

もう1つ分かったのは、基準にしていたエディタの回（cs:7）が、4本同時の幅の中に入っていたことです。エディタで回すと、それだけで Mac が4本同時くらい忙しくなる、ということになります。

| | エディタ（cs:7） | 1本ずつの幅 | 4本同時の幅 |
|---|---|---|---|
| 動けなくなった | 9 | 1〜5 | 2〜11 |
| ロボットが運んだ割合 | 0.6456 | 0.6748〜0.7136 | 0.6359〜0.6942 |
| 店員が遅れて運んだ皿 | 73 | 59〜67 | 63〜75 |
| 入店待ち（分） | 3.004 | 1.584〜2.330 | 1.575〜3.046 |

- ねらい：夜に何本同時に回すかを、速さだけでなく、数字がずれないかで決める
- 変わったこと：同時本数を4に決め、夜の途中で変えない。夜は Unity のエディタを閉じる
- 見込める効果：夜どうし・基準との比べで、Mac の忙しさの違いが混ざらない（4本同時なら70回が約1時間で終わる）

ロボットが動けなくなる回数がパソコンの忙しさで変わるということは、ほかの人の Mac で回すと数字が変わる、ということでもあります。誰が回しても同じ結果になる形にするには、いずれ直す必要があります。原因の候補は、ロボットの動きの計算が Mac の空き具合で割り振られることです。今は本数を固定して比べ、直すのは後に回しました。

### 夜の並びを種ごとにし、1回ごとの開始・終了の時刻を残した

夜の終わりは、残りの回が少なくなり、同時に走る本数が4本より減ります。その数回だけ Mac が空いた状態で回ります。最初の並びは、70回の最後の10回が全部すれ違い入の回でした。これでは、すれ違い入の回だけ詰まりが少なく出るおそれがあります。

そこで並びを「種ごと」（種0の全7条件 → 種1の全7条件 → …）に変えました（cs:18）。最後に薄くなる3回は、種9の別々の条件になります。あわせて、実行係が1回ごとの開始と終了の時刻を `night/{夜}/runs.csv` に書き、その夜に並べた回を `night/{夜}/queue.json` に写すようにしました（cs:19）。

`runs.csv` があると、その回が回っている間に何本走っていたかを、あとから計算できます。最初の夜では、最後の2回だけ平均の同時本数が3.4本と3.0本に下がり、それ以外はほぼ4本でした。


## 作業ログ ― codex のレビューは「見送り」、今夜に関わる8つを直した

### codex の判定：通常の書き方は整っているが、まれな場面で台帳が壊れる道がある

台帳（`unity_ledger_v1.csv`）は、1回の結果を1行ずつ足していくファイルで、ここにしか残らない数字もあります。夜に4本が同時にこの台帳へ書くので、書く所だけを codex（別の AI）にレビューしてもらいました。Mac のターミナルから codex を読むだけの形で起動し、22項目について答えてもらいました。

判定は「現状では、70回・4本同時の本番は見送りを勧めます」でした。指摘のうち重いものです。

| 項目 | 指摘 |
|---|---|
| A2 古い鍵 | 落ちた回の鍵を「古い」と判定した2本が、先に取り直した側の新しい鍵を消し、2本が同時に台帳へ書く道がある |
| A4 半端な行 | 書く途中で落ちると、次の行がくっつく |
| B3 書けなかった回 | 台帳に書けなくても終了コード0で「成功」になり、行が黙って抜ける |
| D6 マウントの確かめ | KIOXIA（外付けSSD）が外れていると、Mac の中にフォルダを作る道がある |

### 直したのは8つ、残りは今夜の前提で踏まないもの

最初の夜は、実行係を1つだけ起動し、途中で止めない・KIOXIA を抜かない・同じ夜を回し直さない前提で回します。その前提で、台帳が壊れる・行が黙って抜ける・Mac の中に書く、の3つの道だけを先に塞ぎました（依頼 v14、cs:21）。

| 直したこと | 中身 |
|---|---|
| 古い鍵 | 年齢だけで鍵を消すのをやめた。取れなければ60秒待ち、過ぎたら「台帳に書けなかった」で失敗にする。解放は自分の札の鍵だけ |
| 半端な行 | 鍵の中で、台帳の最後が改行かを確かめる。改行でなければ書かない（自動で切り詰めない） |
| 書けなかった回 | Python も59列の名前と順番を全部照合する。書けなかったら終了コード3にし、実行係が記録とまとめに出す |
| 行の版の欄 | 実行係が書く止まった回・落ちた回の行にも、版（schema 2）を入れる |
| 版を読む場所 | 実行ファイルの版の情報を探す場所のずれを直し、見つからなければ始めない |
| 保留ありの実行ファイル | 保留があるまま作った実行ファイルでは、夜を始めない |
| KIOXIA の確かめ | 本当にマウントされているかを、夜のフォルダを作る前・ログを開く前・各回を起こす直前に確かめる |
| runs.csv の書き方 | 値によって列がずれないよう、CSV の正しい書き方にする |

確かめは本物の台帳を使わず、コピーで行いました。鍵が残っている・最後が改行でない・ヘッダが1列違う・保留ありの版・KIOXIA がない、の5つの場面すべてで、書かずに断り、台帳は1バイトも変わらず、終了コードとログに出ました。そのあと本物で4本同時の1束（名札 fixcheck）を回し、台帳にちょうど4行入りました。

- ねらい：台帳が壊れたり、行が黙って抜けたりしたまま夜が進むのを防ぐ
- 変わったこと：書けない場面では必ず断り、終了コードとログで分かるようにした
- 見込める効果：3晩・200回で、台帳に書けなかった回は0、古い行の変化も0だった（毎晩、回す前の退避と md5 で照らした）

Claude Code は、レビューで「あとで直す」に回した7件のうち6件を、cs:20 ですでに直していました（同じ夜を回し直したときの扱い、Ctrl+C での記録、時刻の正確さ など）。この6件は codex に見せていないので、あとでまとめて見てもらいます。


## 作業ログ ― 最初の夜（70回）：店が空にならない回が1回出た

### 70回とも最後まで回り、台帳は無事だった

2026-10-26 の22:11に回し始め、23:25に終わりました（夜の名前 20261027）。③と⑦の店（⑦は③の列の間の通路を95cmに狭めた店）それぞれで、ロボット0・1・2台を種0〜9で回す60回（すれ違いは切）と、③ロボット2台・すれ違い入の10回です。回す前に、台帳を `archive/unity_ledger_v1_prenight_20261027/` にコピーで退避しました（121行・md5 bacc1e04…）。

- 70回とも completed・終了コード0。台帳に書けなかった回・止まった回・やり直しは0
- 台帳は121行 → 191行。先頭の121行の md5 は退避と同じ
- 1回の実時間は平均4.2分、最長14.4分（その1回が下の、店が空にならなかった回）

### 判定の印は、基準の幅が狭すぎることで付いていた

朝の判定（judge.md）では、70回のうち30回が「Unity の作り」、2条件が「試算を見直す候補」に振り分けられました。どれも、比べた基準（エディタで1条件3回だけ回した cs:7）の最小〜最大の幅を、少しはみ出したものです。3回の幅に10回を当てれば、はみ出すのは当たり前で、幅の狭さが主な理由と見ました。ロボットを増やしたときの向きが試算と逆になった条件はありませんでした。

そこで、この夜のすれ違い切の60回を、基準データ v3 にしました（エイイチの決定）。1条件10回なので、幅が広く、夜と同じ回し方（画面なし・4本同時）です。v3 で判定し直すと、60回とも正常になりました。

### 店が空にならなかった回：ロボットが同じ皿を持ったまま、56回やり直していた

1回だけ、③ロボット2台・すれ違い入・種0が、店の中の時間の上限720分まで回っても、お客さんが全員帰りませんでした。Claude Code に、何が止まっていたかを調べてもらいました。

| | 内容 |
|---|---|
| 客 | 2組。1組は食べ終わっているのに、残りの1皿を待って会計へ進めない。もう1組は1皿も食べ始めていない |
| ロボット | ロボット1が、その2皿を持って卓へ向かうが着けない。600秒で時間切れ → 待機場所へ戻る、を56回くり返した |
| スタッフ | 4人とも手が空いていて、仕事の板も空。皿はいつもロボットが持っていたので、一度も頼まれなかった |

時間切れのたびに皿は板に戻されていましたが、戻した同じ瞬間に、受け渡し口のすぐ横にいる同じロボットが取り直していました。歩いてくるスタッフより、ロボットの方が先に取ります。だから板はいつも空に見え、スタッフは手が空いたままでした。

なぜ卓に着けないのかは、この調べでは分かりませんでした。同じ種で回し直すと起きず、タイミングで起きる不具合です。同じ回の別の往復では、2台のロボットが互いの前をふさいで道が引けず、60秒待って諦めて引き直し、それでも引けない、をくり返す型が見つかりました。夜の回も、これが抜けられなかったと見ています。

原因が分からなくても、先に入れられる直しが1つありました。**同じ皿が2回時間切れになったら、ロボットに渡さずスタッフの仕事にする**（cs:23・cs:24）。「ロボットが詰まったら人がサポートする」という前からの決まりとも合います。入れる前に、v3 の60回で同じ皿が2回時間切れになったことがあるかを数えると、0回でした。入れても v3 の数字は1つも変わりません。

- ねらい：ロボットが詰まったまま、店が永久に終わらない回をなくす
- 変わったこと：同じ皿が2回時間切れになったら、スタッフが運ぶ
- 見込める効果：2回目の夜・3回目の夜の130回では、店が空にならなかった回は0だった。時間切れが14回あった回も、皿がスタッフに回って最後まで回った

### ロボット1台ずつの位置を、店の時間の5秒おきに残すようにした

次に同じことが起きたときに1回で原因が分かるよう、ロボット1台ずつの位置（`robot_track.csv`）を記録に足しました（cs:25）。時刻・ロボット・x・z・いまの仕事・すれ違い中か・後ずさり中か・向かっている卓です。busycheck で分かったとおり、記録を細かくしすぎると Mac が忙しくなり、ロボットの詰まり方そのものが変わります。そこで、1コマごとではなく、店の中の時間で5秒おきにしました。1回でおよそ2,200行です。cs:25 で回した1回（③1台・種1）は、主な9項目すべてが v3 の幅に入りました。


## 作業ログ ― 2回目の夜（70回）と、すれ違いの入と切の比べ

### 60回とも v3 の幅に入った

2026-10-27 の8:33から9:46に、cs:25 で同じ70回を回しました（夜の名前 20261028）。70回とも最後まで回り、台帳の先頭195行は退避と同じ md5 のままでした。すれ違い切の60回は、60回とも v3 の幅に入り、判定の印は1つも付きませんでした。直しを入れたあとでも結果が変わっていないことが確かめられました。すれ違い入の10回も全部最後まで回り、店が空にならない回はありませんでした。

### 入にすると、長く向かい合う止まりがはっきり短くなる

③ロボット2台の2晩分を合わせて比べました（入は最後まで回らなかった1回を除いた19回）。

| | 切（20回） | 入（19回） | 順位の比べ |
|---|---|---|---|
| 向かい合って止まった最長（秒） | 422 | 64 | はっきり短い（U=271／380） |
| 動けなくなった | 25.5 | 19 | 減る向き（U=253.5、はっきりではない） |
| 入店待ち（分） | 3.04 | 5.42 | 差があるとは言えない（U=171） |
| ロボットが運んだ割合 | 0.93 | 0.91 | ほぼ同じ |
| 人のあきらめ | 63 | 63 | 同じ |

20回と19回なら、順位の比べ（U）が120以下か260以上で、偶然では起きにくい差と見ます。入店待ちは中央値が上がって見えますが、種による幅が1.4〜14分と大きく、その中のばらつきです。

この結果から、すれ違い入を標準にしました（エイイチの決定）。実物の配膳ロボットの動き方に近く、客の待ちを悪くせず、ロボットどうしが長く向かい合う止まりが減るためです。


## 作業ログ ― すれ違い入を標準にし、3回目の夜を基準データ v4 にした

### Claude Code への依頼を、依頼箱から自動で渡した

ここまでは、Claude の書いた依頼文をエイイチが Claude Code に貼り付けていました。この日から、iCloud の `依頼箱/新規/` に依頼のファイルを置くと、Mac の見張り係が拾って Claude Code に渡し、報告を `依頼箱/結果/` に書き戻す形を使い始めました。依頼の頭に作業フォルダと上限時間を書き、自動で回してよいのは ServiceSim の作業フォルダだけです。

この形で、3つの依頼を続けて渡しました。

| 依頼 | 中身 | かかった時間 |
|---|---|---|
| 03 すれ違い入を標準に | 既定を入にし、実行ファイルを作り直して、60回の並びをから回しで確かめる（cs:26） | 5分 |
| 04 v4 の夜を回す | 始める前に5つ（Unity が閉じている・KIOXIA・台帳の md5・鍵なし・保留0）を確かめてから60回を回す | 80分 |
| 05 基準を v4 に | 判定の基準を v4 に替え、余分な回の指定の誤り止めと、手で起こしたときの既定の入をそろえる（cs:27） | 3分 |

- ねらい：人がコマンドや依頼文を貼り付けなくても、夜を回せるようにする
- 変わったこと：依頼のファイルを置くだけで、Claude Code が確かめてから回し、報告を書く
- 見込める効果：3回目の夜は、エイイチがコマンドを打たずに回った。台帳の退避だけは Claude が回す前に行った

### 3回目の夜（60回）：全部最後まで回り、v3 と比べて悪さは見えない

2026-10-27 の10:19から11:18に、全条件をすれ違い入で60回回しました（夜の名前 20261029・cs:26）。60回とも最後まで回り、店が空にならなかった回もありません。台帳は265行 → 325行で、先頭の265行は退避と同じ md5 でした。

v3（すれ違い切）と v4（すれ違い入）を、条件ごとに種10個の中央値で並べました。

| 条件 | 入店待ち（分）v3 → v4 | 動けなくなった v3 → v4 | 向かい合って止まった最長（秒）v3 → v4 |
|---|---|---|---|
| ③ 0台 | 4.08 → 4.21 | 0 → 0 | — |
| ③ 1台 | 4.56 → 3.01 | 8 → 4 | — |
| ③ 2台 | 3.04 → 4.18 | 20.5 → 25 | 630 → 102（はっきり短い） |
| ⑦ 0台 | 4.22 → 4.48 | 0 → 0 | — |
| ⑦ 1台 | 4.98 → 3.84 | 6.5 → 6 | — |
| ⑦ 2台 | 2.89 → 4.81 | 21.5 → 17 | 53 → 77 |

10回と10回なら、順位の比べが23以下か77以上で、はっきり違うと見ます。はっきり違ったのは、③2台の向かい合う最長（U=83）と、③1台の人のあきらめ（51.5 → 39.5、U=79.5）だけでした。通路の狭い⑦でも、すれ違い入による悪さは見えません。⑦2台で時間切れが増える向き（中央1.5 → 3回、U=25.5）はありますが、はっきりではなく、皿はスタッフに回って店は最後まで回っています。

この60回を基準データ v4 にしました（エイイチの決定）。判定の基準の既定を `20261029_night_cs26_*` に替え、v4 の60回そのものを判定にかけると、60回とも正常になりました。

### 判定に新しく出た印：③で1台 → 2台にすると、入店待ちが長くなる種が多い

3回目の夜の判定では、1つ新しい印が出ました。③の店で、ロボットを1台から2台に増やしたとき、10種のうち6種で入店待ちが長くなりました。サイトの試算では、ロボットを増やすと入店待ちは短くなります。その逆の向きです。v3（すれ違い切）では、この向きは出ていませんでした（1台4.56分 → 2台3.04分）。

2台にすると、ロボットどうしが会う場面が増えます。すれ違い入では、狭い所で速さを落として寄るので、その間に人の通り道が狭くなることが、入店待ちに効いているのかもしれません。まだ確かめていない見立てです。サイトの試算が正なので、Unity の数字に合わせてサイトを直すことはしません。次の店に広げながら、同じ向きが出るかを見ます。


## 作業ログ ― 次の店へ

③⑦の2つの店で、夜に回して朝に振り分ける流れが一通り通りました。次は、サイトにすでにある別の店の形に広げます。

| 店 | 形 | 卓 |
|---|---|---|
| ④ | 効率重視・狭い店・入口が右の壁 | 28 |
| ⑤ | ゆったり型・入口が正面の中央 | 46 |
| ⑥ | ゆったり型・入口が左の壁 | 38 |
| ② | 定番の郊外型（記憶の店） | 19 |

サイトが正なので、まずサイトの条件のファイル（serving-robot-params.json）に④の条件を書き出し、それを Unity が読んで店を建てます。1回ずつ回して確かめたあと、④の夜（ロボット0・1・2台 × 種10個）を回し、④の基準にします。

【図解】LOG384_pass_by_nights.html


## コードと記録の全量

この記事で使った分析・依頼文・codex のレビュー・Claude Code の報告・夜の記録・台帳の行・Unity のコード（ServiceSim/cs:27）の全文です。


## 比べに使った分析（compare_nights.py）

busycheck・すれ違いの入と切・v3 と v4 の比べは、このスクリプトで出しました。KIOXIA の service_sim/unity/ の中で動かし、台帳と各回の summary.json を読むだけです。

### compare_nights.py

```python
#!/usr/bin/env python3
"""compare_nights.py — LOG384 の比べ（busycheck・すれ違い入と切・v3 と v4）

service_sim/unity/ の中で動かす（台帳と各回の summary.json を読むだけ。何も書かない）。
  python3 compare_nights.py
順位の比べ（U）は「左が右より大きい組の数」。n=5 と 8 なら両側5%で U<=6 か U>=34、
n=10 と 10 なら U<=23 か U>=77、n=20 と 19 ならおよそ U<=120 か U>=260 が目安。
"""
import csv, json, os, statistics as st

ROWS = list(csv.DictReader(open('unity_ledger_v1.csv', encoding='utf-8')))
BY = {r['run_folder']: r for r in ROWS}


def summary(folder):
    try:
        d = json.load(open(os.path.join(folder, 'summary.json'), encoding='utf-8'))
    except (OSError, ValueError):
        return {}
    flat = {}
    def walk(x):
        for k, v in x.items():
            if isinstance(v, dict):
                walk(v)
            else:
                flat.setdefault(k, v)
    walk(d)
    return flat


def U(a, b):
    return sum((x > y) + 0.5 * (x == y) for x in a for y in b)


def med(v):
    return st.median(v) if v else float('nan')


def show(title, a_rows, b_rows, a_name, b_name, metrics):
    print('== %s（%s %d回 / %s %d回）' % (title, a_name, len(a_rows), b_name, len(b_rows)))
    for key, label, src in metrics:
        if src == 'ledger':
            a = [float(r[key]) for r in a_rows]
            b = [float(r[key]) for r in b_rows]
        else:
            a = [float(summary(r['run_folder']).get(key) or 0) for r in a_rows]
            b = [float(summary(r['run_folder']).get(key) or 0) for r in b_rows]
        print('  %-24s %s 中央 %9.3f（%.3f〜%.3f） %s 中央 %9.3f（%.3f〜%.3f） U(%s>%s)=%.1f/%d'
              % (label, a_name, med(a), min(a), max(a), b_name, med(b), min(b), max(b),
                 a_name, b_name, U(a, b), len(a) * len(b)))


LEDGER = [('stuck_count', '動けなくなった', 'ledger'), ('gave_up', '人のあきらめ', 'ledger'),
          ('entry_wait_mean', '入店待ち（分）', 'ledger'), ('robot_share', 'ロボットが運んだ割合', 'ledger'),
          ('rescues', '助けに行った', 'ledger'), ('late_staff_dishes', '店員が遅れて運んだ皿', 'ledger')]

# 1. busycheck（③1台・種1）
b1 = [r for r in ROWS if '_busy1_' in r['run_folder']]
b4 = [r for r in ROWS if '_busy4_' in r['run_folder']]
show('busycheck ③1台・種1', b1, b4, '1本ずつ', '4本同時', LEDGER)

# 2. すれ違い入と切（③2台・2夜分、全員が帰った回だけ）
def p1r2(prefix, on):
    out = []
    for s in range(10):
        f = '%s_fam_p1_r2_s%d%s' % (prefix, s, '_passon' if on else '')
        r = BY.get(f)
        if r and r['everyone_left'] == '1':
            out.append(r)
    return out
off = p1r2('20261027_night_cs21', False) + p1r2('20261028_night_cs25', False)
on = p1r2('20261027_night_cs21', True) + p1r2('20261028_night_cs25', True)
show('すれ違い ③2台・2夜', off, on, '切', '入', LEDGER + [
    ('face_stop_max_s', '向かい合って止まった最長（秒）', 'summary'),
    ('face_stop_count', '向かい合って止まった回数', 'summary'),
    ('timeouts', '往復の時間切れ', 'ledger')])

# 3. v3 と v4（条件ごと）
def night(prefix, on):
    g = {}
    for r in ROWS:
        f = r['run_folder']
        if f.startswith(prefix) and f.endswith('_passon') == on:
            g.setdefault('%s_r%s' % (r['store'], r['robots']), []).append(r)
    return g
v3 = night('20261027_night_cs21_', False)
v4 = night('20261029_night_cs26_', True)
for k in sorted(v4):
    show('v3 と v4 ' + k, v3[k], v4[k], 'v3', 'v4', [
        ('entry_wait_mean', '入店待ち（分）', 'ledger'), ('dish_wait_mean', '料理の待ち（分）', 'ledger'),
        ('stuck_count', '動けなくなった', 'ledger'), ('robot_share', 'ロボットが運んだ割合', 'ledger'),
        ('gave_up', '人のあきらめ', 'ledger'), ('warped', '瞬間移動', 'ledger'),
        ('timeouts', '往復の時間切れ', 'ledger'),
        ('face_stop_max_s', '向かい合って止まった最長（秒）', 'summary')])
```

### compare_nights_output.txt

```text
== busycheck ③1台・種1（1本ずつ 5回 / 4本同時 8回）
  動けなくなった                  1本ずつ 中央     1.000（1.000〜5.000） 4本同時 中央     7.000（2.000〜11.000） U(1本ずつ>4本同時)=4.0/40
  人のあきらめ                   1本ずつ 中央    34.000（23.000〜38.000） 4本同時 中央    38.000（32.000〜51.000） U(1本ずつ>4本同時)=8.5/40
  入店待ち（分）                  1本ずつ 中央     2.292（1.584〜2.330） 4本同時 中央     1.791（1.575〜3.046） U(1本ずつ>4本同時)=25.0/40
  ロボットが運んだ割合               1本ずつ 中央     0.699（0.675〜0.714） 4本同時 中央     0.660（0.636〜0.694） U(1本ずつ>4本同時)=35.0/40
  助けに行った                   1本ずつ 中央     1.000（1.000〜5.000） 4本同時 中央     7.000（2.000〜11.000） U(1本ずつ>4本同時)=4.0/40
  店員が遅れて運んだ皿               1本ずつ 中央    62.000（59.000〜67.000） 4本同時 中央    70.000（63.000〜75.000） U(1本ずつ>4本同時)=5.0/40
== すれ違い ③2台・2夜（切 20回 / 入 19回）
  動けなくなった                  切 中央    25.500（10.000〜47.000） 入 中央    19.000（5.000〜42.000） U(切>入)=253.5/380
  人のあきらめ                   切 中央    63.000（40.000〜98.000） 入 中央    63.000（27.000〜95.000） U(切>入)=195.0/380
  入店待ち（分）                  切 中央     3.038（1.506〜14.105） 入 中央     5.423（1.397〜13.344） U(切>入)=171.0/380
  ロボットが運んだ割合               切 中央     0.927（0.823〜0.975） 入 中央     0.905（0.658〜0.965） U(切>入)=226.0/380
  助けに行った                   切 中央    25.500（10.000〜48.000） 入 中央    19.000（5.000〜42.000） U(切>入)=248.5/380
  店員が遅れて運んだ皿               切 中央    14.000（5.000〜45.000） 入 中央    20.000（8.000〜67.000） U(切>入)=151.0/380
  向かい合って止まった最長（秒）          切 中央   421.921（10.050〜1872.201） 入 中央    63.699（4.200〜1501.940） U(切>入)=271.0/380
  向かい合って止まった回数             切 中央    24.500（10.000〜90.000） 入 中央    32.000（5.000〜188.000） U(切>入)=137.5/380
  往復の時間切れ                  切 中央     2.500（1.000〜5.000） 入 中央     2.000（1.000〜14.000） U(切>入)=156.0/380
== v3 と v4 fam_p1_r0（v3 10回 / v4 10回）
  入店待ち（分）                  v3 中央     4.084（1.486〜14.816） v4 中央     4.210（1.503〜15.247） U(v3>v4)=51.0/100
  料理の待ち（分）                 v3 中央     0.941（0.829〜1.049） v4 中央     0.938（0.802〜1.076） U(v3>v4)=52.0/100
  動けなくなった                  v3 中央     0.000（0.000〜0.000） v4 中央     0.000（0.000〜0.000） U(v3>v4)=50.0/100
  ロボットが運んだ割合               v3 中央     0.000（0.000〜0.000） v4 中央     0.000（0.000〜0.000） U(v3>v4)=50.0/100
  人のあきらめ                   v3 中央    11.000（5.000〜27.000） v4 中央    12.500（8.000〜18.000） U(v3>v4)=51.5/100
  瞬間移動                     v3 中央    11.000（7.000〜18.000） v4 中央    12.000（9.000〜19.000） U(v3>v4)=36.0/100
  往復の時間切れ                  v3 中央     0.000（0.000〜0.000） v4 中央     0.000（0.000〜0.000） U(v3>v4)=50.0/100
  向かい合って止まった最長（秒）          v3 中央     0.000（0.000〜0.000） v4 中央     0.000（0.000〜0.000） U(v3>v4)=50.0/100
== v3 と v4 fam_p1_r1（v3 10回 / v4 10回）
  入店待ち（分）                  v3 中央     4.564（1.429〜14.302） v4 中央     3.012（1.438〜11.458） U(v3>v4)=49.0/100
  料理の待ち（分）                 v3 中央     2.429（2.073〜2.877） v4 中央     2.398（2.012〜2.641） U(v3>v4)=56.0/100
  動けなくなった                  v3 中央     8.000（1.000〜14.000） v4 中央     4.000（0.000〜12.000） U(v3>v4)=67.0/100
  ロボットが運んだ割合               v3 中央     0.675（0.648〜0.794） v4 中央     0.710（0.648〜0.762） U(v3>v4)=34.0/100
  人のあきらめ                   v3 中央    51.500（35.000〜71.000） v4 中央    39.500（29.000〜69.000） U(v3>v4)=79.5/100
  瞬間移動                     v3 中央    51.500（44.000〜63.000） v4 中央    50.000（24.000〜61.000） U(v3>v4)=62.5/100
  往復の時間切れ                  v3 中央     0.000（0.000〜1.000） v4 中央     0.000（0.000〜1.000） U(v3>v4)=50.0/100
  向かい合って止まった最長（秒）          v3 中央     0.000（0.000〜0.000） v4 中央     0.000（0.000〜0.000） U(v3>v4)=50.0/100
== v3 と v4 fam_p1_r2（v3 10回 / v4 10回）
  入店待ち（分）                  v3 中央     3.038（1.506〜14.105） v4 中央     4.175（1.449〜12.779） U(v3>v4)=45.0/100
  料理の待ち（分）                 v3 中央     1.494（1.218〜1.920） v4 中央     1.618（1.115〜3.399） U(v3>v4)=32.0/100
  動けなくなった                  v3 中央    20.500（10.000〜47.000） v4 中央    25.000（6.000〜43.000） U(v3>v4)=51.0/100
  ロボットが運んだ割合               v3 中央     0.930（0.823〜0.975） v4 中央     0.920（0.709〜0.975） U(v3>v4)=55.5/100
  人のあきらめ                   v3 中央    59.500（40.000〜98.000） v4 中央    66.500（41.000〜84.000） U(v3>v4)=44.0/100
  瞬間移動                     v3 中央    64.000（50.000〜79.000） v4 中央    66.000（43.000〜86.000） U(v3>v4)=49.5/100
  往復の時間切れ                  v3 中央     2.000（1.000〜5.000） v4 中央     3.000（1.000〜15.000） U(v3>v4)=36.5/100
  向かい合って止まった最長（秒）          v3 中央   630.320（10.050〜1872.201） v4 中央   101.502（4.200〜563.086） U(v3>v4)=83.0/100
== v3 と v4 fam_p5_r0（v3 10回 / v4 10回）
  入店待ち（分）                  v3 中央     4.224（1.512〜15.562） v4 中央     4.484（1.583〜15.359） U(v3>v4)=44.0/100
  料理の待ち（分）                 v3 中央     0.979（0.795〜1.078） v4 中央     0.975（0.844〜1.011） U(v3>v4)=47.0/100
  動けなくなった                  v3 中央     0.000（0.000〜0.000） v4 中央     0.000（0.000〜0.000） U(v3>v4)=50.0/100
  ロボットが運んだ割合               v3 中央     0.000（0.000〜0.000） v4 中央     0.000（0.000〜0.000） U(v3>v4)=50.0/100
  人のあきらめ                   v3 中央    13.500（6.000〜24.000） v4 中央    15.000（8.000〜28.000） U(v3>v4)=45.5/100
  瞬間移動                     v3 中央    13.000（11.000〜24.000） v4 中央    14.000（11.000〜18.000） U(v3>v4)=41.0/100
  往復の時間切れ                  v3 中央     0.000（0.000〜0.000） v4 中央     0.000（0.000〜0.000） U(v3>v4)=50.0/100
  向かい合って止まった最長（秒）          v3 中央     0.000（0.000〜0.000） v4 中央     0.000（0.000〜0.000） U(v3>v4)=50.0/100
== v3 と v4 fam_p5_r1（v3 10回 / v4 10回）
  入店待ち（分）                  v3 中央     4.984（1.407〜12.052） v4 中央     3.837（1.534〜13.536） U(v3>v4)=47.0/100
  料理の待ち（分）                 v3 中央     2.447（1.863〜2.770） v4 中央     2.443（1.759〜3.154） U(v3>v4)=46.0/100
  動けなくなった                  v3 中央     6.500（3.000〜15.000） v4 中央     6.000（1.000〜22.000） U(v3>v4)=56.0/100
  ロボットが運んだ割合               v3 中央     0.691（0.648〜0.812） v4 中央     0.666（0.544〜0.825） U(v3>v4)=58.0/100
  人のあきらめ                   v3 中央    49.500（23.000〜86.000） v4 中央    45.000（22.000〜94.000） U(v3>v4)=57.0/100
  瞬間移動                     v3 中央    50.500（29.000〜72.000） v4 中央    48.000（29.000〜66.000） U(v3>v4)=58.0/100
  往復の時間切れ                  v3 中央     0.000（0.000〜1.000） v4 中央     0.000（0.000〜2.000） U(v3>v4)=44.0/100
  向かい合って止まった最長（秒）          v3 中央     0.000（0.000〜0.000） v4 中央     0.000（0.000〜0.000） U(v3>v4)=50.0/100
== v3 と v4 fam_p5_r2（v3 10回 / v4 10回）
  入店待ち（分）                  v3 中央     2.894（1.523〜11.680） v4 中央     4.807（1.429〜10.858） U(v3>v4)=52.0/100
  料理の待ち（分）                 v3 中央     1.378（1.021〜1.915） v4 中央     1.524（1.025〜2.569） U(v3>v4)=42.0/100
  動けなくなった                  v3 中央    21.500（5.000〜51.000） v4 中央    17.000（2.000〜28.000） U(v3>v4)=66.5/100
  ロボットが運んだ割合               v3 中央     0.939（0.850〜0.988） v4 中央     0.919（0.768〜0.979） U(v3>v4)=57.5/100
  人のあきらめ                   v3 中央    60.500（30.000〜116.000） v4 中央    50.500（30.000〜79.000） U(v3>v4)=66.5/100
  瞬間移動                     v3 中央    69.000（47.000〜78.000） v4 中央    61.000（35.000〜82.000） U(v3>v4)=57.0/100
  往復の時間切れ                  v3 中央     1.500（1.000〜7.000） v4 中央     3.000（1.000〜10.000） U(v3>v4)=25.5/100
  向かい合って止まった最長（秒）          v3 中央    53.374（17.500〜753.090） v4 中央    76.600（4.700〜1257.752） U(v3>v4)=47.0/100
```


## 依頼文とレビュー

### codex_review_request_v12_ledger.md（codex へのレビュー依頼・cs:19 に合わせて直した版）

```markdown
# コードレビュー依頼：夜の実行で台帳に書く所（サービスロボット導入シミュレーション・v12）

対象（読むだけ。書き換えない）：Unity プロジェクト `/Users/shimazueiichi/Robotics_UX/ServiceSim/`（ServiceSim/cs:19）

- `Assets/ServiceSim/Experiment/ServiceSimLedger.cs`（台帳に1行足す。鍵もここ。Editor の外へ移した版）
- `Assets/ServiceSim/Experiment/LedgerRow.cs`（1行の中身を作る。Editor と画面なしで共通）
- `Assets/ServiceSim/Experiment/HeadlessRun.cs`（画面なしの実行ファイルで1回回す入口）
- `Assets/ServiceSim/Editor/BuildHeadless.cs`（`build_info.json` を書く。台帳の `unity_cs` の出どころ）
- `tools/ledger.py`（夜の実行係が、止めた回・落ちた回の行を書く）
- `tools/night_runner.py`（何本も同時に起動し、やり直し・止める・判定を呼ぶ。`runs.csv` と `queue.json` の写しもここ）

---

## このコードがやろうとしていること

Unity で配膳ロボットの店を回すシミュレーションを、画面なしの実行ファイルで夜のあいだ何本も同時に回す。1回が終わるたびに、KIOXIA（外付けSSD）の台帳 `/Volumes/KIOXIA/r2r2r-data/service_sim/unity/unity_ledger_v1.csv` に1行足す。台帳は唯一の正本で、壊すと戻せない。このレビューでは**台帳に書く所と、その回を後から辿れるようにする記録だけ**を見てほしい（店の回し方・ロボットの動きは対象外）。

## 設計仕様（台帳の決まり：data_manifest_rules.md 10節）

1. 1回 = 1行・追記のみ。既存の行の上書き・移動・削除をしない
2. 追記ガード：実体のヘッダとコードの列の並び（59列）が違えば書かない
3. 同じ出力フォルダ名の行を2度書かない（出力フォルダ名が照合キー）。やり直しは名前の後ろに `_try2` を付ける
4. 何本も同時に書くので、鍵ファイル `service_sim/unity/unity_ledger_v1.lock` を排他で開けてから書く。開けなければ待って試し直す。C#（実行ファイル）と Python（実行係）が同じ鍵を使う
5. 実行係が止めた回（時間切れ）・落ちた回は、実行係が status = stopped / failed の行を書く（鍵をかけて）
6. 書く前に KIOXIA がつながっているか確かめる。つながっていなければ何も書かない（Mac の中に fallback しない）
7. `unity_cs` は `ServiceSim/cs:N` の形。値は実行ファイルのそばの `build_info.json` から来る
8. **`night/{夜}/runs.csv`**（2026-10-26 追加）：1回ごとの開始・終了の時刻を、completed も含めて全部1行ずつ残す。
   列は `folder,condition,seed,pass_by,tryno,launch_running,parallel,started_iso,ended_iso,started_unix,ended_unix,wall_s,status,exit_code`。
   **なぜ要るか**：`run_info.json` の `parallel_actual` は「その回を**起動したとき**に同時に走っていた本数」なので、並びの最後で同時本数が減るのは出てこない（減るのは回している途中）。開始・終了の時刻をそのまま残せば、「その回が回っている間ほかに何本走っていたか」を後から正確に計算できる。
   同時本数は測ってみると `stuck_count`・`rescues`・`robot_share`・`late_staff_dishes` を動かす（下の補足）ので、どの回が何本の中で回ったかを後から言えることが要る。
   **台帳の列は増やさない**（10節）。だから台帳の外の記録にした
9. **`night/{夜}/queue.json`**（2026-10-26 追加）：その夜に並べた回の写し。`queue.json` はプロジェクトの中で夜ごとに作り直し VCS に入れないので、写さないと「その夜に何をどの順で並べたか」がどこにも残らない。もうあれば上書きしない（同じ夜を続きから回したとき、最初の並びを残す）

## レビューしてほしいチェック項目

### A. ロジックの正しさ

A1. C# と Python の鍵は、本当に同じ鍵として待ち合えるか（使っている仕組み：ファイルの排他オープン／flock／ロックファイルの作成と削除 など。macOS の外付けSSD（exFAT か APFS）で同じように効くか）
A2. 鍵を持ったまま落ちた場合（実行ファイルが落ちる・実行係が強制終了される）、鍵が残って次の回が永久に待たないか。古い鍵の扱い
A3. 「ヘッダの確認」「もう同じフォルダの行があるか」「1行足す」が、すべて鍵の中で行われているか
A4. 1行を書く途中で落ちたとき、半端な行が残らないか（1回の write で書いているか・改行の扱い）
A5. 実行係が stopped / failed の行を書いたあと、遅れて実行ファイル自身も同じフォルダの行を書く、という二重書きが起きないか（時間切れで止めた回を SIGTERM したのに、終了処理で台帳に書いてしまう など）
A6. `_try2` の名前の付け方と、「completed は飛ばす」の照合が、C# と Python で同じ規則か
A7. 出力フォルダ名を決める式が C#（`HeadlessRun.FolderName`）と Python（`night_runner.folder_name`）で**完全に同じ**か。食い違うと、実行係が「もう回した」と誤って飛ばす／同じ回を2度回す
A8. 台帳の `unity_cs` は `build_info.json` から来る。**保留中の変更があるまま作った実行ファイル**でも、警告は出るが止まらない（`BuildHeadless.cs`）。この場合、台帳の `unity_cs` は `cs:N` なのに中身は `cs:N` と違う、という取り違えが起きる。止めるべきか、警告のままでよいか意見がほしい
A9. **`runs.csv`**：completed・stopped・failed・時間切れで止めた回のすべてで1行書かれるか。書き落ちる道はないか（`reap` を通らない終わり方があるか）
A10. **`runs.csv`** の `started_unix` / `ended_unix` から、任意の回について「回っている間に重なっていた本数」を正しく復元できるか（`ended = started + wall_s` の整合、時計の取り方、夏時間や時計合わせでずれないか）

### B. 副作用・破壊チェック

B1. 既存の行（117行）を書き換える経路が1つもないか（ファイルを開くモード：追記のみか）
B2. CSV のクオート・カンマ・改行を含む値（日本語の pending_changes など）で列がずれないか
B3. ヘッダが違うときに、黙って書かないだけでなく、失敗として記録・終了コードに出るか
B4. **`runs.csv` と `queue.json` の写し**が、台帳や回の出力フォルダを壊す道はないか（書く先が `night/{夜}/` の中だけか）
B5. **`queue.json` の写し**が、同じ夜を続きから回したときに最初の並びを上書きしないか
B6. `runs.csv` のヘッダがコードの並びと違うとき、黙って追記して列をずらさないか

### C. 暴走・安全リスク

C1. 鍵が取れないまま待ち続けて、夜じゅう止まる道はないか（待つ上限と、その後の扱い）
C2. 実行係が同じ回を何度も起動し続ける道はないか（やり直しは1度だけ）
C3. KIOXIA が途中で外れたとき、新しい回を始めず、Mac の中に書かないか
C4. **`runs.csv` が開けない・書けないときに、夜が止まってしまわないか**（記録のために本体を落としていないか）。逆に、黙って記録だけが欠ける場合、それが分かるか

### D. 制約ルールの遵守

D5. 台帳への追記処理を持たない「通し完走」の経路がないか（回の終わり方が completed / stopped / failed のどれでも1行残るか）
D6. KIOXIA へ書き込む所すべてに、マウントの確かめが入っているか（未マウント時にローカルへ fallback しない）
D7. 台帳の列を増やしていないか（59列・schema version 2 のまま。`parallel` / `parallel_actual` は `run_info.json` と `runs.csv` 側に置き、台帳には入れていない）
（D1〜D4 は実機のアーム用の項目なので対象外）

## レビュー結果のフォーマット

各項目について、✅ OK／⚠️ 要注意（何が懸念か）／❌ NG（該当行番号と修正案）で答えてほしい。
最後にサマリとして：**70回・4本同時**の夜に進んで良いか、進む前に直すべき項目があるか。

## 補足情報

- 実行ファイルは `Builds/headless/ServiceSim.app`（`build_id: cs19_20261026_1649`・保留 0・ARM64）。`-batchmode -nographics` で1本 = 1回
- 夜の同時本数は **N = 4** に決めた。実測で 49.5 回/時（70回に約63分）。CPU 5.7コア／8コア・メモリ 490 MB／16 GB
- 2026-10-26 に Unity 抜き・実物の両方で確かめた所（レビューの参考にしてよい）
  - 台帳の鍵：8本同時に追記して 8 行・59 列・ヘッダ無傷・鍵が残らない
  - C# と Python が同じ鍵を**両方向**で待ち合える（C# が 3 秒持つ間 Python が 2.5 秒待って取得／逆も）
  - 出力フォルダ名が C# と Python で 8 通り一致（版が読めないときの `csunknown` も一致）
  - 本物の台帳で 2本同時（名札 acheck）・1本（bcheck）・1〜4本同時（c1〜c4check）・
    1本×5回と4本同時×8回（busy1 / busy4）・1回（pcheck）＝ 合わせて 30 行を追記。
    すべて 59 列・completed・重複なし・退避版の 86 行は 1 byte も変化なし
  - 同時本数で動いた列は `stuck_count`・`rescues`・`robot_share`・`late_staff_dishes` の4つだけ
    （入店待ち・店員待ち・料理待ち・会計待ち・回転・店の中の時間は動かない）
- 台帳は確かめの前に `archive/unity_ledger_v1_preacheck_20261026/` にコピーで退避済み（md5 一致）。
  最初の夜の前にも `archive/unity_ledger_v1_prenight_{夜}/` に写す
- `runs.csv` と `queue.json` の写しは、まだ**本物の夜では動かしていない**（Unity 抜きで 19 件の確かめは通っている）
- `git clean` 禁止・`git add .` 禁止（個別ファイル指定）
```

### codex_review_result_v12_ledger_1.md（codex のレビュー結果）

```markdown
**現状では、70回・4本同時の本番は見送りを勧めます。** 通常の追記経路は整っていますが、古い鍵の回収で排他が破れる経路と、台帳への追記失敗を成功終了として扱う経路があります。追加された時刻記録にも修正が必要です。

ファイルは変更していません。コードの読解、実台帳の読み取り、ファイル操作をモックしたPython検証で確認しました。Unityの起動・実台帳への追記はしていません。

現物は **exFAT**。台帳はヘッダ込み117行で、ヘッダ59列・フォルダ名の重複なしでした。履歴中の旧49列行は異常扱いしていません。ビルド情報も指定の `cs19_20261026_1649`・保留0・schema 2と一致しています。

**A：ロジックの正しさ**

| 項目 | 判定・根拠・修正案 |
|---|---|
| **A1 鍵の互換性** | ✅ **OK。** C#は `FileMode.CreateNew`、Pythonは `O_CREAT｜O_EXCL` による**同名ファイルの排他的作成**です。`FileShare.None` とPythonの `flock` を組み合わせた方式ではありません。同じ実ディレクトリを使う限り互換性があります。[C#:406](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/ServiceSimLedger.cs:406)、[Python:66](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/ledger.py:66)。Appleもこの方式を単純な排他に利用できると説明しています。[open(2)](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/open.2.html)。ご提示の両方向実測も裏付けになります。APFSでの追加実測は今回していません。 |
| **A2 落ちた後の鍵** | ❌ **NG。古い鍵の自動削除に競合があります。** 複数プロセスが古い鍵と判定すると、先に取得し直したプロセスの**新しい鍵を、別の待機者が削除できます**。さらに解放時も、現在の鍵が自分のものか確認せず削除します。[C#:429](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/ServiceSimLedger.cs:429)、[C#:485](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/ServiceSimLedger.cs:485)、[Python:74](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/ledger.py:74)、[Python:95](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/ledger.py:95)。PID・ホスト名は書くだけで、生存確認に使っていません。**修正案：年齢だけの自動削除をやめる。恒久策は、両言語を同じOSロックへ統一し、鍵ファイルを削除しない方式にすることです。** |
| **A3 確認と追記の範囲** | ✅ **OK。** 最終的なヘッダ確認・重複確認・追記は、両言語とも鍵の中です。起動前のヘッダ確認に加え、追記直前にも再確認しています。[C#:340](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/ServiceSimLedger.cs:340)、[Python:183](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/ledger.py:183)。ただし、鍵自体の問題はA2、ヘッダ判定の不足はB3にあります。 |
| **A4 途中までの行** | ❌ **NG。半端な末尾を防ぐ・検出する処理が不足しています。** `StreamWriter.Write`／テキストの `write` 1回は、障害時まで含めた原子的な追記を保証しません。C#は末尾改行を確認せず、次の行を連結し得ます。[C#:351](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/ServiceSimLedger.cs:351)。Pythonは改行を補いますが、半端な行を正常化できるわけではありません。[Python:198](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/ledger.py:198)。**修正案：UTF-8行を事前生成して書き込みをまとめ、鍵の中で末尾の完全性を検証。不完全なら追記を拒否し、復旧用の回別記録を残す。既存末尾を自動切り詰めする修正は避けるべきです。** |
| **A5 停止後の二重書き** | ⚠️ **要注意。正常な鍵と同じフォルダ名なら、後着側は重複確認で追記しません。** Unityの終了フックも台帳へは追記せず、途中結果を出力するだけでした。[ExperimentRunner.cs:1556](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/ExperimentRunner.cs:1556)。ただし `_kill` はSIGKILL後の `wait()` がなく、停止エラーも無視します。[night_runner.py:372](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:372)。C#の重複確認も読込エラーを「未登録」と扱います。[ServiceSimLedger.cs:176](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/ServiceSimLedger.cs:176)。**終了確認を必須にし、照合エラー時は追記拒否にしてください。** タイムアウト直前にcompleted行が書かれていた場合、台帳と `runs.csv` のstatusが食い違う点も要処理です。 |
| **A6 `_try2` と再開** | ❌ **NG。接尾辞は一致しますが、再開判定が初回しか見ません。** 初回failed・`_try2` completedでも、再開すると初回を起動し、その後 `_try2` も再度起動し得ます。[night_runner.py:438](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:438)、[night_runner.py:362](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:362)。モックでも再現しました。**修正案：初回と `_try2` の両方について既存statusを確認し、完走済み・試行回数上限を再開後も守る。** |
| **A7 フォルダ名の完全一致** | ❌ **NG。今回の70件・既定設定では一致しますが、設定可能な範囲全体では一致しません。** `--cs-token ss{N}` をPythonだけが使い、Unityへ渡していません。またPythonは `pass_by` を大文字小文字変換せず、Unityは変換します。[night_runner.py:126](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:126)、[HeadlessRun.cs:125](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/HeadlessRun.cs:125)。さらにPythonの `.app` 外側の `build_info.json` 探索が1階層浅く、既定ビルドや `cm status` へfallbackします。[night_runner.py:98](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:98)。**修正案：tokenを固定または双方へ伝達、pass_byをon/offに限定、指定実行ファイルに対応する同じbuild_infoを双方で必須にする。** |
| **A8 保留ありビルド** | ⚠️ **要注意。夜用は止めるべきです。** 現在は警告のみで、同じ `unity_cs` が異なる内容を指せます。[BuildHeadless.cs:78](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Editor/BuildHeadless.cs:78)。さらにHeadless側は `pendingNote = ""` としており、回別記録にも保留情報が引き継がれません。[HeadlessRun.cs:244](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/HeadlessRun.cs:244)。**夜用ビルド・起動時に保留あり／版不明を拒否**するのが適切です。指定の保留0ビルドには、この取り違えの証拠はありません。 |
| **A9 runs.csvの全終了経路** | ❌ **NG。通常終了・クラッシュ・時間切れは `reap` で記録されますが、Ctrl+Cでは子を止めるだけで記録しません。** [night_runner.py:588](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:588)。SIGTERMの処理もなく、起動・ログ・台帳操作の予期しない例外でも処理中の回が残ります。**修正案：全終了経路を共通の終了記録処理へ通す。強制終了後の回収用に、開始時点の記録も永続化する。** SIGKILLは終了ハンドラだけでは救えません。 |
| **A10 重なりの復元** | ❌ **NG。「検知時刻での概算」はできますが、正確な復元はできません。** 開始は `Popen` 後、終了は `poll` で発見した後です。[night_runner.py:332](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:332)、[night_runner.py:341](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:341)。通常のpoll間隔に加え、先に処理した回の台帳待ち等も終了時刻へ混入します。`ended = started + wall` は内部では整合しますが、3桁丸めの差もあります。Unix秒は夏時間に左右されませんが、`time.time()` は時計合わせで前後し得ます。**修正案：終了待機を記録I/Oから独立させて即時採時し、単調時計による共通時刻軸とUTCとの対応を保存する。** [Python time仕様](https://docs.python.org/3/library/time.html)。 |

**B：副作用・破壊チェック**

| 項目 | 判定・根拠・修正案 |
|---|---|
| **B1 既存行の上書き** | ✅ **OK。** 対象の台帳書込はC#の `FileMode.Append` とPythonの `'a'` で、台帳をtruncate・置換・削除する経路はありません。[C#:352](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/ServiceSimLedger.cs:352)、[Python:205](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/ledger.py:205)。A4の末尾破損リスクは別途残ります。 |
| **B2 CSV特殊文字** | ⚠️ **要注意。台帳はカンマ・引用符・改行を空白に変えるため、列ずれは防ぎますが値は変わります。** 日本語自体は保持します。[ServiceSimLedger.cs:502](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/ServiceSimLedger.cs:502)、[ledger.py:159](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/ledger.py:159)。`pending_changes` はJSONで適切にエスケープされています。一方、**runs.csvはCSVエスケープなし**です。[night_runner.py:266](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:266)。カンマ入りpass_byで15列になることを再現しました。**`csv.writer` を使い、label・pass_by等を事前検証してください。** C#は重複照合前と保存後でフォルダ名が変わらないよう、不正文字を拒否する必要もあります。 |
| **B3 ヘッダ不一致の失敗通知** | ❌ **NG。二つ問題があります。** ①Pythonは先頭6列と一部必須列しか確認せず、58列や末尾の列順変更を通します。[ledger.py:124](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/ledger.py:124)。②C#はAppendのfalseを警告するだけでstatusを変えず、完走なら終了コード0になります。[LedgerRow.cs:194](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/LedgerRow.cs:194)、[HeadlessRun.cs:336](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/HeadlessRun.cs:336)。実行係はcompletedへの補完追記をしません。**修正案：Pythonも59列全体・順序を照合し、追記失敗を呼出元と終了コードまで伝播する。正常な重複拒否とは区別する。** |
| **B4 写しの書込先** | ✅ **OK。** `runs.csv` と `queue.json` の宛先は固定で `night/{夜}/` 内です。CLIの夜名も8桁数字に制限されています。[night_runner.py:228](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:228)、[night_runner.py:423](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:423)、[night_runner.py:543](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:543)。台帳や回別出力を直接上書きする経路はありません。マウント確認はD6でNGです。 |
| **B5 queueの上書き防止** | ✅ **OK〔単一runnerで順次再開する場合〕。** 宛先が存在すればcopyを飛ばします。[night_runner.py:427](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:427)。ただしコピー中断で残った不完全ファイルも「既存」として扱います。同じ夜のrunner二重起動にも備えるなら、一時ファイルへ完全に書いた後、既存を置換しない公開方法にするのが適切です。 |
| **B6 runsヘッダの照合** | ✅ **OK。** 既存の非空ファイルはヘッダを比較し、不一致なら追記せずログへ出します。[night_runner.py:239](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:239)。 |

**C：暴走・安全リスク**

| 項目 | 判定・根拠・修正案 |
|---|---|
| **C1 鍵の待機上限** | ⚠️ **要注意。通常は両言語とも60秒で諦めます。** ただし待機上限を実時計で測るため、時計の後退で延びます。[C#:399](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/ServiceSimLedger.cs:399)、[Python:62](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/ledger.py:62)。また、落ちた直後の鍵は120秒まで古い扱いにならず、次の60秒待機が先に失敗し得ます。**待機は単調時計で測り、失敗をB3のように成功扱いしないことが必要です。** |
| **C2 やり直し上限** | ❌ **NG〔同じ夜の再開を含む場合〕。** 単一起動中はtryno=1だけを再試行するので最大2回ですが、上限が再開後に引き継がれません。A6の修正が必要です。今回のqueueは70件すべて別フォルダ名で、queue自体の重複はありません。 |
| **C3 途中でKIOXIAが外れる** | ❌ **NG。確認が外側のループだけなので、内側で複数本を起動する途中に外れると次の回を始めます。** [night_runner.py:454](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:454)。Unityも開始時の確認後、完了時には再確認しません。**各起動直前と書込直前に確認し、外れたら新規起動・ディレクトリ作成を止めてください。** |
| **C4 runs書込失敗時の継続** | ❌ **NG。runs.csvだけの障害は捕捉しますが、そのエラーを出す `self.log()` が失敗すると例外が外へ漏れます。** [night_runner.py:274](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:274)、[night_runner.py:71](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:71)。同じSSDの容量不足・取り外しでは両方失敗し得ます。モックで再現しました。**修正案：ログ書込失敗も捕捉して標準エラーへ通知し、記録欠落数を保持して最終結果に出す。実行中の回の回収処理は継続する。** |

**D：制約ルール**

| 項目 | 判定・根拠・修正案 |
|---|---|
| **D5 全終了で台帳1行** | ❌ **NG。** B3により「完走・終了コード0・台帳なし」が成立します。A9の手動停止経路でも欠落します。また、`summary.json` があれば `run_info.json` が欠落・破損していてもcompleted判定です。[LedgerRow.cs:74](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/LedgerRow.cs:74)。**追記結果の伝播、共通終了処理、完了判定に必要な記録の検証が必要です。** |
| **D6 全書込のマウント確認** | ❌ **NG。開始前にも穴があります。** `main` はprecheckより先に夜フォルダを作り、ログを開きます。[night_runner.py:548](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:548)。queueコピー・runs書込にも確認がなく、C# Appendは保存先を自動作成します。[ServiceSimLedger.cs:319](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/ServiceSimLedger.cs:319)。残存マウントポイントと権限の条件次第で、ローカル側へ作成する経路があります。Unityの確認も `Directory.Exists` で、実マウントの確認ではありません。[HeadlessRun.cs:193](/Users/shimazueiichi/Robotics_UX/ServiceSim/Assets/ServiceSim/Experiment/HeadlessRun.cs:193)。**最初の書込より前に実マウントを確認し、書込共通入口でも対象デバイスを確認。不在時は親ディレクトリも作らないようにしてください。** |
| **D7 59列・schema 2** | ❌ **NG〔Python生成行のschema値〕。** 列追加はなく、parallel関連の分離もOKです。C#はschema 2を書きますが、Pythonのstopped/failed行には `ledger_schema_version` がなく、空欄になります。[night_runner.py:392](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/night_runner.py:392)、[ledger.py:193](/Users/shimazueiichi/Robotics_UX/ServiceSim/tools/ledger.py:193)。モックでも確認しました。**列を増やさず、Python生成行にも検証済みのschema version 2を設定してください。** |

**70回・4本同時へ進む前の必須修正**は、次のまとまりです。

- **排他と台帳保全：** A2、A4、B3
- **再開・照合・版情報：** A6、A7、D7
- **全終了の記録と時刻精度：** A9、A10、D5
- **取り外し・記録障害への対応：** C3、C4、D6

保留ありビルドは、夜用では拒否する方針を勧めます。修正後、隔離した台帳で異常系を確認し、実物の4本同時で `runs.csv` とqueueの写しを確認してから、予定の退避を取って70回へ進むのが妥当です。
```

### claude_code_request_v14_ledger_fixes.md（今夜の前に直す8つの依頼）

```markdown
# 依頼 v14：codex レビュー（台帳に書く所）の指摘のうち、最初の夜の前に直すもの（2026-10-26）

レビュー結果：同じフォルダの codex_review_result_v12_ledger_1.md（判定は「見送り」）。
最初の夜（20261026・70回・4本同時）は、実行係を1つだけ起動し、途中で止めない・KIOXIA を抜かない・同じ夜を回し直さない前提で回す。その前提で、台帳が壊れる・行が黙って抜ける・Mac の中に書く道だけを先に塞ぐ。残りは「あとで直すもの」に回す。

## 変えないもの
- 店の回し方・ロボットと人の動き・記録の中身
- 台帳の列（59列・schema 2）。今までの行
- 夜の並び（種ごと）・runs.csv・queue.json の写し

## 今夜の前に直すもの

1. A2 古い鍵：年齢だけで鍵を消す処理をやめる（C# と Python の両方）。鍵が取れないまま待ちの上限を過ぎたら、その回は「台帳に書けなかった」として失敗にする（下の 3）。解放は、鍵の中身が自分のもの（PID など）のときだけ消す。待ちの上限は単調時計で測る
2. A4 半端な行：鍵の中で、台帳の最後の1バイトが改行かを確かめる。改行でなければ追記しない（自動で切り詰めない）。1行は UTF-8 で先に作り、1回で書く
3. B3 書けなかったときの扱い：Python も59列の名前と順番を全部照合する。C# の Append が false（重複で正しく拒否した場合は除く）なら、実行ファイルの終了コードを 3 にする。実行係は終了コード 3 を「台帳に書けなかった回」として runner_log と最後のまとめに出す
4. D7：Python が書く stopped / failed の行にも ledger_schema_version = 2 を入れる
5. A7（今夜に関わる所だけ）：night_runner.py の build_info.json を探す場所を、実行ファイルの .app と同じ所に直す。見つからなければ始めない（cm status に頼らない）
6. A8：保留がある状態で作った実行ファイルは、夜の実行係が始めない（build_info の pending_changes が空でなければ止める）
7. D6・C3 マウント：KIOXIA が本当にマウントされているかを os.path.ismount('/Volumes/KIOXIA') で確かめる。夜のフォルダを作る前・ログを開く前・各回を起動する直前に確かめ、外れていたら新しい回を起動せず、フォルダも作らない。C# 側も書く前に同じ確かめ（Directory.Exists ではなく、マウントされていること）
8. B2：runs.csv は csv.writer で書く

## 確かめ（本物の台帳は使わない）
- 台帳のコピーを別の場所に置き、そこを指して確かめる（--unity-dir など）。本物の台帳と夜のフォルダには触らない
- 確かめる場面：鍵が残っている／台帳の最後が改行で終わっていない／ヘッダが1列違う／保留ありの build_info／KIOXIA がない（存在しないパスを渡す）
- それぞれで「台帳が1バイトも変わらない」「終了コードとログに出る」ことを報告する
- そのあと本物で、③1台・種1を4本同時で1束だけ回し（名札 fixcheck）、台帳に4行・runs.csv に4行入ることを確かめる
- 実行ファイルは直したあとの cs で、保留0で作り直す

## あとで直すもの（今夜は直さない。報告に一覧で残す）
- A6・C2 同じ夜を回し直したときの _try2 の扱い
- A9 Ctrl+C・SIGTERM で止めたときの runs.csv の記録
- A10 開始・終了の時刻の正確さ（今は5秒の見回りの分だけずれる）
- C4 ログが書けないときの例外
- A5 止めた回の wait()・照合エラー時の扱い
- B5 queue.json の写しの書き方
- A7 の残り（--cs-token ss{N}・pass_by の大文字小文字）

## 順番
- 作業の区切りごとに VCS の check-in。git add . と git clean は使わない
- 終わったら claude_code_reports に報告を書く
```

### 20261027_03_すれ違い入を標準に.md（依頼箱 03（すれ違い入を標準に））

```markdown
---
作業フォルダ: /Users/shimazueiichi/Robotics_UX/ServiceSim
上限時間: 60
---
# 依頼：すれ違い入を標準にし、基準データ v4 の夜の準備をする（2026-10-27）

エイイチの決定（2026-10-27）：ロボットどうしのすれ違いは「入」を標準にする。
根拠：2夜分（20261027・20261028）の ③2台で、入（19回）は切（20回）より、向かい合って止まった一番長い回がはっきり短い（中央 422秒 → 64秒）。客の待ち・ロボットが運んだ割合・人のあきらめは差があると言えない。

## やること

1. RobotSpec の passByEnabled の既定を「入」にして check-in する。
2. tools/make_queue.py の --pass-by の既定を on にする。--pass-by off で切の回も作れるまま残す。--pass-by-extra もそのまま残す。
3. 実行ファイルを作り直す（保留 0 で。Unity が開いていればコマンドから作れないので、その場合は手を止めて報告）。
4. --dry-run で、夜の名前 20261029・4本同時の 60回（③ fam_p1・⑦ fam_p5 × ロボット 0・1・2台 × 種0〜9、全部すれ違い入、種ごとの並び）を確かめる。60回が並び、出力フォルダ名の末尾の付け方（入が標準になったときに _passon が付くか付かないか）を報告する。
5. judge_night.py の基準の既定は v3（20261027_night_cs21_*、_passon を除く）のまま変えない。v4 の夜が終わってエイイチが確かめたあとで替える。

## 変えないもの

- 店の回し方・人の動き・皿の打ち切り上限・robot_track.csv
- 台帳の列（59列・schema 2）と今までの行。台帳に新しい行を書く確かめはしない（--dry-run だけ）
- 本番の夜は回さない（エイイチが台帳を退避してから回す）

## 報告

claude_code_reports に 20261027_HHMM_passby_default_on.txt として書く（場所：~/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/unity/claude_code_reports/）。
入れた cs 番号・作った実行ファイルの build_id と保留の有無・--dry-run の最初と最後の数行・出力フォルダ名の形・本番のコマンド（夜の名前 20261029）を書く。
```

### 20261027_04_v4の夜を回す.md（依頼箱 04（v4 の夜を回す））

````markdown
---
作業フォルダ: /Users/shimazueiichi/Robotics_UX/ServiceSim
上限時間: 150
---
# 依頼：基準データ v4 の夜（20261029・60回・すれ違い入）を回す（2026-10-27）

エイイチが「v4 を回す」と決め、回し始めるのを自動にするよう頼んだ。台帳の退避は Claude がもう済ませた。
- 退避先：/Volumes/KIOXIA/r2r2r-data/service_sim/unity/archive/unity_ledger_v1_prenight_20261029/unity_ledger_v1.csv
- 265 行・md5 b7f65cde2f6f5b1396cb706974bea226

## 始める前に確かめること（1つでも違えば回さずに報告して終わる）

1. Unity エディタが起動していない（pgrep などで確かめる。起動していたら閉じずに報告して終わる）
2. /Volumes/KIOXIA がマウントされている（os.path.ismount）
3. 本物の台帳 /Volumes/KIOXIA/r2r2r-data/service_sim/unity/unity_ledger_v1.csv の md5 が上の退避と同じ（違えば、退避のあとに台帳が変わっている）
4. 鍵ファイル unity_ledger_v1.lock がない
5. ServiceSim の保留が 0 で、実行ファイルの build_info が cs26_20261027_0956・保留なし

## 回す

作業フォルダで、次の2つを順に実行する。night_runner.py は終わるまで待つ（約50分）。

```
python3 tools/make_queue.py --out queue.json
python3 tools/night_runner.py --queue queue.json --parallel 4 --night 20261029
```

- 回している間は、ほかの作業をしない。Assets・tools・設定ファイルを開かない・変えない。Unity を開かない
- Ctrl+C で止めない。途中で止まっても、同じ夜を回し直さない（そのまま報告する）

## 報告

依頼箱の結果に加えて、claude_code_reports に 20261029_night_v4.txt として書く（場所：~/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/unity/claude_code_reports/）。
- 始める前の確かめ5つの結果
- runner_log.txt のまとめ（completed の回数・失敗・止まった回・終了コード3・最長の実時間）
- 台帳の行数（265 → 325 になるはず）と、先頭 265 行の md5 が退避と同じか
- 全員が帰らなかった回があれば、その回の名前（原因の調べはしない）
- judge.md ができたか（中身の判断はしない。v4 にするかはエイイチと Claude が決める）
````

### 20261027_05_基準をv4に.md（依頼箱 05（基準を v4 に））

```markdown
---
作業フォルダ: /Users/shimazueiichi/Robotics_UX/ServiceSim
上限時間: 60
---
# 依頼：基準データを v4 に替える（2026-10-27）

エイイチの決定（2026-10-27）：v4 の夜（20261029_night_cs26_*・すれ違い入・60回）を基準データ v4 にする。
v3（20261027_night_cs21_*、すれ違い切）と比べて、③2台で向かい合って止まる最長がはっきり短く、それ以外の数字にはっきりした差がなかったため。

## やること

1. tools/judge_night.py の基準の既定を 20261029_night_cs26_* にし、既定の除外（*_passon*）を外す（--baseline-exclude で指定したときだけ除く形は残す）。
2. tools/make_queue.py：--pass-by が on のときに --pass-by-extra が指定されたら、誤りとして止める（同じ回が2回並び、出力フォルダ名がぶつかるため）。
3. 実行ファイルを手で起こしたときの -ssPassBy の既定を入にそろえる（HeadlessRun.cs）。夜の実行係は毎回指定を渡すので、夜の回の動きは変わらない。
4. 3 で Assets を変えたので、実行ファイルを作り直す（保留 0）。
5. 確かめ（台帳に新しい行を書かない）
   - judge_night.py を v4 の60回そのものに当てて（--runs '20261029_night_*' --out は一時フォルダ）、正常 60・回の失敗 0・基準なし 0 になること
   - make_queue.py に --pass-by-extra fam_p1_s4_r2_a42 を付けて、誤りで止まること
   - --dry-run（夜の名前 20261030）で60回が並び、-ssPassBy on が渡ること
6. check-in（個別に指定。git add . と git clean は使わない）

## 変えないもの
- 店の回し方・人とロボットの動き・台帳の列と今までの行・night/ の記録
- 本番の夜は回さない

## 報告
claude_code_reports に 20261027_HHMM_baseline_v4.txt として書く（場所：~/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/unity/claude_code_reports/）。cs 番号・build_id と保留の有無・5 の確かめの結果を書く。
```

### 依頼箱の結果 20261027_03_すれ違い入を標準に_20261027_0952.md

````markdown
# 結果：20261027_03_すれ違い入を標準に

- 状態：完了
- 始めた時刻：2026-10-27 09:52
- 終わった時刻：2026-10-27 09:57（5分）
- 作業フォルダ：/Users/shimazueiichi/Robotics_UX/ServiceSim
- この続きを Claude Code で話すとき：作業フォルダで `claude --resume d6b8340e-d116-41a3-9203-442063340d8a`

## Claude Code の報告

**やったこと**
- RobotSpec の passByEnabled の既定を「入」にしました。設定ファイル（.asset）の値も 0 から 1 にしています。実際に効くのはこちらの値です。
- tools/make_queue.py の --pass-by の既定を on にしました。--pass-by off で切の回も作れます。--pass-by-extra は変えていません。
- 上の2つを3ファイル個別に指定して check-in しました（**cs:26**、保留 0）。
- Unity は閉じていたので、コマンドから実行ファイルを作り直しました。
- 夜の名前 20261029・4本同時で --dry-run を行いました。
- judge_night.py の基準は v3 のまま触っていません。本番の夜は回しておらず、台帳にも書いていません。

**結果**
- 実行ファイル：build_id **cs26_20261027_0956**、保留 なし、作ったあとも保留 0 です。
- --dry-run は終了コード 0 で、**60回**（切 0回・入 60回）が種ごとに並びました。種0 の全6条件 → 種1 → … の順です。
  - 最初：1 回め fam_p1_s4_r0_a42 種0 すれ違い入 → 20261029_night_cs26_fam_p1_r0_s0_passon
  - 最後：60 回め fam_p5_s4_r2_a42 種9 すれ違い入 → 20261029_night_cs26_fam_p5_r2_s9_passon
- 出力フォルダ名は `20261029_night_cs26_{店}_r{台数}_s{種}_passon` の形です。入が標準になっても **_passon は付きます**。切の回は前と同じで末尾が付きません。
- から回しで KIOXIA の night/20261029/ にログが1つできました。本番のログと混ざらないよう runner_log_dryrun_20261027.txt に改名しました（消していません）。出力フォルダと台帳の行は 0 で、台帳は 265 行のままです。
- 本番のコマンドは次のとおりです（台帳を退避して、Unity を閉じてから）。
  ```
  cd /Users/shimazueiichi/Robotics_UX/ServiceSim
  python3 tools/make_queue.py --out queue.json
  python3 tools/night_runner.py --queue queue.json --parallel 4 --night 20261029
  ```

**変えたファイル**
- Assets/ServiceSim/Robot/RobotSpec.cs
- Assets/ServiceSim/Robot/RobotSpec.asset
- tools/make_queue.py
- 報告：claude_code_reports/20261027_0958_passby_default_on.txt

**決めてほしいこと**
1. **v4 に替えるときは、除く決まりも一緒に替える必要があります。** v4 の60回は全部 _passon が付きます。いまの「_passon を除く」のまま基準を 20261029_night_cs26_* にすると、基準が 0 回になります。
2. **夜の最後に自動で行う判定は、基準 v3（切）と比べます。** 入の60回を切の基準と比べるので、印が付くことがあります。v4 は判定の印ではなく、数字そのもので確かめてください。
3. **--pass-by-extra は変えていないので、既定の入と一緒に使うと名前がぶつかります。** 同じ回が2回並び、出力フォルダ名が重なります。--pass-by off のときだけ使うか、直すかを決めてください。
4. **実行ファイルを手で起こすときの既定は、まだ切です。** -ssPassBy を付けないと切で回ります（HeadlessRun.cs）。夜の実行では毎回この指定を渡すので影響はありません。既定を入に揃えるかを決めてください（依頼の範囲外なので触っていません）。
````

### 依頼箱の結果 20261027_04_v4の夜を回す_20261027_1018.md

```markdown
# 結果：20261027_04_v4の夜を回す

- 状態：完了
- 始めた時刻：2026-10-27 10:18
- 終わった時刻：2026-10-27 11:39（81分）
- 作業フォルダ：/Users/shimazueiichi/Robotics_UX/ServiceSim
- この続きを Claude Code で話すとき：作業フォルダで `claude --resume 3ca18741-89aa-4270-8a67-bbe1d36b8843`

## Claude Code の報告

**やったこと**
- night_runner が終わったことはすでに確かめ、報告も書き終えていたので、今回はあとで自動で起きる予定を止めただけです。

**結果**
- 夜 20261029 は 60 回とも completed で、失敗・止まった回・終了コード 3 は 0 回でした。night_runner の終了コードは 0 です。
- 実時間は平均 3.9 分、最長 4.8 分でした。
- 台帳は 265 行から 325 行になりました。先頭 265 行の md5 は退避と同じでした。
- 全員が帰らなかった回はありません。
- judge.md はできています。

**変えたファイル**
- 今回はなし。夜の実行で変わったファイルと報告ファイル（claude_code_reports/20261029_night_v4.txt）は、前の報告に書いたとおりです。

**決めてほしいこと**
- v4 にするかどうか。judge.md を見て、エイイチと Claude で決めてください。
```

### 依頼箱の結果 20261027_05_基準をv4に_20261027_1139.md

```markdown
# 結果：20261027_05_基準をv4に

- 状態：完了
- 始めた時刻：2026-10-27 11:39
- 終わった時刻：2026-10-27 11:42（3分）
- 作業フォルダ：/Users/shimazueiichi/Robotics_UX/ServiceSim
- この続きを Claude Code で話すとき：作業フォルダで `claude --resume d996f97a-09e9-4ade-ba46-f4f492dbd4d7`

## Claude Code の報告

**やったこと**
- judge_night.py：基準データの既定を v4（20261029_night_cs26_*）に替え、既定の除外（*_passon*）をやめました。--baseline-exclude を渡したときだけ除く形は残しています。
- make_queue.py：すれ違いが入のとき（既定）に --pass-by-extra を渡すと、理由を出して止まるようにしました。
- HeadlessRun.cs：-ssPassBy を渡さないときの既定を「入」にしました。
- 3つのファイルを個別に指定して check-in し、cs:27 になりました。そのあと実行ファイルを作り直しました。
- 実行ファイルの番号（build_id）は cs27_20261027_1141 で、保留は 0 です（作り直したあとも 0 のまま）。
- 本番の夜は回していません。台帳にも書いていません。

**結果**
- **v4 の60回への判定**：基準 60回、振り分けた回 60回。正常 60・回の失敗 0・基準なし 0 でした。何も外さずに基準が組めています。台帳は前後とも 325 行で、中身も変わっていません。
- **--pass-by-extra fam_p1_s4_r2_a42**：誤りとして止まりました（終了コード 1）。並べた回のファイルは作られていません。--pass-by off と一緒に使う場合は、今までどおり 70回（入 10回）が並びます。
- **から回し（夜の名前 20261030・4本同時）**：60回が並び、60本すべてに -ssPassBy on が渡りました（off は 0本）。出力フォルダ名は 20261030_night_cs27_…_passon の形です。
- から回しで night/20261030/ にできた runner_log.txt は、本番の記録と混ざらないよう runner_log_dryrun_20261027.txt に名前を替えました。消してはいません。

**変えたファイル**
- tools/judge_night.py
- tools/make_queue.py
- Assets/ServiceSim/Experiment/HeadlessRun.cs
- Builds/headless/（作り直した実行ファイルと build_info.json）
- 報告：claude_code_reports/20261027_1141_baseline_v4.txt

**決めてほしいこと**
なし

（範囲外なので触っていませんが、ひとつ気づいたことがあります。judge_night.py の印の文に「基準 v2 でも同じ（既知）」という古い名前が残っています。直すかどうかは、必要なら依頼してください。）
```


## Claude Code の報告

### 20261026_1556_busycheck.txt

```text
====================================================================
busycheck：同時に何本走らせるかで数字が動くか
③1台・種1（fam_p1_s4_r1_a42・種1）／Unity エディタを閉じて
2026-10-26 15:56
====================================================================

結論

  ・**動きます。ただし動くのは4つの列だけです。**
      動けなくなった      中央 1 → **7**（1本ずつ → 4本同時）
      助けた              中央 1 → **7**（動けなくなったと同じ数）
      ロボット分担        中央 0.699 → **0.660**
      店員が遅れて運んだ  中央 62 → **70**
    どれも n=5 対 8 の順位の検定（両側 5%）で、はっきり違います。
  ・**夜で主に見る数字は動きません。**
      入店待ち・店員待ち・料理待ち・会計待ち・卓が空くまで・回転・
      店員の忙しさ・ロボットの忙しさ・往復・店の中の時間
    ぜんぶ「違いは言えない」。入店待ちは中央 2.29 → 1.79 で **逆向き**です。
  ・★ そして大事な発見：**エディタ（cs:7）の回は「4本同時」の側に入ります。**
    6つの列のうち5つで、エディタの値は 4本同時の幅の中・1本ずつの幅の外でした。
    → 基準データ（cs:7・エディタで測った18回）と並べるなら、
      **夜も N = 4 で回すのが合っています**。
  ・なので **N = 4 を勧めます**（速さでも最良・基準データとも揃う）。
  ・台帳は健全。足した 29 行すべて 59 列・completed・重複なし。
    退避版の86行はそのまま。


====================================================================
1. 回し方
====================================================================

  ・Unity エディタは **閉じて**回しました（エイイチの指示どおり）。
    閉じた効果はすぐ出ました。1本の実時間が **151 秒 → 115 秒**になり、
    5回のばらつきも 113.6〜119.0 秒とほぼ揃いました
    （開いていたときは 115〜199 秒）。
  ・busy1 … 1本ずつ 5 回（-ssTry 1〜5 で名前が重ならないように）
  ・busy4 … 4本同時 × 2 束 = 8 回
    1束が4本なので「5回」ぴったりにはなりません。2束にすれば
    8回すべてが「4本同時のときの1回」になり、標本も増えます。
    かかる時間は5回でも同じ（2束ぶん）です。
  ・実行ファイルは cs:17（保留 0・ARM64）。終了コードは 13 回すべて 0。
  ・資源：1本 CPU 約145%・RSS 約100 MB ／ 4本同時 CPU 約572%・RSS 約490 MB
    （8コア・16 GB。エディタを閉じたので、開いていたときの 458% より
      使えるようになっています）


====================================================================
2. 回ごとの数字（エイイチの指定の3つ）
====================================================================

  busy1（1本ずつ・5回）

    回   動けなくなった   人のあきらめ   入店待ち（分）   店の中（分）   実時間
    ──────────────────────────────────────────────────────────────────
    1         5             38          2.3297        182.56      119.0 秒
    2         3             31          2.2925        183.09      114.4 秒
    3         1             23          1.5839        183.11      114.0 秒
    4         1             35          2.3109        180.28      113.6 秒
    5         1             34          2.1926        181.75      115.5 秒

    中央     1             34          2.292
    幅      1〜5          23〜38       1.584〜2.330

  busy4（4本同時・8回）

    回   動けなくなった   人のあきらめ   入店待ち（分）   店の中（分）   実時間
    ──────────────────────────────────────────────────────────────────
    1         3             36          1.6533        181.22      217.4 秒
    2         6             40          2.0028        184.88      219.5 秒
    3         8             51          1.6424        181.15      217.4 秒
    4         5             33          1.9285        181.42      217.3 秒
    5         9             35          2.8235        182.40      213.9 秒
    6         2             32          1.5746        183.89      214.5 秒
    7        11             51          3.0459        181.44      212.3 秒
    8         8             48          1.6230        184.72      215.3 秒

    中央     7             38          1.791
    幅      2〜11         32〜51       1.575〜3.046

  13 回すべて status = completed・everyone_left = 1。
  店の中の時間は 180.3〜184.9 分で、忙しさによらずほぼ同じです。


====================================================================
3. 判定
====================================================================

  使った検定：順位の検定（Mann–Whitney）。n=5 対 8 の両側 5% は U ≤ 6。

  ① 動けなくなった（stuck_count）… **はっきり違う**

      1本ずつ  中央 1（1〜5）
      4本同時  中央 7（2〜11）
      U = 4（≤ 6）。40 組のうち 36 組（90%）で 4本同時の方が大きい。
      幅は 2〜5 で重なりますが、中央は **7 倍**です。

  ② 人のあきらめ（gave_up）… **違うとは言えない**

      1本ずつ  中央 34（23〜38）
      4本同時  中央 38（32〜51）
      U = 8.5（> 6）。増える向きには見えますが、言い切れません。

  ③ 入店待ち（entry_wait_mean）… **違わない**

      1本ずつ  中央 2.292（1.584〜2.330）
      4本同時  中央 1.791（1.575〜3.046）
      U = 15（> 6）。しかも中央は **減って**います（逆向き）。

  ④ ほかの列をぜんぶ当たった結果（はっきり違ったのは4列だけ）

      列                    1本ずつ            4本同時            向き
      ─────────────────────────────────────────────────────────────
      stuck_count           1（1〜5）          7（2〜11）         増える
      rescues               1（1〜5）          7（2〜11）         増える
      robot_share           0.699              0.660              減る
      late_staff_dishes     62（59〜67）       70（63〜75）       増える

      ※ rescues と stuck_count が同じ数なのは、動けなくなった回が
        ぜんぶ助けられているという意味です（二重に数えてはいません）。

      動かなかった主な列（U と判定）
      ─────────────────────────────────────────────────────────────
      entry_wait_mean    U=15  違いは言えない
      staff_wait_mean    U=16  違いは言えない
      dish_wait_mean     U=8   違いは言えない
      staff_util         U=13  違いは言えない
      robot_util         U=11  違いは言えない
      pay_wait_mean      U=19  違いは言えない
      blocked_wait_mean  U=16  違いは言えない
      turn_mean          U=17  違いは言えない
      total_minutes      U=19  違いは言えない
      trips              U=11  違いは言えない

  筋も通ります

    忙しい → ロボットが止まる回数が増える（stuck_count ↑・rescues ↑）
          → ロボットが運べる割合が下がる（robot_share ↓）
          → 足りない分を店員が遅れて運ぶ（late_staff_dishes ↑）
          → **客の待ちは店員が吸収するので変わらない**（entry_wait ほぼ同じ）

    10/25 の切り分けで最後まで消えなかった容疑（NavMeshAgent の動きが
    Unity の job で並列に動く）と向きが合っています。
    止まる・助ける所にだけ出て、店の流れ全体には出ない、という形です。


====================================================================
4. ★ エディタ（cs:7）はどちらの忙しさに近いか
====================================================================

  同じ条件・同じ種1 で、基準データ（cs:7・エディタで測った回）を並べました。

    列                  エディタ   1本ずつ(5回)      4本同時(8回)      どちらに入るか
    ──────────────────────────────────────────────────────────────────────
    動けなくなった          9       1〜5            2〜11           **4本同時だけ**
    助けた                 9       1〜5            2〜11           **4本同時だけ**
    ロボット分担         0.6456    0.6748〜0.7136  0.6359〜0.6942  **4本同時だけ**
    店員が遅れて運んだ      73      59〜67          63〜75          **4本同時だけ**
    入店待ち            3.004     1.584〜2.330    1.575〜3.046    **4本同時だけ**
    人のあきらめ           63      23〜38          32〜51          どちらの外（上）

  6つのうち5つで、エディタの値は **4本同時の幅の中・1本ずつの幅の外**でした。
  つまり **エディタで回すのは「4本同時くらい忙しい」状態**ということです。
  （エディタ自身が重いので、筋は通ります。）

  人のあきらめだけは、4本同時の上（51）よりさらに上（63）でした。
  この列は忙しさでは「はっきり違う」とは出なかった列なので、
  同じ種でも揺れる幅が大きいのだと思います。断定はしません
  （エディタ側は1回しかないので）。

  ここから出る結論

    判定（judge_night）の基準データは **cs:7・エディタで測った18回**です。
    夜を 1本ずつ（N=1）で回すと、止まる回数が減り・ロボット分担が上がるので、
    判定が「基準と違う」と印を付けます。でもそれは
    **店が良くなったのではなく、Mac が空いていただけ**です。

    → 基準データと並べるなら、夜も **N = 4** が合っています。
      速さでも最良（49.5 回/時）なので、2つの決め方が同じ答えになります。


====================================================================
5. 夜の進め方（勧め）
====================================================================

  ① **N = 4** で回す
     ・依頼の決め方（1時間に終わる回がいちばん多く、メモリが足りている）→ 4
     ・基準データ（cs:7・エディタ）と忙しさが揃う → 4
     ・メモリは 490 MB（16 GB のうち 3%）。CPU は 5.7 コア（8 のうち）
     ・1回 215 秒で、40 分の上限に遠く届かない
     ・70回の見込み **約 63 分**（エディタを閉じた実測から）

  ② **夜の途中で N を変えない**
     run_info.json の parallel に残るので、あとから確かめられます。

  ③ **Unity エディタは閉じて回す**
     閉じるだけで1回が 151 秒 → 115 秒（1本ずつ）になります。

  ④ 判定を読むときの注意（報告書に残します）
     ・stuck_count・rescues・robot_share・late_staff_dishes の4列は
       **忙しさで動く列**です。夜どうしの比べ（③対⑦、ロボット 0/1/2 台、
       すれ違い 入対切）は同じ N なので問題ありません。
     ・基準データと並べるときも N=4 なら揃いますが、
       「ぴったり同じ忙しさ」ではないので、この4列の小さな差は
       店の違いだと決めつけないでください。
     ・入店待ち・店員待ち・料理待ち・会計待ち・回転は忙しさに動かないので、
       そのまま信じてよい列です。


====================================================================
6. 台帳
====================================================================

  行数              87（退避版）→ **116**（足した 29 行）
  退避版の86行      **そのまま**
  どの行も          **59 列**
  status            completed × 29
  run_folder        重複なし
  鍵                残っていない
  名札ごと          acheck 2 / bcheck 1 / c1check 3 / c2check 3 /
                    c3check 3 / c4check 4 / **busy1 5 / busy4 8**

  退避
    archive/unity_ledger_v1_preacheck_20261026/unity_ledger_v1.csv
    87 行・md5 4d8486b9f75e3bef8f130bad4ba3e6b5

  確かめの 29 行は名札で分かれるので、夜の判定（{夜}_night_*）には入りません。


====================================================================
7. 次
====================================================================

  1. 5 章 D：codex レビューの依頼文を作る
     見てもらう所 … 台帳の鍵（ServiceSimLedger.TakeLock / Append）・
                    LedgerRow・night_runner.py の stopped / failed の行
  2. 実行係を通した から回し（--dry-run）で 70 回の並びを確かめる
  3. 最初の夜 70 回（台帳を archive/unity_ledger_v1_prenight_{夜}/ に写してから）

     cd /Users/shimazueiichi/Robotics_UX/ServiceSim
     python3 tools/make_queue.py --out queue.json \
         --pass-by-extra fam_p1_s4_r2_a42
     python3 tools/night_runner.py --queue queue.json --parallel 4 \
         --night 20261027
```

### 20261026_1744_v14_ledger_fixes.txt

```text
====================================================================
依頼 v14：codex レビューの指摘のうち、最初の夜の前に直す8つ
2026-10-26 17:44　check-in：ServiceSim/cs:21（保留 0）
====================================================================

結論

  ・8つぜんぶ直しました。実行ファイルも cs:21・保留 0 で作り直しました
    （build_id cs21_20261026_1739・ARM64）。
  ・確かめは **本物の台帳を使わず、コピー**で行いました。5つの場面すべてで
    「書かずに断る」「台帳が 1 byte も変わらない」「終了コードとログに出る」。
  ・そのあと本物で ③1台 を **4本同時で1束**（名札 fixcheck）。
    台帳に **4行**・runs.csv に **4行**・queue.json の写しも出ました。
    前の 117 行は 1 byte も変わらず、鍵も残っていません。
  ・★ 「あとで直すもの」として挙がっていた7件のうち **6件は cs:20 で
    すでに直っています**（7 章）。今夜は何もしなくて大丈夫です。
  ・★ v14 の確かめの文面を1か所だけ変えました（6 章）。
    「③1台・種1を4本同時」は、同じ条件・同じ種を4本並べると
    出力フォルダ名がぶつかって実行ファイルが断るので、**種1〜4** にしました。


====================================================================
1. 直した8つ
====================================================================

  1 A2 古い鍵：**年だけ見て壊す処理をやめた**（C# と Python の両方）

      やめた理由は依頼のとおりです。2本が同時に「古い」と判定すると、
      先に取り直した側の **新しい鍵** をもう一方が消して、
      2本が同時に持つ道がありました。

      いまの形
        ・鍵が取れなければ、上限（60 秒・**単調時計**で測る）まで待つ
        ・上限を過ぎたら「台帳に書けなかった」として失敗（下の 3）
        ・**残った鍵は自動で消さない**。人が中身を見て消す
          中身は「札 pid ホスト名 unix秒」の1行なので、
          どのプロセスが残したかが分かります
        ・解放は、鍵の中身が **自分の札** のときだけ消す
        ・取れた直後にも読み直して、自分の札か確かめる

      C#  ServiceSimLedger.TakeLock / LedgerKey.Dispose
      Py  tools/ledger.py の Lock（LOCK_STALE_SEC と _stale は消した）

  2 A4 半端な行：鍵の中で末尾を確かめ、改行でなければ追記しない

      ・自動で切り詰めません（台帳は書き換えない決まり）
      ・1行は UTF-8 の byte にしてから **1回で** 書く（C# も Python も）
      ・Python は前は改行を1つ継ぎ足していましたが、やめて
        C# と同じ「断る」にそろえました

  3 B3 書けなかったときの扱い

      ・Python は **59列の名前と順番を全部照合**します。
        COLUMNS を C# の ServiceSimLedger.Columns と同じ並びで置きました
        （列を足すときは両方を直す。data_manifest_rules.md 10節）
      ・C# の Append が失敗したら（**重複の正しい拒否は除く**）
        実行ファイルの **終了コードを 3** にします。
        LedgerRow.AppendState（Appended / AlreadyThere / Failed）で分けています
      ・実行係は終了コード 3 を status = ledger_failed として
        runner_log と最後のまとめに出します。
        そのとき **実行係からは台帳に書きません**
        （実行ファイルが書けなかった所に実行係が書いても、
          たぶん同じ理由で書けないため。人が見て決める）

  4 D7 Python が書く stopped / failed の行にも ledger_schema_version = 2

      値は tools/ledger.py の SCHEMA_VERSION に1つだけ置きました。

  5 A7 build_info.json は **実行ファイルの .app と同じ所だけ**

      …/headless/ServiceSim.app/Contents/MacOS/ServiceSim から
      .app の1つ上（…/headless/build_info.json）だけを見ます。
      **無ければ始めません**（cm status には頼りません）。

      前の形の何が危なかったか
        上に6階層まで探したうえ、見つからなければ cm status に落ちていました。
        cm status は「いまの作業ツリーの版」なので、
        **実行ファイルの版とは別のもの**です。それを台帳の unity_cs に
        書くと、どの版で回したのか分からなくなります。

  6 A8 保留がある build_info では始めない

      pending_changes が空でなければ実行係が止まります（終了コード 1）。
      前に付けていた --allow-dirty-build は **廃止**しました
      （隠れた抜け道を残さないため）。

  7 D6・C3 マウント

      Python … os.path.ismount('/Volumes/KIOXIA') で確かめます。
               確かめる場所は3つ：
                 ・夜のフォルダを作る前／ログを開く前（main の頭）
                 ・各回を起動する直前（4本立ち上げる途中に外れても始めない）
                 ・runs.csv を開く前／queue.json を写す前
      C#   … ServiceSimLedger.IsOnMountedVolume を足しました。
               DriveInfo.GetDrives()（＝いまマウントされている所の一覧）に
               その道の頭と一致する所があるかを見ます。
               「/」しか一致しなければ Mac の中なので断ります。
               一覧が取れない実行環境では、台帳の実体があることを
               代わりの印にして、そのことをログに出します（黙って通さない）。
               HeadlessRun の回し始めの確かめと、Append の中の両方で見ます。

      なぜ Directory.Exists では足りないか
        KIOXIA を抜いたあと /Volumes/KIOXIA というフォルダが
        Mac の中に残ることがあります。Directory.Exists はそれを通すので、
        外付けに書いたつもりで Mac の中に書いてしまいます。

  8 B2 runs.csv は csv.writer（cs:20 で入れた分）

  変えていないもの（依頼のとおり）
    店の回し方・ロボットと人の動き・記録の中身／
    台帳の列（59列・schema 2）・今までの行／
    夜の並び（種ごと）・runs.csv・queue.json の写し


====================================================================
2. 確かめ①〜⑤（本物の台帳は使わず、コピーで）
====================================================================

  コピーの場所
    scratchpad/v14/unity/unity_ledger_v1.csv（117 行）
    md5 b2fd0118f1346b460b5350e5f815a49e（本物と一致を確認してから使った）
  ★ 本物の台帳と夜のフォルダには触っていません。

  ① 鍵が残っている

      1時間以上古い鍵（別のホスト・別の pid）を置いて追記を試した。
        ・書かずに断った（3 秒の上限で試験。実運用は 60 秒）
          「台帳の鍵を 3 秒待っても取れません」
        ・**古い鍵を自動では消していない**（v14 の 1 のとおり）
        ・台帳は 1 byte も変わらない
      C# 側でも同じ形で確かめた（上限 60 秒 → 60.2 秒待って諦め、鍵は残る）

  ② 台帳の最後が改行で終わっていない

      末尾に半端な行（改行なし）を足してから追記を試した。
        ・書かずに断った
          「台帳の最後が改行で終わっていません（前の追記が途中で切れた疑い）」
        ・**切り詰めていない**（台帳は 1 byte も変わらない）
        ・鍵も残らない

  ③ ヘッダが1列違う

      31 列めと 32 列めを入れ替えたコピーで追記を試した。
        ・書かずに断った。**どこが違うかを出す**
            「台帳のヘッダがコードの列と違います（2 か所）
               31 列め: 実体 site_dish_wait_mean / コード site_staff_wait_mean
               32 列め: 実体 site_staff_wait_mean / コード site_dish_wait_mean」
        ・台帳は 1 byte も変わらない
      ★ 前の形（頭6列＋末尾10列だけ照合）では **これを通していました**。
        途中の列の入れ替えを見つけられるようになりました。

  ④ 保留ありの build_info

      pending_changes を入れた build_info.json を置いて実行係を起動した。
        ・始めなかった（終了コード **1**）
            「NG: 実行ファイルは保留中の変更があるまま作られています: 保留あり 3 件…
               台帳の unity_cs（ServiceSim/cs:20）と中身が合いません」
        ・台帳は 1 byte も変わらない

  ⑤ KIOXIA がない（存在しない道を渡す）

      --volume /Volumes/NOT_THERE で実行係を起動した。
        ・何も作らずに終わった（終了コード **1**）
            「KIOXIA がつながっていません（/Volumes/NOT_THERE / …）。
               何も作らずに終わります（Mac の中には書きません）」
        ・**/Volumes/NOT_THERE が作られていない**（夜のフォルダもログも作らない）

  ＋ build_info.json が無いとき

      .app のそばに build_info.json が無い実行ファイルで起動した。
        ・始めなかった
            「NG: build_info.json がありません（実行ファイルの .app と同じ所）」
        ・cm status に落ちない（版を取り違えない）
        ・台帳は 1 byte も変わらない

  ＋ C# 側 10 件（mono で直接）

      マウント判定 … KIOXIA の上 → OK ／ /Users の下 → 断る ／
                     /tmp → 断る ／ /Volumes/NOT_THERE → 断る
      Mac の中のコピーには足さない（台帳 1 byte も不変・鍵も残らない）
      古い鍵でも自動で壊さず、60.2 秒待って諦める／鍵は残る


====================================================================
3. 本物で4本同時（名札 fixcheck）
====================================================================

  回したもの　③1台（fam_p1_s4_r1_a42）・種1〜4 を4本同時で1束
  実行ファイル　cs21_20261026_1739（保留 0・ARM64）

  実行係のログ
    始めました（1本目・同時 1）… 種1（pid 2538）
    始めました（2本目・同時 2）… 種2（pid 2547）
    始めました（3本目・同時 3）… 種3（pid 2552）
    始めました（4本目・同時 4）… 種4（pid 2556）
    おわり 種1 completed 終了コード 0 実時間 3.5 分
    おわり 種3 completed 終了コード 0 実時間 3.5 分
    おわり 種2 completed 終了コード 0 実時間 4.3 分
    おわり 種4 completed 終了コード 0 実時間 4.3 分
    まとめ completed: 4 回 / 平均 3.9 分・最長 4.3 分
    判定を呼びます → judge.md を書きました
    終了コード 0

  台帳
    117 → **121 行**（ちょうど4行）
    4行すべて status completed・unity_cs ServiceSim/cs:21・**59 列**
    退避版（archive/unity_ledger_v1_preacheck_20261026）の 87 行は **そのまま**
    鍵は残っていない

  night/20261026/ に出たもの
    runner_log.txt … 人が読む記録
    runs.csv       … **4行・16 列**（単調時計の列も入っている）
       folder                                起動時/決め  開始 → 終了          実時間
       …_fixcheck_cs21_fam_p1_r1_s1           1/4        17:39:37 → 17:43:07   3.5 分
       …_fixcheck_cs21_fam_p1_r1_s3           3/4        17:39:47 → 17:43:17   3.5 分
       …_fixcheck_cs21_fam_p1_r1_s2           2/4        17:39:42 → 17:44:02   4.3 分
       …_fixcheck_cs21_fam_p1_r1_s4           4/4        17:39:52 → 17:44:07   4.3 分
       （行の順は **終わった順**。鍵が順に渡っている証拠）
    queue.json     … 並べた回の写し（元と中身が同じことを確認）
    logs/          … 1回ごとの Unity のログ
    judge.csv / judge.md … 判定

  run_info.json（種1）
    runner headless / build_id cs21_20261026_1739 / night 20261026 /
    parallel 4 / **parallel_actual 1** / unity_cs ServiceSim/cs:21

  ★ parallel_actual は「起動したときの本数」なので 1・2・3・4 と並びます。
    「回っている間に何本だったか」は runs.csv の開始・終了の時刻から出します。


====================================================================
4. 気づいたこと（直していない。判断が要るもの）
====================================================================

  ・1回が **3.5〜4.3 分**でした。busycheck の 4本同時（3.6 分）とほぼ同じで、
    5章C の見込み（70回に約63分）どおりです。
  ・終了コード 3（台帳に書けなかった）は、**実物では出せていません**
    （本物の台帳で意図的に失敗させると行が残らない道を作ることになるため）。
    コピーの台帳で「断る」ことは確かめましたが、
    「断ったときに実行ファイルが 3 を返す」ことは実物では未確認です。
    確かめるなら、コピーの台帳を指した ExperimentPlan で1回だけ回す形になり、
    .asset を一時的に書き換えることになります。やりますか。


====================================================================
5. あとで直すもの（今夜は直さない）
====================================================================

  ★ 依頼では7件挙がっていましたが、**6件は cs:20 ですでに直っています**。
    今夜のために何かする必要はありません。下は現状です。

  A6・C2 同じ夜を回し直したときの _try2 の扱い      → **cs:20 で直した**
      再開のとき初回と _try2 の両方を見る。_try2 の行がもうあれば
      やり直しを使い切ったものとして扱う（L.all_folders）
  A9 Ctrl+C・SIGTERM で止めたときの runs.csv        → **cs:20 で直した**
      走っている回を interrupted として残す（SIGINT・SIGTERM・SIGHUP）。
      ★ SIGKILL と電源断は救えない。そのときは runner_log の
        「始めました」の行に開始時刻が残るので、そこから追える
  A10 開始・終了の時刻の正確さ                      → **cs:20 で直した**
      終わった回の時刻を先に全部採ってから書く（前は1本目の鍵待ちが
      2本目の終了時刻に混ざった）。runs.csv に started_mono / ended_mono。
      ★ 残るずれは見回りの間隔（--poll 5 秒）ぶん。
        1回 210 秒に対して最大 5 秒（2%）。重なりの判定には十分
  C4 ログが書けないときの例外                       → **cs:20 で直した**
      note_lost が標準エラーにも出し、欠けた数をまとめに出す
  A5 止めた回の wait()・照合エラー時の扱い          → **cs:20 で直した**
      _kill は SIGKILL のあと wait（30 秒）。終わらなければ
      「台帳に二重に書く恐れ」と出す。
      HasRow が読めないときは「分からない」を返して書かない
  B5 queue.json の写しの書き方                      → **cs:20 で直した**
      一時ファイルに写してから os.replace で置く
  A7 の残り（--cs-token ss{N}・pass_by の大文字小文字）→ **cs:20 で直した**
      --cs-token は廃止（cs{N} 固定）。pass_by は norm_pass_by で
      on/off にそろえる

  まだ残っているもの（今夜の前提では踏まない）
  ・A2 の恒久策：両言語を同じ OS ロック（flock）に統一し、鍵ファイルを
    消さない形にする。いまは O_EXCL ＋ 札の確認で、
    **残った鍵は人が消す**形です。今夜は実行係を1つだけ起動するので、
    鍵を取り合うのは4本の実行ファイルだけ（どれも 100 ミリ秒ほどで返す）。
    C# から flock を呼ぶ（P/Invoke）必要があるので、別件にしたい
  ・A4 の恒久策：ファイルシステムの原子性に頼らない書き方
    （別ファイルに書いて rename で置く、など）。
    いまは「末尾が改行でなければ書かない」で、壊れた台帳に
    さらに足すことは防げています


====================================================================
6. 依頼の文面から変えた1か所
====================================================================

  v14 の確かめ：「本物で、③1台・種1を4本同時で1束」

  同じ条件・同じ種を4本並べると、出力フォルダ名が
  20261026_fixcheck_cs21_fam_p1_r1_s1 の1つになり、
  2本目から先は実行ファイルが「その出力フォルダはもうあります」と断ります
  （上書きしない決まり）。実行係のやり直し（-ssTry）も2までです。

  4本同時で台帳と runs.csv に4行入ることを確かめるのが目的なので、
  **③1台・種1〜4** の4本にしました。
  同じ種を4回回したい場合は、名札を4つに分ける（fixcheck1〜4）形になります。
  必要なら言ってください。


====================================================================
7. 次
====================================================================

  ・codex に再レビューを出すなら、依頼文は
    unity/codex_review_request_v12_ledger.md を cs:21 向けに直して出します
    （版・直した所・残した所を書き換えます）。
  ・そのまま夜に進むなら、台帳を写してから1行で始められます。

      cd /Users/shimazueiichi/Robotics_UX/ServiceSim
      cp -p /Volumes/KIOXIA/r2r2r-data/service_sim/unity/unity_ledger_v1.csv \
        /Volumes/KIOXIA/r2r2r-data/service_sim/unity/archive/unity_ledger_v1_prenight_20261027/
      python3 tools/make_queue.py --out queue.json \
          --pass-by-extra fam_p1_s4_r2_a42
      python3 tools/night_runner.py --queue queue.json --parallel 4 \
          --night 20261027

    ※ 夜の名前を 20261027 にしています。20261026 は fixcheck で
      night/20261026/ を使ったので、同じ夜の名前にすると
      runs.csv と queue.json の写しが混ざります。
      20261026 で回したいときは、先に night/20261026/ を
      別の名前に寄せてください（私がやってもよいです）。
    ※ Unity エディタは閉じたまま回してください（1回が 151→115 秒になります）。

  check-in：**cs:21**。保留 0。実行ファイルは cs21_20261026_1739。
```

### 20261027_2327_night1_70runs.txt

```text
====================================================================
最初の夜：70回・4本同時（night 20261027）の結果
2026-10-26 22:11 〜 23:25（1時間15分）／ServiceSim/cs:21
====================================================================

結論

  ・**70回ぜんぶ completed・終了コード 0**。止まった回・落ちた回・
    台帳に書けなかった回（終了コード 3）は **0 件**。
  ・台帳は 121 → 191 行（ちょうど70行）。**前の121行は 1 byte も変わらず**、
    70行すべて 59 列・ServiceSim/cs:21・run_folder の重複なし。鍵も残らず。
  ・runs.csv 70行・queue.json の写しも出ました。
  ・★ ただし **1回は数字として使えません**（③2台・すれ違い入・種0）。
    店が空にならず、中の時間の上限 720 分まで回りました。
    **台帳の status は completed** です（回し方としては終わったため）。
    判定が「全員が帰っていない」で拾っています。→ 4 章で相談。
  ・基準データ v3 にするのは **勧めます**（60回・10種）。ただし
    すれ違い「入」の10回は別扱いにしてください。→ 5 章。


====================================================================
1. 回し方と、始める前の確かめ
====================================================================

  実行ファイル  cs21_20261026_1739（ServiceSim/cs:21・保留 ''・ARM64）
  並び          種ごと（種0 の全7条件 → 種1 の全7条件 → …）
  同時本数      4（決めた N）
  Unity         閉じて回した
  KIOXIA        os.path.ismount True を確認してから開始

  台帳の退避（v12 5章D）
    archive/unity_ledger_v1_prenight_20261027/unity_ledger_v1.csv
    121 行・md5 bacc1e04ffe854c29d94da20343c59c6・cmp 一致（コピーのみ）

  1つ寄せたもの
    night/20261027/ に 10/26 の --dry-run のログが残っていたので、
    夜の記録と混ざらないよう runner_log_dryrun_20261026.txt に
    **改名して退けました**（消していません）。
    出力フォルダも台帳の行も 0 件だったので、夜の名前はそのままにしました。


====================================================================
2. 実時間と同時本数
====================================================================

  70回の実時間   平均 4.2 分 / 最短 3.3 分 / 最長 14.4 分
  全体           1時間15分（22:11 〜 23:25）
                 ※ 見込みは約63分。使えなかった1回が 14.4 分かかった分が主

  runs.csv の「起動したときの同時本数」
    1 本 … 1 回（立ち上がりの1本目）
    2 本 … 1 回
    3 本 … 2 回
    4 本 … **66 回**
  → 立ち上がりの3回を除き、ぜんぶ4本の中で回りました。
    並びを種ごとにしたので、薄かったのは種0 の最初の3条件
    （③0台・③1台・③2台）だけで、すれ違い入には当たっていません。

  ★ 「回っている間」に何本だったかは、runs.csv の開始・終了の時刻
    （started_mono / ended_mono）から正確に出せます。


====================================================================
3. 台帳の健全さ
====================================================================

  退避版 121 行 → いま 191 行（足した 70）
  退避版の行          **そのまま（1 byte も違わない）**
  足した70行          すべて **59 列**
  status              completed 70
  unity_cs            ServiceSim/cs:21 70
  run_folder の重複   なし
  すれ違い            入 10 回 / 切 60 回
  鍵                  残っていない

  v14 で入れた守りは、どれも発動しませんでした（発動する場面が起きなかった）。
  鍵の取り合いは4本の実行ファイルだけで、1回も待ちで失敗していません。


====================================================================
4. ★ 使えなかった1回（相談したいこと）
====================================================================

  20261027_night_cs21_fam_p1_r2_s0_passon
    （③ロボット2台・すれ違い「入」・種0）

    everyone_left   **False**（全員が帰っていない）
    total_minutes   **720.0001 分**（中の時間の上限まで回った）
    stopped_early   False
    実時間          14.4 分
    入店待ち        1.46 分（低い）
    動けなくなった   3 / あきらめた 29 / 卓を空けた 0

  同じ条件のほかの9種は 182〜222 分で全員帰っています。種0 だけ
  **720 分まで回って終わらなかった**ので、どこかで流れが止まったままに
  なったと見ています（入店待ちが低いので、客が溜まったわけではありません）。
  すれ違い「入」はエイイチが「標準は切のまま」と決めた側なので、
  **夜の標準の60回には影響しません**。

  ★ 相談：**台帳の status が completed になっています。**
    回し方としては最後まで回った（途中で止めていない）ので completed ですが、
    数字としては使えません。いまは判定だけが「全員が帰っていない」で拾います。

    直し方の案（どれも数の定義の変更なので、勝手には入れません）
      (ア) いまのまま。status は completed、判定で拾う
           → 台帳だけ見ると「70回成功」に見える
      (イ) everyone_left が false なら status を **unfinished** にする
           → 台帳だけで使える回が分かる。列は増えない（status の値が増える）
      (ウ) status は completed のまま、判定の「回の失敗」を
           台帳の行からも分かるようにする（列を足す）
           → 列を足すのは 10節の決まりで相談が要る

    私は **(イ)** を勧めます。「最後まで回った」と「数字が使える」は
    別のことなので、台帳の1列目近くで見分けられるほうが安全です。
    ただし過去の行の status は書き換えません（これからの回だけ）。


====================================================================
5. 判定（judge.md）と、基準データ v3 にするか
====================================================================

  判定の振り分け（基準は cs:7 の18回）

    正常                30 回
    試算を見直す候補      0 回
    Unity の作り         30 回
    回の失敗             1 回   ← 4 章のこれ
    基準なし             9 回   ← すれ違い「入」（基準に同じ条件が無い）

  「Unity の作り」の印は4つ（条件ごと・種の中央値が基準の最大を超えた）

    ③1台  30秒以上の止まり（1往復あたり） 0.203（基準 0.083〜0.182）
    ⑦1台  30秒以上の止まり（1往復あたり） 0.152（基準 0.075〜0.119）
    ⑦1台  動けなくなった（60秒）          6.5 （基準 3.0〜3.0）
    ⑦2台  瞬間移動                       69.0（基準 47.0〜59.0）

  ★ この印は **あまり強い証拠ではありません**。基準（cs:7）は
    **種が3つしかない**ので、幅が偶然狭く出ます。10種の幅と並べると：

    ③1台 動けなくなった   夜 中央 8  （幅 1〜14） ／ 基準 中央 4（幅 3〜9）
                          夜の10回のうち基準の最大を超えたのは 4 回
    ⑦1台 動けなくなった   夜 中央 6.5（幅 3〜15） ／ 基準 中央 3（幅 3〜3）
                          夜の10回のうち基準の最大を超えたのは 8 回
    ⑦2台 瞬間移動         夜 中央 69 （幅 47〜78）／ 基準 中央 57（幅 47〜59）
                          夜の10回のうち基準の最大を超えたのは 7 回

    「基準の幅 3〜3」のような所を超えたかどうかで判定しても、
    何も言えません。**10種の夜のほうが基準にふさわしい**です。

  夜（10種の中央値）と cs:7（3種の中央値）の並び

    条件        入店待ち          ロボット分担        動けなくなった      回転
    ─────────────────────────────────────────────────────────────────────
    ③0台       4.084（3.801）    0（0）            0（0）            10.74（10.74）
    ③1台       4.564（3.004）    0.675（0.701）    8（4）            5.272（6.949）
    ③2台       3.038（2.407）    0.930（0.908）    20.5（23）        5.07（6.214）
    ⑦0台       4.224（4.182）    0（0）            0（0）            11.4（11.47）
    ⑦1台       4.984（2.042）    0.691（0.709）    6.5（3）          21.5→6.813（5.599）
    ⑦2台       2.894（1.625）    0.939（0.893）    21.5（32）        4.135（5.11）
    ③2台・入   6.194（基準なし）  0.912             23                7.608
                （）内は cs:7

    ・**ロボット0台は基準とほぼ同じ**（③ 4.08 対 3.80／⑦ 4.22 対 4.18）。
      ロボットが居ない条件は再現できています。
    ・ロボット有りの入店待ちは夜のほうが大きく出ていますが、
      基準が3種しかないためと見ています（③1台 の cs:7 は
      種0〜2 が 1.40 / 3.00 / 11.99 分で、中央値 3.00 は偶然の位置）。
    ・ロボット分担はどれも基準に近い（差は 0.03 以内）。

  ★ 勧め：**基準データ v3 を、この夜の「すれ違い切」60回にする**。

      理由
        ・種が10個（基準の3個より散らばりを押さえられる）
        ・画面なし・4本同時で揃っている（これからの夜と同じ条件）
        ・cs:7 はエディタで測ったもの。busycheck で
          「エディタは4本同時くらい忙しい」と分かったので向きは近いが、
          測り方が違うものを基準に置き続ける理由がない
      扱い
        ・すれ違い「入」の10回は **基準に入れない**（標準は「切」）。
          使えない種0 を除いた9回を、別の比べ用に取っておく
        ・判定の既定（judge_night.py の BASELINE_DEFAULT）を
          20261027_night_cs21_* に替える。
          ただし glob が「入」も拾うので、切だけに絞る形にする必要がある
          → 判定側の直しが要ります（やりますか）

  「試算を見直す候補」は 0 件でした。
  記録として残っている Unity÷試算 で範囲から外れたのは2つだけです。
    ⑦1台 料理の待ち 1.24倍（基準 0.95〜1.22倍）
    ⑦2台 料理の待ち 1.40倍（基準 1.52〜1.57倍）


====================================================================
6. すれ違い 入 と 切（同じ夜・同じ条件・種の中央値）
====================================================================

  条件            入店待ち  料理の待ち  席は空いているのに待った  動けなくなった  あきらめた  ロボット分担
  ───────────────────────────────────────────────────────────────────────────
  ③2台（10回）     3.04     1.49      1.36                20.5        59.5     0.93
  ③2台・入（9回）   6.19     1.54      4.20                23.0        66.0     0.91

  10種で回しても、やはり **「入」は店全体が悪くなります**
  （入店待ち 3.04 → 6.19 分・席が空いているのに待った 1.36 → 4.20 分）。
  10/26 に1種で見たときと同じ向きで、種を10個にしても変わりませんでした。
  さらに「入」は10回のうち1回が **720 分まで終わらない**（4 章）。

  → 「標準は切のまま」というエイイチの判断は、10種でも裏づけられました。
    すれ違いの作り（cs:10 の①②③）は、入れても店は良くなりません。
    直すか・置いておくかは別途ご相談です。


====================================================================
7. 出たもの
====================================================================

  night/20261027/
    runner_log.txt                 … 夜の記録（70回ぶん）
    runs.csv                       … 70行・16列（開始・終了・単調時計つき）
    queue.json                     … 並べた回の写し
    logs/                          … 1回ごとの Unity のログ 70 本
    judge.csv / judge.md           … 判定
    runner_log_dryrun_20261026.txt … 10/26 の から回しのログ（退けたもの）

  20261027_night_cs21_*            … 70 フォルダ（1回ぶんの記録）
  unity_ledger_v1.csv              … 191 行
  archive/unity_ledger_v1_prenight_20261027/ … 夜の前の退避（121 行）


====================================================================
8. 次（相談したいこと3つ）
====================================================================

  1. 4 章：使えない回の status をどうするか（私の案は (イ) unfinished）
  2. 5 章：基準データ v3 をこの夜の「切」60回にするか。
     するなら judge_night.py の既定を替え、glob を「切」だけに絞る
  3. 6 章：すれ違いの作りを直すか、置いておくか

  そのほか、今夜のうちにできること
  ・使えなかった1回（③2台・入・種0）を、画面を出して1倍で見てみる
    （依頼 v13 の 依頼2 の SimHud が使えます）。
    720 分終わらない原因が目で見えるはずです。
  ・判定の「Unity の作り」の線を、基準の最小〜最大ではなく
    10種の分布（四分位など）で引き直す
```

### 20261027_0743_stuck_run_s0_passon.txt

```text
====================================================================
720 分終わらなかった回の調べ
20261027_night_cs21_fam_p1_r2_s0_passon（③2台・すれ違い入・種0）
2026-10-27 07:43　調べただけ。Assets は直していません
====================================================================

結論（ひとことで）

  **ロボット1 が、料理2皿を持ったまま 600 秒ごとのやり直しを 56 回くり返し、
  その2皿を待っていた2組が永久に帰れなくなりました。**

  ・持っていた料理   dish 157（卓15・**割れやすい**）と dish 153（卓16）
  ・待っていた客     組62（卓15・4人）と 組63（卓16・2人）
  ・スタッフ         4人とも手空き。仕事板は空。**一度も頼まれていない**
  ・場所             待機場所 standby1（x=16.9, z=6.05）から出て、
                     卓15 の停止点（x=5.613, z=8.662）に着けないまま時間切れ
  ・始まり           132.0 分（**最後のすれ違いが起きた分と同じ**）
  ・終わり           708.2 分まで同じことをくり返し、720 分の上限で打ち切り

  ★ **なぜロボットが着けないのか**は、出力だけでは特定できませんでした。
    記録の上ではロボットは「止まってもいない・立ち往生もしていない・
    道も引けている」と言っているのに、600 秒かけて着きません（4 章）。


====================================================================
1. 店が固まった様子（timeline.csv）
====================================================================

  171 分から **549 分間、1ミリも変わりません**。

    分      待ち組  着席  空卓  汚卓  板（案内/配膳/呼出/会計/片付）  手空  ロボ稼働
    ────────────────────────────────────────────────────────────────────
    171      0      2    40    0    0 / 0 / 0 / 0 / 0            4      1
    …（720 分まで同じ）

  ・着席している組が **2組**。待っている組は 0
  ・空いている卓 40（店はほぼ空）
  ・**仕事板は空っぽ**（案内も配膳も呼出も会計も片付けも 0 件）
  ・**スタッフ4人は全員手空き**
  ・**ロボットは1台だけ、ずっと稼働中**

  店は 118 分の 24 組から順調に減り、168 分で 2組になって、そこで止まりました。
  混んで詰まったのではありません（入店待ちは 1.46 分と低い）。


====================================================================
2. 帰れなかった2組（groups.csv）
====================================================================

  組 62　卓15・4人
    来店 113.4 分 → 着席 114.6 分 → 食べ始め 128.0 分 → **食べ終わり 153.8 分**
    会計の開始 なし（-1）／帰った なし（-1）／state = Eating
    料理 2皿（dish 145・148）は届いている。**3皿めの dish 157 が来ない**
    → 食べ終わってはいるが、注文した料理が揃っていないので会計に進まない

  組 63　卓16・2人
    来店 114.0 分 → 着席 115.9 分 → **食べ始め なし（-1）**
    state = Seated
    料理 1皿（dish 147）は 126.2 分に届いている。**2皿めの dish 153 が来ない**
    → 料理が揃わないので、食べ始められない

  ★ 組63 は全64組の中で **料理が1皿しか記録されていない唯一の組**です
    （ほかの2人組は 2〜3皿）。足りない1皿が dish 153 で、
    それがロボット1 に握られたままでした。


====================================================================
3. ロボット1 の無限やり直し（trips.csv・requeues.csv）
====================================================================

  往復の記録（ロボット1。132 分以降）

    往復   開始     所要     時間切れ  未配膳  卓      料理        戻り先
    ──────────────────────────────────────────────────────────────
    118   132.0分  600.0秒   1       1     16     153        standby1
    119   141.9分  600.0秒   1       2     15;16  157;153    pass
    121   151.9分  600.0秒   1       2     15;16  157;153    standby1
    122   161.8分  600.0秒   1       2     15;16  157;153    standby1
      …（**まったく同じ行が 176 まで続く**）…
    176   708.2分  600.0秒   1       2     15;16  157;153    standby1

    ・**56 回**（往復121〜176）、10 分ごとに、1秒も違わず同じ
    ・毎回 total_s = 600.027 秒で **時間切れ**（timeout = 1）
    ・毎回 未配膳 2 皿
    ・毎回 standby1 に戻ってやり直し

  料理の積み直し（requeues.csv）

    t=9710.8 秒  dish 157（卓15・割れやすい）waited 1228.9 秒  積み直し 2 回め  why=打ち切り
    t=9710.8 秒  dish 153（卓16）          waited 1793.3 秒  積み直し 3 回め  why=打ち切り
    t=10308.5 秒 dish 157  waited 1826.7 秒  積み直し 3 回め  why=打ち切り
    t=10308.5 秒 dish 153  waited 2391.0 秒  積み直し 4 回め  why=打ち切り
      …（158 分以降で **112 件**。dish 157 が 56 回・dish 153 が 56 回）…

    ・**料理はちゃんと board に戻されています**（why = 打ち切り）
    ・けれど戻した **同じ瞬間に同じロボットが取り直します**
    ・積み直しの回数（requeued_so_far）は 57 回まで増え続ける
    ・待ち時間（waited_s）は 2988 秒 … と増え続け、上限がない

  ★ ここが本当の詰まりです。
    「board に戻す」までは動いているのに、**何回失敗しても
    ロボットから取り上げてスタッフに渡す道がありません**。
    だから仕事板は（私たちが見る瞬間には）いつも空で、
    スタッフ4人は「頼まれていないから手空き」のままでした。

  ★ スタッフに渡す道は **ある**のに使われていません。
    同じ回の dish 93 は robot_wait_expired=1 で、
    staff_dishes=7（7皿はスタッフが運んだ）と記録されています。
    つまり「ロボットを待ちきれない→スタッフが運ぶ」仕組みは動くのに、
    **往復が時間切れになった後の皿には効いていません**。


====================================================================
4. なぜロボットが着けないのか（ここは特定できていません）
====================================================================

  記録の上では、ロボットは自分を「正常」と言っています。

    立ち往生（stuck）        0
    止まった回数（n_stops）   1（0.95 秒だけ）
    通路待ち（wait_aisle_s）  0
    他のロボットと会った      0 回
    道の引き直し（replans）   0
    よけた（robot_yields）    0
    道は引けたか             **引けた**（standby1 → 卓15 の停止点・角 10 個）
    最小の余裕              1.97 m（狭い通路に入っていない）

  動いた記録（往復121 のみ）
    x=16.9, z=6.05（standby1・左の余裕 29.9 cm）
      → x=15.3, z=5.95（左の余裕 42.1 cm）
      → x=12.298, z=3.857（左の余裕 90.0 cm・**スタッフとすれ違った**）
    行き先は毎回 x=5.613, z=8.662（卓15 の停止点）。標準の経路長 14.31 m。

  往復122 以降は、standby1 での 0.95 秒の止まりが1件だけで、
  **ほかには何も記録されていません**。9.3 時間で止まりの記録は合計 57 件。
  経路 14.3 m を普通に走れば 30 秒ほどで着く距離を、600 秒かけて着きません。

  ・NavMesh の警告・例外はログに1件もありません（Unity のログを確認）
  ・NavMesh からの外れ（off_mesh 1.77 cm）は standby1 では **8.2 分の往復1
    でも同じ値**で、そのときは普通に動けています。原因ではありません

  いちばん疑っているもの（**証拠はまだありません**）

    cs:10 で入れた「すれ違いの後ずさり」の分かれ道です。
      ServingRobotController.FaceAndMove の頭に
        if (_passByBackingOff) { BackOffStep(dt); return; }
      を置きました。ここに入ると、**目的地へ進む処理も、
      止まりを記録する処理も通りません**。
    これなら「止まりも立ち往生も記録されないのに着かない」と合います。

    合わない点もあります：132 分以降 すれ違いは1件も起きておらず
    （met_robot=0）、_passByOther は居ないはずです。
    「旗が立ったまま戻らない」場合に限って説明がつきます。

  はっきりさせる方法（どれも Assets を直さずにできます）
    (ア) この回を **SimHud の1倍**で見る（依頼 v13 の 依頼2）。
         種0・③2台・すれ違い入 を Site Run で回し、132 分あたりを見る。
         1倍だと 132 分まで 2 時間以上かかるので、
         倍率表示だけ入れて速く回し、止まったところで見るのが現実的です
    (バ) det_frames（1コマごとの位置の指紋）を入れて回し、
         132 分以降のロボット1 の座標を追う。
         ExperimentPlan の detCheckFrameFingerprint を入れるだけで、
         Assets のコードは直しません
    (ウ) 後ずさりの旗が立った回数を数える欄を足す（コードを直すので、
         今日はやりません）

  私は **(バ)** を勧めます。座標が出れば「その場で揺れている」のか
  「どこかを往復している」のかが分かり、疑いを白黒つけられます。


====================================================================
5. 「切」と比べる（これは「入」特有です）
====================================================================

  70回ぜんぶの分布

    積み直し（requeued_dishes）
      切 60回   中央 0.0   幅 0〜4
      入 10回   中央 0.5   幅 **0〜117**

    時間切れ（trips.timeouts）
      切 60回   中央 0.0   幅 0〜7
      入 10回   中央 2.0   幅 **1〜59**

  ・**「入」の10回は、すべて1回以上 時間切れ**になっています（幅 1〜59）。
    「切」の60回は中央値 0 です。
  ・この回（種0・入）は 時間切れ 59 回・積み直し 117 回で、
    2番目（切の ⑦2台・種2 の 時間切れ 7 回）から大きく離れた外れ値です。
  ・全員が帰らなかった回は、70回のうち **この1回だけ**です。

  → すれ違いの走り方（cs:10 の①②③）は、
    **店を悪くするだけでなく、まれに回を壊します**。
    「標準は切のまま」というエイイチの判断は、この件でさらに裏づけられました。


====================================================================
6. まとめ（誰が・どこで・何を待っていたか）
====================================================================

  客     組62（卓15・4人）… dish 157（割れやすい）を待っていた。
                            食べ終わっているのに会計に進めない
         組63（卓16・2人）… dish 153 を待っていた。食べ始められない

  ロボット ロボット1 … 上の2皿を持って、standby1（x=16.9, z=6.05）から
                      卓15 の停止点（x=5.613, z=8.662）へ向かうが着けない。
                      600 秒で時間切れ → 皿を board に戻す →
                      同じ瞬間に取り直す、を **56 回**

  スタッフ 4人とも手空き。仕事板は空。
          **皿はいつもロボットが握っていたので、一度も頼まれなかった**

  ロボット2 往復120（152 分ごろ）で仕事を終えて、あとは待機。
            この件には関わっていません


====================================================================
7. 直すとしたらの案（まだ直していません）
====================================================================

  A. 積み直しに上限をつける（いちばん効く・いちばん小さい）
     同じ皿が N 回（例 2回）打ち切りで戻されたら、
     **ロボットには渡さずスタッフの仕事にする**。
     仕組みは既にあります（robot_wait_expired → staff_dishes）。
     「往復の時間切れ」をその引き金に足すだけです。
     → これなら、原因が分からなくても回が壊れなくなります

  B. 同じ皿を同じロボットが続けて取らないようにする
     打ち切った皿は、そのロボットだけ一定時間取れないようにする

  C. 往復の時間切れを「立ち往生」として数える
     いまは stuck=0・n_stops=1 なので、判定にも台帳にも
     「おかしい」が出ません。時間切れ59回が summary の
     trips.timeouts にしか出ないのは見つけにくいです

  D. 4 章の原因そのものを直す（まず (バ) で特定してから）

  ★ A だけでも「720 分終わらない回」は無くなります。
    原因（D）が分かっていなくても先に入れられるので、
    次の夜の前に A を入れるのを勧めます。やりますか。


====================================================================
8. 使った記録
====================================================================

  20261027_night_cs21_fam_p1_r2_s0_passon/
    timeline.csv    60 秒ごとの店の様子（722 行）… 1 章
    groups.csv      64 組の流れ … 2 章
    dishes.csv      届いた 158 皿 … 2 章（153・157 は入っていない＝届いていない）
    trips.csv       176 往復 … 3 章
    requeues.csv    117 件の積み直し … 3 章
    stops.csv       1051 件の止まり … 4 章
    passes.csv      84 件のすれ違い（最後は 132.4 分）… 4 章
    summary.json    face_stop・requeued_dishes など … 3・5 章
  night/20261027/logs/…_s0_passon.log   Unity のログ（警告・例外なし）
```

### 20261027_0804_v3_baseline_and_dish_fix.txt

```text
====================================================================
基準データ v3・皿の打ち切り上限・原因の調べ
2026-10-27 08:04　check-in：cs:22・cs:23・cs:24（保留 0）
====================================================================

結論

  1. 基準データ v3 に替えました（**cs:22**）。基準どうしの確かめで
     誤った印が1つも出ません。
  2. v3 の60回で「同じ皿が2回以上 打ち切り」は **0 回・0 枚**でした。
     なので直し（2回でスタッフに渡す）を入れました（**cs:23・cs:24**）。
     **基準データ v3 の数字は1つも変わりません**（実測でも確かめた）。
  3. 原因の調べ：**同じ種では再現しませんでした**（全員帰って 172 分で終了）。
     ただし同じ回の中で **ロボットが 551.9 秒（9.2 分）完全に静止**している
     場面を捕まえ、その中身が分かりました（3 章）。
     ★ 私が前に「いちばん疑っている」と書いた**後ずさりの分かれ道は、
       今回の証拠では支持されません**。取り下げます。
  4. 切り替え（det_check）は全部 切 に戻し、ExperimentPlan.asset が
     cs:22 と差分なしであることを確かめました。保留 0。


====================================================================
1. 基準データ v3（cs:22）
====================================================================

  judge_night.py
    BASELINE_DEFAULT  '20261025_stageB_cs7_*' → **'20261027_night_cs21_*'**
    BASELINE_EXCLUDE  **'*_passon*'**（新しく足した。--baseline-exclude で変更可）

  v3 ＝ 最初の夜（night 20261027・ServiceSim/cs:21）の
       **すれ違い「切」の60回**（③⑦ × ロボット 0/1/2 台 × 種0〜9）

  幅の取り方は **1条件10回の最小〜最大**。
  これは前からそうだったので（min(bv)〜max(bv)）、コードは変えていません。
  変わったのは「1条件が3回→10回になった」ことです。

  すれ違い「入」を外す理由
    標準は「切」。しかも種0 は 720 分終わらず数字が使えない。
    glob では名前を除けないので、BASELINE_EXCLUDE で外す形にしました。

  基準どうしの確かめ（いちばん大事な確かめ）
    python3 judge_night.py --runs '20261027_night_cs21_*' --out …
      基準データ: 20261027_night_cs21_*（*_passon* を除く）（**60回**）
      振り分けた回: 70回
        正常               **60**
        Unity の作り        **0**  ← 基準そのものに誤った印が出ない
        試算を見直す候補      0
        回の失敗             1（s0_passon）
        基準なし             9（すれ違い入）

    前の基準（cs:7）では ③1台・⑦1台・⑦2台 の3条件に
    「Unity の作り」の印が付いていました。あれは基準が3種しかなく
    幅が偶然狭かったせい（⑦1台の「動けなくなった」の幅は 3〜3）で、
    v3 では消えました。


====================================================================
2. 皿の打ち切り上限（cs:23・cs:24）
====================================================================

  2-1. 数え上げ（直す前に数えた・エイイチの条件）

    v3 の60回（すれ違い切）で「同じ皿が2回以上 打ち切りで戻された」
      **0 回 / 60 回**・**0 枚**

    参考
      1回だけ打ち切り           12 回で 25 枚
      入 s0_passon             2 枚（皿153:58 回・皿157:57 回）
      入 s1_passon             3 枚（皿126:4 回・皿146:3 回・皿145:3 回）

    → 0 回なので、相談なしで入れました（エイイチの条件どおり）。
      そしてこれは **「基準データ v3 の数字が1つも変わらない」証明**
      にもなります。上限 2 に1枚も届かないので、決まりが当たりません。

  2-2. なぜ「戻す」だけでは助からなかったか（調べて分かったこと）

    RequeueDish は、ちゃんと皿を戻して板にも出していました。

      _pass.Remove(d); _pass.Insert(0, d);          // 受け渡し口の **先頭** へ
      RemoveServeJob(d);
      if (slot != null && d.staffEligibleS >= 0f)
          PostJobFront(StaffJobKind.Serve, …);       // 板にも出す

    ところが受け渡し口のすぐ横にロボットが居るので、
    **歩いてくるスタッフより先にロボットが取り直し**、
    PickBatch → RemoveServeJob で板から消えます。
    だから 60 秒ごとの timeline では板はいつも空に見え、
    スタッフ4人は「頼まれていない」まま手空きでした。

  2-3. 入れた直し

    ServiceSpec   ＋ maxRobotTripTimeoutsPerDish = **2**（0 で無制限＝前と同じ）
    DishOrder     ＋ robotTimeouts（打ち切りで戻された回数）
                  ＋ staffOnly（ロボットには渡さない皿）
    RequeueDish   why が「打ち切り」のとき数え、上限に届いたら
                  staffOnly を立て、staffEligibleS を入れ、警告を1行出す
    PickBatch     ＋ if (d.staffOnly) continue;
                  （「人だけが運ぶ料理」の前例と同じ形）
    記録          summary.json の sim_like に dishes_to_staff_after_timeout
                  requeues.csv に to_staff 列（末尾に追加）

    ★ cs:23 で数えるところまで入れて summary.json に出し忘れ、
      cs:24 で足しました（commit メッセージでは「記録」と書いていたので）。

  2-4. 効き目の確かめ（実測）

    ① 発動するか … ③2台・種1・すれ違い入（名札 fixcheck2・cs:23）
       「皿 14（卓 4）は往復の打ち切りが 2 回になったので、
         ロボットには渡さずスタッフの仕事にします」… **8 枚**で発動
       requeues.csv に to_staff=1 が 8 行
       全員帰った True / 店の中 184.3 分

                        直す前（夜 cs:21）  直したあと（cs:23）
         時間切れ            6               8
         積み直し            11              18
         ロボが運んだ皿       171             140
         スタッフが運んだ皿    35              66
         ロボット分担         0.830           0.680

       ★ すれ違い「入」の回は、この直しで数字が変わります
         （皿がロボットからスタッフへ移るので当然）。
         「入」は標準ではなく基準も無いので問題にしませんが、
         入の回を直す前と比べるときは覚えておいてください。
         なお上の差には「同じ種でもずれる」ぶんも混ざっています。

    ② 「切」で発動しないか … ⑦2台・種2（切の中で時間切れが最多だった回。
       名札 fixcheck3・cs:24）
         **dishes_to_staff_after_timeout = 0**
         警告も 0 件（時間切れは 4 回あったが、同じ皿が2回には届かず）
       → 2-1 の数え上げ（0 枚）が、実測でも裏づけられました。
         ※ この回の入店待ちは 12.21 分で夜の 1.63 分と大きく違いますが、
           これは既知の「同じ種でもずれる」ぶんです。
           **直しが効いていないことは、数（0）で言えます**。


====================================================================
3. 原因の調べ（detCheckFrameFingerprint）
====================================================================

  3-1. まず分かったこと：det_frames は座標を持っていません

    det_frames.csv が記録しているのはロボット **位置の合計値**です。
      rSum = Σ(round(x*10000)*31 + round(z*10000)*17)
    2台ぶんが1つの数に混ざるので、
      取れる   … 動いているか・止まっているか
      取れない … 座標そのもの、「後ずさりの分かれ道に入ったか」
    後者を出すには det_frames に1台ずつの x,z を書き足す（コードを直す）
    必要があるので、今回は前者で進めました。

  3-2. 同じ種では再現しませんでした

    ③2台・種0・すれ違い入 を1回（名札 detcheck・cs:22）
      everyone_left  **True**（夜は False）
      店の中          **172.3 分**（夜は 720.0 分）
      時間切れ         **3**（夜は 59）
      積み直し         **4**（夜は 117）
      実時間           2.3 分（夜は 14.4 分）

    → **種では決まらない**不具合です。間合い（どのコマで何が重なるか）で
      起きます。10/25 に分かった「同じ種でもずれる」と、
      busycheck で分かった「忙しさで詰まりが変わる」と同じ筋です。
      夜の失敗した回は同時 4 本の中の1本（parallel_actual 4）、
      今回は 1 本で回したので、条件も違います。
    ★ 観測そのものが間合いを変えている可能性もあります
      （det_frames で 1 コマごとに 20 万行書くので、重さが変わる）。

  3-3. ただし同じ回の中で、同じ型の固まりを捕まえました

    この回には時間切れが3回あり、そのうち **往復116（ロボット2）** が
    決定的でした。

      det_frames … 位置の合計が **9150.4 秒 〜 9700.1 秒 の 551.9 秒間
                   まったく同じ**（＝ロボットは完全に静止）
      その間の止まり記録（stops.csv）

        9094.1 秒  x=15.11 z=5.82  原因=分からない    3.05 秒  自分で動き出した
        9167.4 秒  x=14.358 z=5.384 原因=**もう1台のロボット（ServingRobot01）**
                   60.05 秒  **動けなくなった**  blocked 60.05 秒 / no_path **52.9 秒**
        9243.2 秒  同じ場所・同じ内容（60.05 秒）
        9319.7 秒  同じ
        9396.2 秒  同じ
        9473.2 秒  同じ
        9550.8 秒  同じ
        9628.6 秒  同じ          ← ここまで **7 回**くり返し
        9705.8 秒  13.45 秒  **回り込んで動き出した**（やっと抜けた）

      ・場所の余裕は **275 cm**。狭くて挟まったのではありません
      ・60 秒のうち **53 秒は「道が引けない」**（no_path_s 52.9）
      ・原因欄に **もう1台のロボット（ServingRobot01）** と出ています
      ・直前（9077.95 秒）に すれ違いが1件あり、結果は「すれ違えた」

    つまり型はこうです。
      **2台が互いの前をふさぐ → 道が引けない → 60 秒待って「動けなくなった」
      → 引き直す → まだ引けない → … を 9.2 分くり返し、
      たまたま回り込めて抜けた。**
    夜の失敗した回は、これが **抜けられなかった**のだと見ています。

  3-4. 前に書いた疑いを取り下げます

    10/27 07:43 の報告で「いちばん疑っているのは cs:10 で入れた
    後ずさりの分かれ道（_passByBackingOff → BackOffStep → return）」
    と書きました。**今回の証拠では支持されません。**
    止まりはちゃんと記録されていて、原因欄も「もう1台のロボット」と
    出ています。記録に出ない分かれ道に入っていたのではなく、
    **記録される普通の道（道が引けない）で詰まっていました**。

    ただし完全に同じとは言えません。夜の失敗した回は
    600 秒の往復に対して止まりが **standby1 で 0.95 秒 1 件だけ**で、
    今回の「60 秒 × 7 回」とは形が違います。
    夜の回そのものの原因は、まだ特定できていません。

  3-5. 切り替えは戻しました

    ExperimentPlan.asset の detCheckSyncPeoplePaths / detCheckNoAvoidance /
    detCheckFrameFingerprint はすべて 0。
    cm cat で cs:22 と比べて **差分なし**を確認。保留 0。
    Assets のコードは、この調べでは1行も直していません。


====================================================================
4. 残っていること・相談
====================================================================

  ① 夜の失敗した回そのものの原因は未特定です
     再現しないので、やれることは2つです。
     (ア) 4本同時で s0_passon を何回か回して再現を狙う
          （1回 4 分弱。10 回で 40 分ほど。再現率は不明）
     (イ) det_frames に **1台ずつの x,z** を書き足して、
          次に起きたときに必ず分かるようにしておく（十数行の直し）
     私は **(イ)** を勧めます。再現待ちをするより、
     次に起きたときに1回で分かる形にしておくほうが確実です。

  ② 2台が互いをふさぐ件（3-3）は、直しの候補が別にあります
     ・60 秒で「動けなくなった」と判定したら、**回り込み**を
       もっと早く試す（今回は 9.2 分かかって最後に回り込めた）
     ・受け渡し口の点が1つしかない件（既知）を直す
     どちらも動きを変えるので、基準データ v3 を取り直すことになります。
     今は触らないほうがよいと思います。

  ③ Unity を次に開くと、ServiceSpec.asset に
     maxRobotTripTimeoutsPerDish: 2 の行が出て保留 1 件になります。
     その行はそのまま check-in して構いません（既定を .asset に残す形）。
     私がやってもよいので、開いたら声をかけてください。

  ④ 台帳に足した行（この作業ぶん）
     20261027_detcheck_cs22_fam_p1_r2_s0_passon    （調べ用）
     20261027_fixcheck2_cs23_fam_p1_r2_s1_passon   （発動の確かめ）
     20261027_fixcheck3_cs24_fam_p5_r2_s2          （切で発動しない確かめ）
     名札で分かれるので、基準 v3（20261027_night_*）には入りません。


====================================================================
5. check-in
====================================================================

  cs:22  judge_night.py … 基準データ v3（BASELINE_DEFAULT・BASELINE_EXCLUDE）
  cs:23  ServiceSpec / ServiceOps / TripLogger … 打ち切り2回でスタッフへ
  cs:24  TripLogger / ExperimentRunner … dishes_to_staff_after_timeout を
         summary.json に出す（cs:23 の書き忘れ）

  いまの保留 0。実行ファイルは cs24_20261027_0800（保留 0・ARM64）。
  台帳の列（59列・schema 2）は変えていません。
```

### 20261027_0958_passby_default_on.txt

```text
====================================================================
すれ違い「入」を標準にする・基準データ v4 の夜の準備
2026-10-27 09:58　check-in：cs:26（保留 0）
====================================================================

結論

  ・RobotSpec の passByEnabled の既定を「入」にしました（**cs:26**）。
  ・make_queue.py の --pass-by の既定を on にしました（同じ cs:26）。
    --pass-by off で切の回も作れます。--pass-by-extra はそのままです。
  ・実行ファイルを作り直しました。**build_id cs26_20261027_0956・保留 なし**
    （Unity は閉じていたので、コマンドから作れました）。
  ・--dry-run（夜の名前 20261029・4本同時）で **60回が種ごとに並びました**。
    全部すれ違い入、出力フォルダ名の末尾には **_passon が付きます**。
  ・judge_night.py は触っていません（基準は v3 のまま）。
  ・本番の夜は回していません。台帳にも書いていません。


====================================================================
1. 変えたところ（cs:26）
====================================================================

  Assets/ServiceSim/Robot/RobotSpec.cs
    public bool passByEnabled = false; → **true**
    説明（Tooltip）を「標準は入（2026-10-27 エイイチの決定）」に書き替え

  Assets/ServiceSim/Robot/RobotSpec.asset
    passByEnabled: 0 → **1**
    ※ 実際に効くのはこちらの値です（.cs の既定は新しく作る時だけ使われる）

  tools/make_queue.py
    --pass-by の既定 'off' → **'on'**
    使い方の説明を書き替え（60回＝入が標準、切の70回の作り方も残した）

  変えていないもの
    judge_night.py（BASELINE_DEFAULT '20261027_night_cs21_*'・
    BASELINE_EXCLUDE '*_passon*' のまま）、night_runner.py、
    店の回し方・人の動き・皿の打ち切り上限・robot_track.csv、台帳


====================================================================
2. 実行ファイル
====================================================================

  build_id         cs26_20261027_0956
  unity_cs         ServiceSim/cs:26
  pending_changes  ''（保留なし）
  作った時刻        2026-10-27 09:56:44
  ARM64・StandaloneOSX・Unity 2022.3.62f3
  作ったあとも cm status は保留 0


====================================================================
3. --dry-run（夜の名前 20261029・4本同時）
====================================================================

  python3 tools/make_queue.py --out /tmp/queue_20261029_dryrun.json
    → 条件 6 / 種 0〜9 / 合わせて 60 回（すれ違い 切 0 回・入 60 回）
      並べ方: 種ごと（種0 の全6条件 → 種1 の全6条件 → …）

  python3 tools/night_runner.py --queue /tmp/queue_20261029_dryrun.json \
      --parallel 4 --night 20261029 --dry-run
    → 終了コード 0・dry-run: 60 回

  最初の数行
    2026-10-27 09:57:04  === 夜の実行係を始めます（night 20261029・同時 4 本・上限 90.0 分）===
    2026-10-27 09:57:04  並べた回: /tmp/queue_20261029_dryrun.json（60 回）
    2026-10-27 09:57:04  台帳: /Volumes/KIOXIA/r2r2r-data/service_sim/unity/unity_ledger_v1.csv（59 列）
    2026-10-27 09:57:04  版: ServiceSim/cs:26（Builds/headless/build_info.json）→ 出力フォルダの字 cs26
    2026-10-27 09:57:04  並べた回の試算の版: v2.8 / 店の形の版: 8a5b2f6a016a
    2026-10-27 09:57:04  並べた回 60 / もう completed の回を飛ばす 0 / 回す 60
    2026-10-27 09:57:04  （から回し）1 回め fam_p1_s4_r0_a42 種0 すれ違い入 同時1 → 20261029_night_cs26_fam_p1_r0_s0_passon
    2026-10-27 09:57:04    /Users/shimazueiichi/Robotics_UX/ServiceSim/Builds/headless/ServiceSim.app/Contents/MacOS/ServiceSim -batchmode -nographics -logFile /Volumes/KIOXIA/r2r2r-data/service_sim/unity/night/20261029/logs/20261029_night_cs26_fam_p1_r0_s0_passon.log -ssCondition fam_p1_s4_r0_a42 -ssSeed 0 -ssLabel night -ssNight 20261029 -ssParallel 4 -ssRunning 1 -ssPassBy on
    （…）

  最後の数行
    2026-10-27 09:57:04  （から回し）60 回め fam_p5_s4_r2_a42 種9 すれ違い入 同時4 → 20261029_night_cs26_fam_p5_r2_s9_passon
    2026-10-27 09:57:04    /Users/shimazueiichi/Robotics_UX/ServiceSim/Builds/headless/ServiceSim.app/Contents/MacOS/ServiceSim -batchmode -nographics -logFile /Volumes/KIOXIA/r2r2r-data/service_sim/unity/night/20261029/logs/20261029_night_cs26_fam_p5_r2_s9_passon.log -ssCondition fam_p5_s4_r2_a42 -ssSeed 9 -ssLabel night -ssNight 20261029 -ssParallel 4 -ssRunning 4 -ssPassBy on
    2026-10-27 09:57:04  --- まとめ ---
    2026-10-27 09:57:04    dry-run: 60 回
    2026-10-27 09:57:04  === 夜の実行係を終わります（終了コード 0）===

  出力フォルダ名の形
    20261029_night_cs26_{店}_r{台数}_s{種}_passon
    例 20261029_night_cs26_fam_p1_r2_s0_passon
    ★ 入が標準になっても **_passon は付きます**（night_runner.py は
      「入の回に _passon を付ける」決まりのまま。変えていません）。
      切の回（--pass-by off）は前と同じく末尾なしです。

  から回しが残したもの
    KIOXIA の night/20261029/ に runner_log.txt が1つだけできたので、
    本番のログと混ざらないよう runner_log_dryrun_20261027.txt に
    **改名しました**（消していません。前の夜と同じ扱い）。
    出力フォルダ・runs.csv・台帳の行は 0。queue.json（プロジェクトの）も
    触っていません（から回しは /tmp に並べた）。
    台帳は 265 行のまま（最後の行は 09:46 の 20261028 の夜のもの）。


====================================================================
4. 本番のコマンド（夜の名前 20261029）
====================================================================

  ※ 台帳の退避はエイイチがしてから。Unity は閉じて、KIOXIA をつないで。

  cd /Users/shimazueiichi/Robotics_UX/ServiceSim
  python3 tools/make_queue.py --out queue.json
  python3 tools/night_runner.py --queue queue.json --parallel 4 --night 20261029


====================================================================
5. 決めてほしいこと・気をつけること
====================================================================

  ① v4 に替えるときは、除く決まりも替える必要があります
     v4 の60回は全部 _passon が付くので、いまの BASELINE_EXCLUDE
     '*_passon*' のまま '20261029_night_cs26_*' を基準にすると
     **0回になります**。替えるときは
       BASELINE_DEFAULT '20261029_night_cs26_*'
       BASELINE_EXCLUDE なし（または切の回を除く形）
     の組で替えることになります。どちらにするかは夜のあとで決めてください。

  ② 夜の最後に自動で呼ばれる判定は、基準 v3（切）と比べます
     入の60回を切の基準と比べるので、「Unity の作り」などの印が
     付くことがあり得ます。v4 は判定の印ではなく、数字そのものを
     見て確かめてください。

  ③ --pass-by on（既定）と --pass-by-extra を一緒に使うと、
     同じ条件・同じ種の「入」が2回並び、出力フォルダ名がぶつかります。
     依頼どおり --pass-by-extra は変えていません。使うのは
     --pass-by off のときだけにしてください（直すなら相談のうえで）。

  ④ 実行ファイルに -ssPassBy を渡さないときの既定は、
     HeadlessRun.cs ではまだ「切」です。night_runner.py はいつも
     -ssPassBy を渡すので夜には効きませんが、手で実行ファイルを
     起こすときは -ssPassBy on を付けてください。
     既定を「入」に揃えるかは決めてください（範囲外なので触っていません）。
```

### 20261029_night_v4.txt

```text
基準データ v4 の夜（20261029・60回・すれ違い入）報告（2026-10-27）

■ 始める前の確かめ（5つとも合格）
1. Unity エディタ：起動していない（Unity Hub は起動していたが、エディタ本体は動いていない）
2. /Volumes/KIOXIA：マウントされている（os.path.ismount = True）
3. 台帳の md5：b7f65cde2f6f5b1396cb706974bea226（退避と同じ・265 行）
4. 鍵ファイル unity_ledger_v1.lock：なし
5. ServiceSim の保留：0（cm status で保留なし・cs:26 head）
   build_info：build_id = cs26_20261027_0956、pending_changes = 空（保留なし）

■ 回した内容
- make_queue.py：60 回（条件 6 × 種 0〜9、すれ違い 入 60 回・切 0 回）、試算の版 v2.8、店の形の版 8a5b2f6a016a
- night_runner.py --parallel 4 --night 20261029：10:19:09 開始 → 11:18:48 終了、終了コード 0

■ runner_log.txt のまとめ（night/20261029/runner_log.txt）
- completed：60 回
- 失敗：0 回
- 止まった回：0 回
- 終了コード 3：0 回
- 実時間：平均 3.9 分、最長 4.8 分（20261029_night_cs26_fam_p5_r2_s6_passon）

■ 台帳
- 行数：265 → 325（60 行増えた）
- 先頭 265 行の md5：b7f65cde2f6f5b1396cb706974bea226（退避と同じ）
- 新しい 60 行：status はすべて completed、鍵ファイルは残っていない

■ 全員が帰らなかった回
- なし（新しい 60 行すべて everyone_left = 1）

■ judge.md
- できた：/Volumes/KIOXIA/r2r2r-data/service_sim/unity/night/20261029/judge.md（中身の判断はしていない）
```

### 20261027_1141_baseline_v4.txt

```text
====================================================================
基準データを v4 に替える
2026-10-27 11:41　check-in：cs:27（保留 0）
====================================================================

結論

  ・judge_night.py の基準の既定を **20261029_night_cs26_*（v4・すれ違い入の60回）** に
    替え、既定の除外（*_passon*）を外しました。--baseline-exclude を渡したときだけ除きます。
  ・make_queue.py は、--pass-by on（既定）で --pass-by-extra を渡すと誤りで止まります。
  ・HeadlessRun.cs の -ssPassBy の既定を「入」にしました。
  ・check-in **cs:27**、実行ファイル **build_id cs27_20261027_1141・保留 なし**。
  ・確かめ3つはどれも依頼のとおりでした（下の 3 章）。
  ・本番の夜は回していません。台帳は 325 行のまま（書いていません）。


====================================================================
1. 変えたところ（cs:27）
====================================================================

  tools/judge_night.py
    BASELINE_DEFAULT '20261027_night_cs21_*' → **'20261029_night_cs26_*'**
    BASELINE_EXCLUDE '*_passon*'             → **''**（既定では外さない）
    説明を v4 に書き替え（v3 を見たいときの指定も書いた：
      --baseline '20261027_night_cs21_*' --baseline-exclude '*_passon*'）
    --baseline-exclude の説明「既定はすれ違い「入」を外す」→「既定は外さない」

  tools/make_queue.py
    --pass-by が on のときに --pass-by-extra が指定されたら、理由を出して止まる
    （終了コード 1・queue は書かない）。説明の文も合わせて直した。

  Assets/ServiceSim/Experiment/HeadlessRun.cs
    _passBy の初期値 "off" → **"on"**
    -ssPassBy が無いときの既定 off → **on**（"off" と渡したときだけ切）
    使い方の説明「既定 off」→「既定 on。2026-10-27 エイイチの決定」

  変えていないもの
    night_runner.py、店の回し方・人とロボットの動き、台帳の列と今までの行、night/ の記録


====================================================================
2. 実行ファイル
====================================================================

  build_id         cs27_20261027_1141
  unity_cs         ServiceSim/cs:27
  pending_changes  ''（保留なし）
  作った時刻        2026-10-27 11:41:41
  ARM64・StandaloneOSX・Unity 2022.3.62f3
  Unity は閉じていたのでコマンドから作った（Build Finished, Result: Success・error CS 0）
  作ったあとも cm status は保留 0


====================================================================
3. 確かめ（台帳に新しい行は書いていない）
====================================================================

  ① judge_night.py を v4 の60回そのものに当てた
     cd /Volumes/KIOXIA/r2r2r-data/service_sim/unity
     python3 .../tools/judge_night.py --runs '20261029_night_*' --out /tmp/judge_v4_GLpj
     → 基準データ: 20261029_night_cs26_*（60回）／振り分けた回: 60回
       正常 **60**・試算を見直す候補 0・Unity の作り 0・回の失敗 **0**・基準なし **0**
       （「基準から N 回 外した」の表示は出ない＝既定で何も外していない）
     ※ 記録として、向きの印が1つ出ました（数えは「正常」のまま）
       fam_p1 スタッフ4・すれ違い入 1→2台: 入店待ち — 逆 6/10 種 — 「基準 v2 でも同じ（既知）」
     台帳 325 行・中身の md5 も前後で同じ

  ② make_queue.py --out /tmp/q_err.json --pass-by-extra fam_p1_s4_r2_a42
     → 「--pass-by-extra は --pass-by off のときだけ使えます。…」で **終了コード 1**、
       /tmp/q_err.json はできない
     （参考：--pass-by off --pass-by-extra fam_p1_s4_r2_a42 は今までどおり 70 回・入 10 回）

  ③ --dry-run（夜の名前 20261030・4本同時）
     python3 tools/make_queue.py --out /tmp/queue_20261030_dryrun.json   → 60 回・入 60 回
     python3 tools/night_runner.py --queue /tmp/queue_20261030_dryrun.json \
         --parallel 4 --night 20261030 --dry-run
     → 終了コード 0・dry-run: **60 回**・版 ServiceSim/cs:27 → 出力フォルダの字 cs27
       60 本すべてに **-ssPassBy on**（off は 0 本）
       例 20261030_night_cs27_fam_p1_r0_s0_passon … 20261030_night_cs27_fam_p5_r2_s9_passon
     から回しが night/20261030/ に作った runner_log.txt は、本番と混ざらないよう
     runner_log_dryrun_20261027.txt に改名した（消していない。前と同じ扱い）。
     出力フォルダ 0・台帳の行 0。プロジェクトの queue.json は触っていない。


====================================================================
4. 決めてほしいこと・気づいたこと
====================================================================

  決めてほしいこと：なし

  気づいたこと（範囲外なので触っていません）
    judge_night.py の向きの印の文「基準 v2 でも同じ（既知）」は、v3 のときから
    基準の名前が書き替わっていません（中身は「いまの基準でも同じ向き」の意味）。
    「基準 v4 でも同じ」などに直すかは、必要なら依頼してください。
```


## 夜の記録（KIOXIA の service_sim/unity/night/）

### night/20261027（最初の夜・cs:21・70回）

### night/20261027/runner_log.txt

```text
2026-10-26 22:11:01  === 夜の実行係を始めます（night 20261027・同時 4 本・上限 90.0 分）===
2026-10-26 22:11:01  並べた回: queue.json（70 回）
2026-10-26 22:11:01  台帳: /Volumes/KIOXIA/r2r2r-data/service_sim/unity/unity_ledger_v1.csv（59 列）
2026-10-26 22:11:01  版: ServiceSim/cs:21（Builds/headless/build_info.json）→ 出力フォルダの字 cs21
2026-10-26 22:11:01  並べた回の試算の版: v2.8 / 店の形の版: 8a5b2f6a016a
2026-10-26 22:11:01  スリープを止めました（caffeinate pid 7845）
2026-10-26 22:11:01  並べた回を写しました: /Volumes/KIOXIA/r2r2r-data/service_sim/unity/night/20261027/queue.json
2026-10-26 22:11:01  並べた回 70 / もう completed の回を飛ばす 0 / 回す 70
2026-10-26 22:11:01  始めました（1本目・同時 1）: fam_p1_s4_r0_a42 種0 すれ違い切 → 20261027_night_cs21_fam_p1_r0_s0（pid 7846）
2026-10-26 22:11:06  始めました（2本目・同時 2）: fam_p1_s4_r1_a42 種0 すれ違い切 → 20261027_night_cs21_fam_p1_r1_s0（pid 7848）
2026-10-26 22:11:11  始めました（3本目・同時 3）: fam_p1_s4_r2_a42 種0 すれ違い切 → 20261027_night_cs21_fam_p1_r2_s0（pid 7851）
2026-10-26 22:11:16  始めました（4本目・同時 4）: fam_p5_s4_r0_a42 種0 すれ違い切 → 20261027_night_cs21_fam_p5_r0_s0（pid 7857）
2026-10-26 22:14:21  おわり: 20261027_night_cs21_fam_p1_r0_s0 / completed / 終了コード 0 / 実時間 3.3 分
2026-10-26 22:14:21  始めました（5本目・同時 4）: fam_p5_s4_r1_a42 種0 すれ違い切 → 20261027_night_cs21_fam_p5_r1_s0（pid 7924）
2026-10-26 22:14:41  おわり: 20261027_night_cs21_fam_p1_r1_s0 / completed / 終了コード 0 / 実時間 3.6 分
2026-10-26 22:14:41  始めました（6本目・同時 4）: fam_p5_s4_r2_a42 種0 すれ違い切 → 20261027_night_cs21_fam_p5_r2_s0（pid 7935）
2026-10-26 22:14:46  おわり: 20261027_night_cs21_fam_p5_r0_s0 / completed / 終了コード 0 / 実時間 3.5 分
2026-10-26 22:14:46  始めました（7本目・同時 4）: fam_p1_s4_r2_a42 種0 すれ違い入 → 20261027_night_cs21_fam_p1_r2_s0_passon（pid 7938）
2026-10-26 22:15:01  おわり: 20261027_night_cs21_fam_p1_r2_s0 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-26 22:15:01  始めました（8本目・同時 4）: fam_p1_s4_r0_a42 種1 すれ違い切 → 20261027_night_cs21_fam_p1_r0_s1（pid 7956）
2026-10-26 22:18:12  おわり: 20261027_night_cs21_fam_p5_r1_s0 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-26 22:18:12  始めました（9本目・同時 4）: fam_p1_s4_r1_a42 種1 すれ違い切 → 20261027_night_cs21_fam_p1_r1_s1（pid 8071）
2026-10-26 22:18:47  おわり: 20261027_night_cs21_fam_p5_r2_s0 / completed / 終了コード 0 / 実時間 4.1 分
2026-10-26 22:18:47  始めました（10本目・同時 4）: fam_p1_s4_r2_a42 種1 すれ違い切 → 20261027_night_cs21_fam_p1_r2_s1（pid 8103）
2026-10-26 22:19:02  おわり: 20261027_night_cs21_fam_p1_r0_s1 / completed / 終了コード 0 / 実時間 4.0 分
2026-10-26 22:19:02  始めました（11本目・同時 4）: fam_p5_s4_r0_a42 種1 すれ違い切 → 20261027_night_cs21_fam_p5_r0_s1（pid 8119）
2026-10-26 22:22:12  おわり: 20261027_night_cs21_fam_p1_r1_s1 / completed / 終了コード 0 / 実時間 4.0 分
2026-10-26 22:22:12  始めました（12本目・同時 4）: fam_p5_s4_r1_a42 種1 すれ違い切 → 20261027_night_cs21_fam_p5_r1_s1（pid 8225）
2026-10-26 22:23:07  おわり: 20261027_night_cs21_fam_p5_r0_s1 / completed / 終了コード 0 / 実時間 4.1 分
2026-10-26 22:23:07  始めました（13本目・同時 4）: fam_p5_s4_r2_a42 種1 すれ違い切 → 20261027_night_cs21_fam_p5_r2_s1（pid 8256）
2026-10-26 22:23:12  おわり: 20261027_night_cs21_fam_p1_r2_s1 / completed / 終了コード 0 / 実時間 4.4 分
2026-10-26 22:23:12  始めました（14本目・同時 4）: fam_p1_s4_r2_a42 種1 すれ違い入 → 20261027_night_cs21_fam_p1_r2_s1_passon（pid 8258）
2026-10-26 22:26:02  おわり: 20261027_night_cs21_fam_p5_r1_s1 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-26 22:26:02  始めました（15本目・同時 4）: fam_p1_s4_r0_a42 種2 すれ違い切 → 20261027_night_cs21_fam_p1_r0_s2（pid 8313）
2026-10-26 22:27:07  おわり: 20261027_night_cs21_fam_p1_r2_s1_passon / completed / 終了コード 0 / 実時間 3.9 分
2026-10-26 22:27:07  始めました（16本目・同時 4）: fam_p1_s4_r1_a42 種2 すれ違い切 → 20261027_night_cs21_fam_p1_r1_s2（pid 8340）
2026-10-26 22:27:17  おわり: 20261027_night_cs21_fam_p5_r2_s1 / completed / 終了コード 0 / 実時間 4.2 分
2026-10-26 22:27:17  始めました（17本目・同時 4）: fam_p1_s4_r2_a42 種2 すれ違い切 → 20261027_night_cs21_fam_p1_r2_s2（pid 8342）
2026-10-26 22:29:12  おわり: 20261027_night_cs21_fam_p1_r2_s0_passon / completed / 終了コード 0 / 実時間 14.4 分
2026-10-26 22:29:12  始めました（18本目・同時 4）: fam_p5_s4_r0_a42 種2 すれ違い切 → 20261027_night_cs21_fam_p5_r0_s2（pid 8384）
2026-10-26 22:30:22  おわり: 20261027_night_cs21_fam_p1_r0_s2 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-26 22:30:22  始めました（19本目・同時 4）: fam_p5_s4_r1_a42 種2 すれ違い切 → 20261027_night_cs21_fam_p5_r1_s2（pid 8414）
2026-10-26 22:31:37  おわり: 20261027_night_cs21_fam_p1_r1_s2 / completed / 終了コード 0 / 実時間 4.5 分
2026-10-26 22:31:37  始めました（20本目・同時 4）: fam_p5_s4_r2_a42 種2 すれ違い切 → 20261027_night_cs21_fam_p5_r2_s2（pid 8431）
2026-10-26 22:32:07  おわり: 20261027_night_cs21_fam_p1_r2_s2 / completed / 終了コード 0 / 実時間 4.8 分
2026-10-26 22:32:07  始めました（21本目・同時 4）: fam_p1_s4_r2_a42 種2 すれ違い入 → 20261027_night_cs21_fam_p1_r2_s2_passon（pid 8439）
2026-10-26 22:33:32  おわり: 20261027_night_cs21_fam_p5_r0_s2 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-26 22:33:32  始めました（22本目・同時 4）: fam_p1_s4_r0_a42 種3 すれ違い切 → 20261027_night_cs21_fam_p1_r0_s3（pid 8473）
2026-10-26 22:34:52  おわり: 20261027_night_cs21_fam_p5_r1_s2 / completed / 終了コード 0 / 実時間 4.5 分
2026-10-26 22:34:52  始めました（23本目・同時 4）: fam_p1_s4_r1_a42 種3 すれ違い切 → 20261027_night_cs21_fam_p1_r1_s3（pid 8496）
2026-10-26 22:36:27  おわり: 20261027_night_cs21_fam_p5_r2_s2 / completed / 終了コード 0 / 実時間 4.8 分
2026-10-26 22:36:27  始めました（24本目・同時 4）: fam_p1_s4_r2_a42 種3 すれ違い切 → 20261027_night_cs21_fam_p1_r2_s3（pid 8530）
2026-10-26 22:36:42  おわり: 20261027_night_cs21_fam_p1_r2_s2_passon / completed / 終了コード 0 / 実時間 4.6 分
2026-10-26 22:36:42  始めました（25本目・同時 4）: fam_p5_s4_r0_a42 種3 すれ違い切 → 20261027_night_cs21_fam_p5_r0_s3（pid 8541）
2026-10-26 22:36:57  おわり: 20261027_night_cs21_fam_p1_r0_s3 / completed / 終了コード 0 / 実時間 3.4 分
2026-10-26 22:36:57  始めました（26本目・同時 4）: fam_p5_s4_r1_a42 種3 すれ違い切 → 20261027_night_cs21_fam_p5_r1_s3（pid 8545）
2026-10-26 22:38:33  おわり: 20261027_night_cs21_fam_p1_r1_s3 / completed / 終了コード 0 / 実時間 3.7 分
2026-10-26 22:38:33  始めました（27本目・同時 4）: fam_p5_s4_r2_a42 種3 すれ違い切 → 20261027_night_cs21_fam_p5_r2_s3（pid 8595）
2026-10-26 22:40:08  おわり: 20261027_night_cs21_fam_p5_r0_s3 / completed / 終了コード 0 / 実時間 3.4 分
2026-10-26 22:40:08  始めました（28本目・同時 4）: fam_p1_s4_r2_a42 種3 すれ違い入 → 20261027_night_cs21_fam_p1_r2_s3_passon（pid 8664）
2026-10-26 22:40:13  おわり: 20261027_night_cs21_fam_p1_r2_s3 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-26 22:40:13  始めました（29本目・同時 4）: fam_p1_s4_r0_a42 種4 すれ違い切 → 20261027_night_cs21_fam_p1_r0_s4（pid 8668）
2026-10-26 22:40:48  おわり: 20261027_night_cs21_fam_p5_r1_s3 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-26 22:40:48  始めました（30本目・同時 4）: fam_p1_s4_r1_a42 種4 すれ違い切 → 20261027_night_cs21_fam_p1_r1_s4（pid 8710）
2026-10-26 22:42:23  おわり: 20261027_night_cs21_fam_p5_r2_s3 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-26 22:42:23  始めました（31本目・同時 4）: fam_p1_s4_r2_a42 種4 すれ違い切 → 20261027_night_cs21_fam_p1_r2_s4（pid 8766）
2026-10-26 22:44:13  おわり: 20261027_night_cs21_fam_p1_r0_s4 / completed / 終了コード 0 / 実時間 4.0 分
2026-10-26 22:44:13  始めました（32本目・同時 4）: fam_p5_s4_r0_a42 種4 すれ違い切 → 20261027_night_cs21_fam_p5_r0_s4（pid 8810）
2026-10-26 22:44:18  おわり: 20261027_night_cs21_fam_p1_r2_s3_passon / completed / 終了コード 0 / 実時間 4.2 分
2026-10-26 22:44:18  始めました（33本目・同時 4）: fam_p5_s4_r1_a42 種4 すれ違い切 → 20261027_night_cs21_fam_p5_r1_s4（pid 8819）
2026-10-26 22:44:48  おわり: 20261027_night_cs21_fam_p1_r1_s4 / completed / 終了コード 0 / 実時間 4.0 分
2026-10-26 22:44:48  始めました（34本目・同時 4）: fam_p5_s4_r2_a42 種4 すれ違い切 → 20261027_night_cs21_fam_p5_r2_s4（pid 8826）
2026-10-26 22:46:33  おわり: 20261027_night_cs21_fam_p1_r2_s4 / completed / 終了コード 0 / 実時間 4.2 分
2026-10-26 22:46:33  始めました（35本目・同時 4）: fam_p1_s4_r2_a42 種4 すれ違い入 → 20261027_night_cs21_fam_p1_r2_s4_passon（pid 8875）
2026-10-26 22:48:03  おわり: 20261027_night_cs21_fam_p5_r0_s4 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-26 22:48:03  始めました（36本目・同時 4）: fam_p1_s4_r0_a42 種5 すれ違い切 → 20261027_night_cs21_fam_p1_r0_s5（pid 8900）
2026-10-26 22:48:33  おわり: 20261027_night_cs21_fam_p5_r1_s4 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-26 22:48:33  始めました（37本目・同時 4）: fam_p1_s4_r1_a42 種5 すれ違い切 → 20261027_night_cs21_fam_p1_r1_s5（pid 8907）
2026-10-26 22:49:08  おわり: 20261027_night_cs21_fam_p5_r2_s4 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-26 22:49:08  始めました（38本目・同時 4）: fam_p1_s4_r2_a42 種5 すれ違い切 → 20261027_night_cs21_fam_p1_r2_s5（pid 8918）
2026-10-26 22:50:48  おわり: 20261027_night_cs21_fam_p1_r2_s4_passon / completed / 終了コード 0 / 実時間 4.3 分
2026-10-26 22:50:48  始めました（39本目・同時 4）: fam_p5_s4_r0_a42 種5 すれ違い切 → 20261027_night_cs21_fam_p5_r0_s5（pid 8944）
2026-10-26 22:51:33  おわり: 20261027_night_cs21_fam_p1_r0_s5 / completed / 終了コード 0 / 実時間 3.5 分
2026-10-26 22:51:33  始めました（40本目・同時 4）: fam_p5_s4_r1_a42 種5 すれ違い切 → 20261027_night_cs21_fam_p5_r1_s5（pid 8964）
2026-10-26 22:52:08  おわり: 20261027_night_cs21_fam_p1_r1_s5 / completed / 終了コード 0 / 実時間 3.6 分
2026-10-26 22:52:08  始めました（41本目・同時 4）: fam_p5_s4_r2_a42 種5 すれ違い切 → 20261027_night_cs21_fam_p5_r2_s5（pid 8973）
2026-10-26 22:52:58  おわり: 20261027_night_cs21_fam_p1_r2_s5 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-26 22:52:58  始めました（42本目・同時 4）: fam_p1_s4_r2_a42 種5 すれ違い入 → 20261027_night_cs21_fam_p1_r2_s5_passon（pid 8989）
2026-10-26 22:54:18  おわり: 20261027_night_cs21_fam_p5_r0_s5 / completed / 終了コード 0 / 実時間 3.5 分
2026-10-26 22:54:18  始めました（43本目・同時 4）: fam_p1_s4_r0_a42 種6 すれ違い切 → 20261027_night_cs21_fam_p1_r0_s6（pid 9014）
2026-10-26 22:55:18  おわり: 20261027_night_cs21_fam_p5_r1_s5 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-26 22:55:18  始めました（44本目・同時 4）: fam_p1_s4_r1_a42 種6 すれ違い切 → 20261027_night_cs21_fam_p1_r1_s6（pid 9034）
2026-10-26 22:55:58  おわり: 20261027_night_cs21_fam_p5_r2_s5 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-26 22:55:58  始めました（45本目・同時 4）: fam_p1_s4_r2_a42 種6 すれ違い切 → 20261027_night_cs21_fam_p1_r2_s6（pid 9042）
2026-10-26 22:56:44  おわり: 20261027_night_cs21_fam_p1_r2_s5_passon / completed / 終了コード 0 / 実時間 3.8 分
2026-10-26 22:56:44  始めました（46本目・同時 4）: fam_p5_s4_r0_a42 種6 すれ違い切 → 20261027_night_cs21_fam_p5_r0_s6（pid 9055）
2026-10-26 22:58:39  おわり: 20261027_night_cs21_fam_p1_r0_s6 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-26 22:58:39  始めました（47本目・同時 4）: fam_p5_s4_r1_a42 種6 すれ違い切 → 20261027_night_cs21_fam_p5_r1_s6（pid 9113）
2026-10-26 22:59:54  おわり: 20261027_night_cs21_fam_p1_r1_s6 / completed / 終了コード 0 / 実時間 4.6 分
2026-10-26 22:59:54  始めました（48本目・同時 4）: fam_p5_s4_r2_a42 種6 すれ違い切 → 20261027_night_cs21_fam_p5_r2_s6（pid 9131）
2026-10-26 23:00:39  おわり: 20261027_night_cs21_fam_p1_r2_s6 / completed / 終了コード 0 / 実時間 4.7 分
2026-10-26 23:00:39  始めました（49本目・同時 4）: fam_p1_s4_r2_a42 種6 すれ違い入 → 20261027_night_cs21_fam_p1_r2_s6_passon（pid 9176）
2026-10-26 23:01:04  おわり: 20261027_night_cs21_fam_p5_r0_s6 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-26 23:01:04  始めました（50本目・同時 4）: fam_p1_s4_r0_a42 種7 すれ違い切 → 20261027_night_cs21_fam_p1_r0_s7（pid 9194）
2026-10-26 23:03:24  おわり: 20261027_night_cs21_fam_p5_r1_s6 / completed / 終了コード 0 / 実時間 4.8 分
2026-10-26 23:03:24  始めました（51本目・同時 4）: fam_p1_s4_r1_a42 種7 すれ違い切 → 20261027_night_cs21_fam_p1_r1_s7（pid 9302）
2026-10-26 23:05:04  おわり: 20261027_night_cs21_fam_p5_r2_s6 / completed / 終了コード 0 / 実時間 5.2 分
2026-10-26 23:05:04  おわり: 20261027_night_cs21_fam_p1_r0_s7 / completed / 終了コード 0 / 実時間 4.0 分
2026-10-26 23:05:04  始めました（52本目・同時 3）: fam_p1_s4_r2_a42 種7 すれ違い切 → 20261027_night_cs21_fam_p1_r2_s7（pid 9353）
2026-10-26 23:05:09  始めました（53本目・同時 4）: fam_p5_s4_r0_a42 種7 すれ違い切 → 20261027_night_cs21_fam_p5_r0_s7（pid 9354）
2026-10-26 23:05:39  おわり: 20261027_night_cs21_fam_p1_r2_s6_passon / completed / 終了コード 0 / 実時間 5.0 分
2026-10-26 23:05:39  始めました（54本目・同時 4）: fam_p5_s4_r1_a42 種7 すれ違い切 → 20261027_night_cs21_fam_p5_r1_s7（pid 9361）
2026-10-26 23:07:24  おわり: 20261027_night_cs21_fam_p1_r1_s7 / completed / 終了コード 0 / 実時間 4.0 分
2026-10-26 23:07:24  始めました（55本目・同時 4）: fam_p5_s4_r2_a42 種7 すれ違い切 → 20261027_night_cs21_fam_p5_r2_s7（pid 9398）
2026-10-26 23:08:59  おわり: 20261027_night_cs21_fam_p5_r0_s7 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-26 23:08:59  始めました（56本目・同時 4）: fam_p1_s4_r2_a42 種7 すれ違い入 → 20261027_night_cs21_fam_p1_r2_s7_passon（pid 9477）
2026-10-26 23:09:19  おわり: 20261027_night_cs21_fam_p1_r2_s7 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-26 23:09:19  始めました（57本目・同時 4）: fam_p1_s4_r0_a42 種8 すれ違い切 → 20261027_night_cs21_fam_p1_r0_s8（pid 9492）
2026-10-26 23:09:54  おわり: 20261027_night_cs21_fam_p5_r1_s7 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-26 23:09:54  始めました（58本目・同時 4）: fam_p1_s4_r1_a42 種8 すれ違い切 → 20261027_night_cs21_fam_p1_r1_s8（pid 9501）
2026-10-26 23:11:44  おわり: 20261027_night_cs21_fam_p5_r2_s7 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-26 23:11:44  始めました（59本目・同時 4）: fam_p1_s4_r2_a42 種8 すれ違い切 → 20261027_night_cs21_fam_p1_r2_s8（pid 9568）
2026-10-26 23:12:59  おわり: 20261027_night_cs21_fam_p1_r0_s8 / completed / 終了コード 0 / 実時間 3.7 分
2026-10-26 23:12:59  始めました（60本目・同時 4）: fam_p5_s4_r0_a42 種8 すれ違い切 → 20261027_night_cs21_fam_p5_r0_s8（pid 9625）
2026-10-26 23:13:19  おわり: 20261027_night_cs21_fam_p1_r2_s7_passon / completed / 終了コード 0 / 実時間 4.3 分
2026-10-26 23:13:19  始めました（61本目・同時 4）: fam_p5_s4_r1_a42 種8 すれ違い切 → 20261027_night_cs21_fam_p5_r1_s8（pid 9633）
2026-10-26 23:13:54  おわり: 20261027_night_cs21_fam_p1_r1_s8 / completed / 終了コード 0 / 実時間 4.0 分
2026-10-26 23:13:54  始めました（62本目・同時 4）: fam_p5_s4_r2_a42 種8 すれ違い切 → 20261027_night_cs21_fam_p5_r2_s8（pid 9667）
2026-10-26 23:15:49  おわり: 20261027_night_cs21_fam_p1_r2_s8 / completed / 終了コード 0 / 実時間 4.1 分
2026-10-26 23:15:49  始めました（63本目・同時 4）: fam_p1_s4_r2_a42 種8 すれ違い入 → 20261027_night_cs21_fam_p1_r2_s8_passon（pid 9736）
2026-10-26 23:16:44  おわり: 20261027_night_cs21_fam_p5_r0_s8 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-26 23:16:44  始めました（64本目・同時 4）: fam_p1_s4_r0_a42 種9 すれ違い切 → 20261027_night_cs21_fam_p1_r0_s9（pid 9770）
2026-10-26 23:17:19  おわり: 20261027_night_cs21_fam_p5_r1_s8 / completed / 終了コード 0 / 実時間 4.0 分
2026-10-26 23:17:19  始めました（65本目・同時 4）: fam_p1_s4_r1_a42 種9 すれ違い切 → 20261027_night_cs21_fam_p1_r1_s9（pid 9783）
2026-10-26 23:18:00  おわり: 20261027_night_cs21_fam_p5_r2_s8 / completed / 終了コード 0 / 実時間 4.1 分
2026-10-26 23:18:00  始めました（66本目・同時 4）: fam_p1_s4_r2_a42 種9 すれ違い切 → 20261027_night_cs21_fam_p1_r2_s9（pid 9790）
2026-10-26 23:19:50  おわり: 20261027_night_cs21_fam_p1_r2_s8_passon / completed / 終了コード 0 / 実時間 4.0 分
2026-10-26 23:19:50  始めました（67本目・同時 4）: fam_p5_s4_r0_a42 種9 すれ違い切 → 20261027_night_cs21_fam_p5_r0_s9（pid 9825）
2026-10-26 23:20:45  おわり: 20261027_night_cs21_fam_p1_r0_s9 / completed / 終了コード 0 / 実時間 4.0 分
2026-10-26 23:20:45  始めました（68本目・同時 4）: fam_p5_s4_r1_a42 種9 すれ違い切 → 20261027_night_cs21_fam_p5_r1_s9（pid 9854）
2026-10-26 23:21:25  おわり: 20261027_night_cs21_fam_p1_r1_s9 / completed / 終了コード 0 / 実時間 4.1 分
2026-10-26 23:21:25  始めました（69本目・同時 4）: fam_p5_s4_r2_a42 種9 すれ違い切 → 20261027_night_cs21_fam_p5_r2_s9（pid 9866）
2026-10-26 23:22:10  おわり: 20261027_night_cs21_fam_p1_r2_s9 / completed / 終了コード 0 / 実時間 4.2 分
2026-10-26 23:22:10  始めました（70本目・同時 4）: fam_p1_s4_r2_a42 種9 すれ違い入 → 20261027_night_cs21_fam_p1_r2_s9_passon（pid 9884）
2026-10-26 23:23:45  おわり: 20261027_night_cs21_fam_p5_r0_s9 / completed / 終了コード 0 / 実時間 3.9 分
2026-10-26 23:24:35  おわり: 20261027_night_cs21_fam_p5_r1_s9 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-26 23:25:20  おわり: 20261027_night_cs21_fam_p5_r2_s9 / completed / 終了コード 0 / 実時間 3.9 分
2026-10-26 23:25:45  おわり: 20261027_night_cs21_fam_p1_r2_s9_passon / completed / 終了コード 0 / 実時間 3.6 分
2026-10-26 23:25:45  --- まとめ ---
2026-10-26 23:25:45    completed: 70 回
2026-10-26 23:25:45    completed の実時間: 平均 4.2 分 / 最長 14.4 分
2026-10-26 23:25:45  判定を呼びます（/Volumes/KIOXIA/r2r2r-data/service_sim/unity で）: /opt/homebrew/Caskroom/miniforge/base/bin/python3 /Users/shimazueiichi/Robotics_UX/ServiceSim/tools/judge_night.py --runs 20261027_night_* --out night/20261027
2026-10-26 23:25:45  判定を書きました: /Volumes/KIOXIA/r2r2r-data/service_sim/unity/night/20261027/judge.md
2026-10-26 23:25:45  スリープの設定を戻しました
2026-10-26 23:25:45  === 夜の実行係を終わります（終了コード 0）===
```

### night/20261027/judge.md

```markdown
# 夜の結果の振り分け（20261027_night_*）

基準データ: 20261025_stageB_cs7_*（18回）／振り分けた回: 70回

| 振り分け | 回数 |
|---|---|
| 正常 | 30 |
| 試算を見直す候補 | 0 |
| Unity の作り | 30 |
| 回の失敗 | 1 |
| 基準なし | 9 |

## 回の失敗

- 20261027_night_cs21_fam_p1_r2_s0_passon: 全員が帰っていない

## 基準なし（基準データに同じ条件がないので判定していない）

- fam_p1 スタッフ4 ロボット2台・すれ違い入

## Unity の作り（種の中央値が、基準の同じ条件の最大を超えた）

- fam_p1 スタッフ4 ロボット1台: 30秒以上の止まり（1往復あたり） 0.203（基準 0.083〜0.182）
- fam_p5 スタッフ4 ロボット1台: 30秒以上の止まり（1往復あたり） 0.152（基準 0.075〜0.119）
- fam_p5 スタッフ4 ロボット1台: 動けなくなった（60秒） 6.5（基準 3.0〜3.0）
- fam_p5 スタッフ4 ロボット2台: 瞬間移動 69.0（基準 47.0〜59.0）

## 試算を見直す候補

### ロボットを増やしたときの向きが、種の過半数で試算と逆

- なし

### Unity÷試算 の大きさが、基準の同じ条件の範囲から外れた

- fam_p5 スタッフ4 ロボット1台: 料理の待ち（Unity÷試算） 1.24倍（基準 0.95〜1.22倍）
- fam_p5 スタッフ4 ロボット2台: 料理の待ち（Unity÷試算） 1.4倍（基準 1.52〜1.57倍）

## 記録（印にしない）：試算との差の大きさ（条件ごとの種の中央値・Unity÷試算）

| 条件 | 種の数 | 入店待ち | 料理の待ち | 席は空いているのに待った |
|---|---|---|---|---|
| fam_p1 スタッフ4 ロボット0台 | 10 | 1.24 | 2.05 | 3.01 |
| fam_p1 スタッフ4 ロボット2台・すれ違い入 | 9 | 5.25 | 1.62 | 10.89 |
| fam_p1 スタッフ4 ロボット1台 | 10 | 1.76 | 1.25 | 3.22 |
| fam_p1 スタッフ4 ロボット2台 | 10 | 2.58 | 1.57 | 3.54 |
| fam_p5 スタッフ4 ロボット0台 | 10 | 1.53 | 2.19 | 3.39 |
| fam_p5 スタッフ4 ロボット1台 | 10 | 2.75 | 1.24 | 6.53 |
| fam_p5 スタッフ4 ロボット2台 | 10 | 1.47 | 1.40 | 2.72 |

## すれ違い 入 と 切 の比べ（同じ夜・同じ条件・種の中央値。印にしない）

| 条件 | 入店待ち | 料理の待ち | 席は空いているのに待った | 動けなくなった | あきらめた | ロボットが運んだ割合 |
|---|---|---|---|---|---|---|
| fam_p1 スタッフ4 ロボット2台（10回） | 3.04 | 1.49 | 1.36 | 20.50 | 59.50 | 0.93 |
| fam_p1 スタッフ4 ロボット2台・すれ違い入（9回） | 6.19 | 1.54 | 4.20 | 23.00 | 66.00 | 0.91 |

線: 立ち往生 30秒以上の止まり／向きは 5%・0.2分より小さい変化を数えない／外れは基準の同じ条件の最小〜最大
```

### night/20261027/runs.csv

```csv
folder,condition,seed,pass_by,tryno,launch_running,parallel,started_iso,ended_iso,started_unix,ended_unix,started_mono,ended_mono,wall_s,status,exit_code
20261027_night_cs21_fam_p1_r0_s0,fam_p1_s4_r0_a42,0,off,1,1,4,2026-10-26T22:11:01,2026-10-26T22:14:21,1793020261.620,1793020461.753,246064.959,246265.092,200.133,completed,0
20261027_night_cs21_fam_p1_r1_s0,fam_p1_s4_r1_a42,0,off,1,2,4,2026-10-26T22:11:06,2026-10-26T22:14:41,1793020266.624,1793020481.766,246069.963,246285.105,215.143,completed,0
20261027_night_cs21_fam_p5_r0_s0,fam_p5_s4_r0_a42,0,off,1,4,4,2026-10-26T22:11:16,2026-10-26T22:14:46,1793020276.636,1793020486.777,246079.976,246290.116,210.141,completed,0
20261027_night_cs21_fam_p1_r2_s0,fam_p1_s4_r2_a42,0,off,1,3,4,2026-10-26T22:11:11,2026-10-26T22:15:01,1793020271.629,1793020501.796,246074.969,246305.135,230.167,completed,0
20261027_night_cs21_fam_p5_r1_s0,fam_p5_s4_r1_a42,0,off,1,4,4,2026-10-26T22:14:21,2026-10-26T22:18:11,1793020461.840,1793020691.998,246265.101,246495.258,230.158,completed,0
20261027_night_cs21_fam_p5_r2_s0,fam_p5_s4_r2_a42,0,off,1,4,4,2026-10-26T22:14:41,2026-10-26T22:18:47,1793020481.855,1793020727.019,246285.114,246530.279,245.165,completed,0
20261027_night_cs21_fam_p1_r0_s1,fam_p1_s4_r0_a42,1,off,1,4,4,2026-10-26T22:15:01,2026-10-26T22:19:02,1793020501.882,1793020742.034,246305.141,246545.293,240.152,completed,0
20261027_night_cs21_fam_p1_r1_s1,fam_p1_s4_r1_a42,1,off,1,4,4,2026-10-26T22:18:12,2026-10-26T22:22:12,1793020692.011,1793020932.157,246495.265,246735.410,240.146,completed,0
20261027_night_cs21_fam_p5_r0_s1,fam_p5_s4_r0_a42,1,off,1,4,4,2026-10-26T22:19:02,2026-10-26T22:23:07,1793020742.047,1793020987.201,246545.299,246790.452,245.153,completed,0
20261027_night_cs21_fam_p1_r2_s1,fam_p1_s4_r2_a42,1,off,1,4,4,2026-10-26T22:18:47,2026-10-26T22:23:12,1793020727.034,1793020992.214,246530.286,246795.467,265.180,completed,0
20261027_night_cs21_fam_p5_r1_s1,fam_p5_s4_r1_a42,1,off,1,4,4,2026-10-26T22:22:12,2026-10-26T22:26:02,1793020932.169,1793021162.338,246735.415,246965.584,230.169,completed,0
20261027_night_cs21_fam_p1_r2_s1_passon,fam_p1_s4_r2_a42,1,on,1,4,4,2026-10-26T22:23:12,2026-10-26T22:27:07,1793020992.229,1793021227.385,246795.473,247030.629,235.156,completed,0
20261027_night_cs21_fam_p5_r2_s1,fam_p5_s4_r2_a42,1,off,1,4,4,2026-10-26T22:23:07,2026-10-26T22:27:17,1793020987.219,1793021237.397,246790.463,247040.641,250.178,completed,0
20261027_night_cs21_fam_p1_r2_s0_passon,fam_p1_s4_r2_a42,0,on,1,4,4,2026-10-26T22:14:46,2026-10-26T22:29:12,1793020486.861,1793021352.458,246290.121,247155.718,865.597,completed,0
20261027_night_cs21_fam_p1_r0_s2,fam_p1_s4_r0_a42,2,off,1,4,4,2026-10-26T22:26:02,2026-10-26T22:30:22,1793021162.352,1793021422.533,246965.591,247225.772,260.182,completed,0
20261027_night_cs21_fam_p1_r1_s2,fam_p1_s4_r1_a42,2,off,1,4,4,2026-10-26T22:27:07,2026-10-26T22:31:37,1793021227.398,1793021497.588,247030.635,247300.826,270.191,completed,0
20261027_night_cs21_fam_p1_r2_s2,fam_p1_s4_r2_a42,2,off,1,4,4,2026-10-26T22:27:17,2026-10-26T22:32:07,1793021237.410,1793021527.613,247040.647,247330.850,290.204,completed,0
20261027_night_cs21_fam_p5_r0_s2,fam_p5_s4_r0_a42,2,off,1,4,4,2026-10-26T22:29:12,2026-10-26T22:33:32,1793021352.493,1793021612.686,247155.727,247415.920,260.193,completed,0
20261027_night_cs21_fam_p5_r1_s2,fam_p5_s4_r1_a42,2,off,1,4,4,2026-10-26T22:30:22,2026-10-26T22:34:52,1793021422.545,1793021692.769,247225.778,247496.002,270.224,completed,0
20261027_night_cs21_fam_p5_r2_s2,fam_p5_s4_r2_a42,2,off,1,4,4,2026-10-26T22:31:37,2026-10-26T22:36:27,1793021497.599,1793021787.869,247300.831,247591.101,290.270,completed,0
20261027_night_cs21_fam_p1_r2_s2_passon,fam_p1_s4_r2_a42,2,on,1,4,4,2026-10-26T22:32:07,2026-10-26T22:36:42,1793021527.625,1793021802.891,247330.855,247606.122,275.266,completed,0
20261027_night_cs21_fam_p1_r0_s3,fam_p1_s4_r0_a42,3,off,1,4,4,2026-10-26T22:33:32,2026-10-26T22:36:57,1793021612.699,1793021817.914,247415.927,247621.142,205.215,completed,0
20261027_night_cs21_fam_p1_r1_s3,fam_p1_s4_r1_a42,3,off,1,4,4,2026-10-26T22:34:52,2026-10-26T22:38:33,1793021692.782,1793021913.019,247496.008,247716.245,220.237,completed,0
20261027_night_cs21_fam_p5_r0_s3,fam_p5_s4_r0_a42,3,off,1,4,4,2026-10-26T22:36:42,2026-10-26T22:40:08,1793021802.904,1793022008.130,247606.127,247811.352,205.226,completed,0
20261027_night_cs21_fam_p1_r2_s3,fam_p1_s4_r2_a42,3,off,1,4,4,2026-10-26T22:36:27,2026-10-26T22:40:13,1793021787.884,1793022013.145,247591.107,247816.368,225.261,completed,0
20261027_night_cs21_fam_p5_r1_s3,fam_p5_s4_r1_a42,3,off,1,4,4,2026-10-26T22:36:57,2026-10-26T22:40:48,1793021817.924,1793022048.179,247621.146,247851.401,230.255,completed,0
20261027_night_cs21_fam_p5_r2_s3,fam_p5_s4_r2_a42,3,off,1,4,4,2026-10-26T22:38:33,2026-10-26T22:42:23,1793021913.031,1793022143.226,247716.250,247946.445,230.195,completed,0
20261027_night_cs21_fam_p1_r0_s4,fam_p1_s4_r0_a42,4,off,1,4,4,2026-10-26T22:40:13,2026-10-26T22:44:13,1793022013.155,1793022253.294,247816.371,248056.511,240.139,completed,0
20261027_night_cs21_fam_p1_r2_s3_passon,fam_p1_s4_r2_a42,3,on,1,4,4,2026-10-26T22:40:08,2026-10-26T22:44:18,1793022008.142,1793022258.308,247811.359,248061.524,250.165,completed,0
20261027_night_cs21_fam_p1_r1_s4,fam_p1_s4_r1_a42,4,off,1,4,4,2026-10-26T22:40:48,2026-10-26T22:44:48,1793022048.190,1793022288.329,247851.406,248091.544,240.138,completed,0
20261027_night_cs21_fam_p1_r2_s4,fam_p1_s4_r2_a42,4,off,1,4,4,2026-10-26T22:42:23,2026-10-26T22:46:33,1793022143.242,1793022393.386,247946.455,248196.598,250.143,completed,0
20261027_night_cs21_fam_p5_r0_s4,fam_p5_s4_r0_a42,4,off,1,4,4,2026-10-26T22:44:13,2026-10-26T22:48:03,1793022253.310,1793022483.454,248056.519,248286.663,230.144,completed,0
20261027_night_cs21_fam_p5_r1_s4,fam_p5_s4_r1_a42,4,off,1,4,4,2026-10-26T22:44:18,2026-10-26T22:48:33,1793022258.318,1793022513.487,248061.527,248316.696,255.169,completed,0
20261027_night_cs21_fam_p5_r2_s4,fam_p5_s4_r2_a42,4,off,1,4,4,2026-10-26T22:44:48,2026-10-26T22:49:08,1793022288.340,1793022548.525,248091.549,248351.734,260.185,completed,0
20261027_night_cs21_fam_p1_r2_s4_passon,fam_p1_s4_r2_a42,4,on,1,4,4,2026-10-26T22:46:33,2026-10-26T22:50:48,1793022393.367,1793022648.597,248196.604,248451.833,255.229,completed,0
20261027_night_cs21_fam_p1_r0_s5,fam_p1_s4_r0_a42,5,off,1,4,4,2026-10-26T22:48:03,2026-10-26T22:51:33,1793022483.432,1793022693.651,248286.669,248496.888,210.219,completed,0
20261027_night_cs21_fam_p1_r1_s5,fam_p1_s4_r1_a42,5,off,1,4,4,2026-10-26T22:48:33,2026-10-26T22:52:08,1793022513.466,1793022728.691,248316.702,248531.927,215.225,completed,0
20261027_night_cs21_fam_p1_r2_s5,fam_p1_s4_r2_a42,5,off,1,4,4,2026-10-26T22:49:08,2026-10-26T22:52:58,1793022548.503,1793022778.740,248351.739,248581.976,230.237,completed,0
20261027_night_cs21_fam_p5_r0_s5,fam_p5_s4_r0_a42,5,off,1,4,4,2026-10-26T22:50:48,2026-10-26T22:54:18,1793022648.605,1793022858.826,248451.839,248662.061,210.221,completed,0
20261027_night_cs21_fam_p5_r1_s5,fam_p5_s4_r1_a42,5,off,1,4,4,2026-10-26T22:51:33,2026-10-26T22:55:18,1793022693.659,1793022918.894,248496.893,248722.128,225.235,completed,0
20261027_night_cs21_fam_p5_r2_s5,fam_p5_s4_r2_a42,5,off,1,4,4,2026-10-26T22:52:08,2026-10-26T22:55:58,1793022728.698,1793022958.947,248531.932,248762.180,230.248,completed,0
20261027_night_cs21_fam_p1_r2_s5_passon,fam_p1_s4_r2_a42,5,on,1,4,4,2026-10-26T22:52:58,2026-10-26T22:56:44,1793022778.748,1793023004.003,248581.981,248807.236,225.255,completed,0
20261027_night_cs21_fam_p1_r0_s6,fam_p1_s4_r0_a42,6,off,1,4,4,2026-10-26T22:54:18,2026-10-26T22:58:39,1793022858.837,1793023119.113,248662.069,248922.345,260.276,completed,0
20261027_night_cs21_fam_p1_r1_s6,fam_p1_s4_r1_a42,6,off,1,4,4,2026-10-26T22:55:18,2026-10-26T22:59:54,1793022918.902,1793023194.194,248722.134,248997.425,275.291,completed,0
20261027_night_cs21_fam_p1_r2_s6,fam_p1_s4_r2_a42,6,off,1,4,4,2026-10-26T22:55:58,2026-10-26T23:00:39,1793022958.955,1793023239.234,248762.186,249042.465,280.279,completed,0
20261027_night_cs21_fam_p5_r0_s6,fam_p5_s4_r0_a42,6,off,1,4,4,2026-10-26T22:56:44,2026-10-26T23:01:04,1793023004.011,1793023264.250,248807.242,249067.480,260.239,completed,0
20261027_night_cs21_fam_p5_r1_s6,fam_p5_s4_r1_a42,6,off,1,4,4,2026-10-26T22:58:39,2026-10-26T23:03:24,1793023119.122,1793023404.358,248922.351,249207.587,285.236,completed,0
20261027_night_cs21_fam_p5_r2_s6,fam_p5_s4_r2_a42,6,off,1,4,4,2026-10-26T22:59:54,2026-10-26T23:05:04,1793023194.203,1793023504.429,248997.432,249307.658,310.226,completed,0
20261027_night_cs21_fam_p1_r0_s7,fam_p1_s4_r0_a42,7,off,1,4,4,2026-10-26T23:01:04,2026-10-26T23:05:04,1793023264.257,1793023504.430,249067.485,249307.658,240.173,completed,0
20261027_night_cs21_fam_p1_r2_s6_passon,fam_p1_s4_r2_a42,6,on,1,4,4,2026-10-26T23:00:39,2026-10-26T23:05:39,1793023239.245,1793023539.461,249042.473,249342.689,300.217,completed,0
20261027_night_cs21_fam_p1_r1_s7,fam_p1_s4_r1_a42,7,off,1,4,4,2026-10-26T23:03:24,2026-10-26T23:07:24,1793023404.383,1793023644.516,249207.609,249447.742,240.134,completed,0
20261027_night_cs21_fam_p5_r0_s7,fam_p5_s4_r0_a42,7,off,1,4,4,2026-10-26T23:05:09,2026-10-26T23:08:59,1793023509.443,1793023739.586,249312.668,249542.811,230.143,completed,0
20261027_night_cs21_fam_p1_r2_s7,fam_p1_s4_r2_a42,7,off,1,3,4,2026-10-26T23:05:04,2026-10-26T23:09:19,1793023504.440,1793023759.614,249307.665,249562.839,255.173,completed,0
20261027_night_cs21_fam_p5_r1_s7,fam_p5_s4_r1_a42,7,off,1,4,4,2026-10-26T23:05:39,2026-10-26T23:09:54,1793023539.470,1793023794.643,249342.695,249597.868,255.173,completed,0
20261027_night_cs21_fam_p5_r2_s7,fam_p5_s4_r2_a42,7,off,1,4,4,2026-10-26T23:07:24,2026-10-26T23:11:44,1793023644.524,1793023904.718,249447.748,249707.941,260.194,completed,0
20261027_night_cs21_fam_p1_r0_s8,fam_p1_s4_r0_a42,8,off,1,4,4,2026-10-26T23:09:19,2026-10-26T23:12:59,1793023759.622,1793023979.783,249562.845,249783.005,220.160,completed,0
20261027_night_cs21_fam_p1_r2_s7_passon,fam_p1_s4_r2_a42,7,on,1,4,4,2026-10-26T23:08:59,2026-10-26T23:13:19,1793023739.595,1793023999.806,249542.817,249803.028,260.211,completed,0
20261027_night_cs21_fam_p1_r1_s8,fam_p1_s4_r1_a42,8,off,1,4,4,2026-10-26T23:09:54,2026-10-26T23:13:54,1793023794.653,1793024034.834,249597.874,249838.056,240.182,completed,0
20261027_night_cs21_fam_p1_r2_s8,fam_p1_s4_r2_a42,8,off,1,4,4,2026-10-26T23:11:44,2026-10-26T23:15:49,1793023904.728,1793024149.916,249707.948,249953.137,245.188,completed,0
20261027_night_cs21_fam_p5_r0_s8,fam_p5_s4_r0_a42,8,off,1,4,4,2026-10-26T23:12:59,2026-10-26T23:16:44,1793023979.796,1793024204.964,249783.016,250008.184,225.168,completed,0
20261027_night_cs21_fam_p5_r1_s8,fam_p5_s4_r1_a42,8,off,1,4,4,2026-10-26T23:13:19,2026-10-26T23:17:19,1793023999.813,1793024239.989,249803.033,250043.208,240.175,completed,0
20261027_night_cs21_fam_p5_r2_s8,fam_p5_s4_r2_a42,8,off,1,4,4,2026-10-26T23:13:54,2026-10-26T23:18:00,1793024034.842,1793024280.016,249838.061,250083.235,245.174,completed,0
20261027_night_cs21_fam_p1_r2_s8_passon,fam_p1_s4_r2_a42,8,on,1,4,4,2026-10-26T23:15:49,2026-10-26T23:19:50,1793024149.928,1793024390.090,249953.146,250193.308,240.162,completed,0
20261027_night_cs21_fam_p1_r0_s9,fam_p1_s4_r0_a42,9,off,1,4,4,2026-10-26T23:16:44,2026-10-26T23:20:45,1793024204.972,1793024445.132,250008.190,250248.349,240.160,completed,0
20261027_night_cs21_fam_p1_r1_s9,fam_p1_s4_r1_a42,9,off,1,4,4,2026-10-26T23:17:19,2026-10-26T23:21:25,1793024239.997,1793024485.156,250043.213,250288.373,245.159,completed,0
20261027_night_cs21_fam_p1_r2_s9,fam_p1_s4_r2_a42,9,off,1,4,4,2026-10-26T23:18:00,2026-10-26T23:22:10,1793024280.013,1793024530.176,250083.241,250333.404,250.163,completed,0
20261027_night_cs21_fam_p5_r0_s9,fam_p5_s4_r0_a42,9,off,1,4,4,2026-10-26T23:19:50,2026-10-26T23:23:45,1793024390.052,1793024625.194,250193.316,250428.457,235.142,completed,0
20261027_night_cs21_fam_p5_r1_s9,fam_p5_s4_r1_a42,9,off,1,4,4,2026-10-26T23:20:45,2026-10-26T23:24:35,1793024445.092,1793024675.228,250248.356,250478.492,230.136,completed,0
20261027_night_cs21_fam_p5_r2_s9,fam_p5_s4_r2_a42,9,off,1,4,4,2026-10-26T23:21:25,2026-10-26T23:25:20,1793024485.115,1793024720.269,250288.379,250523.533,235.154,completed,0
20261027_night_cs21_fam_p1_r2_s9_passon,fam_p1_s4_r2_a42,9,on,1,4,4,2026-10-26T23:22:10,2026-10-26T23:25:45,1793024530.145,1793024745.291,250333.409,250548.555,215.146,completed,0
```

### night/20261028（2回目の夜・cs:25・70回）

### night/20261028/runner_log.txt

```text
2026-10-27 08:33:59  === 夜の実行係を始めます（night 20261028・同時 4 本・上限 90.0 分）===
2026-10-27 08:33:59  並べた回: queue.json（70 回）
2026-10-27 08:33:59  台帳: /Volumes/KIOXIA/r2r2r-data/service_sim/unity/unity_ledger_v1.csv（59 列）
2026-10-27 08:33:59  版: ServiceSim/cs:25（Builds/headless/build_info.json）→ 出力フォルダの字 cs25
2026-10-27 08:33:59  並べた回の試算の版: v2.8 / 店の形の版: 8a5b2f6a016a
2026-10-27 08:33:59  スリープを止めました（caffeinate pid 22110）
2026-10-27 08:33:59  並べた回を写しました: /Volumes/KIOXIA/r2r2r-data/service_sim/unity/night/20261028/queue.json
2026-10-27 08:33:59  並べた回 70 / もう completed の回を飛ばす 0 / 回す 70
2026-10-27 08:33:59  始めました（1本目・同時 1）: fam_p1_s4_r0_a42 種0 すれ違い切 → 20261028_night_cs25_fam_p1_r0_s0（pid 22111）
2026-10-27 08:34:04  始めました（2本目・同時 2）: fam_p1_s4_r1_a42 種0 すれ違い切 → 20261028_night_cs25_fam_p1_r1_s0（pid 22130）
2026-10-27 08:34:09  始めました（3本目・同時 3）: fam_p1_s4_r2_a42 種0 すれ違い切 → 20261028_night_cs25_fam_p1_r2_s0（pid 22131）
2026-10-27 08:34:14  始めました（4本目・同時 4）: fam_p5_s4_r0_a42 種0 すれ違い切 → 20261028_night_cs25_fam_p5_r0_s0（pid 22145）
2026-10-27 08:37:24  おわり: 20261028_night_cs25_fam_p1_r0_s0 / completed / 終了コード 0 / 実時間 3.4 分
2026-10-27 08:37:24  始めました（5本目・同時 4）: fam_p5_s4_r1_a42 種0 すれ違い切 → 20261028_night_cs25_fam_p5_r1_s0（pid 22227）
2026-10-27 08:37:39  おわり: 20261028_night_cs25_fam_p1_r1_s0 / completed / 終了コード 0 / 実時間 3.6 分
2026-10-27 08:37:39  おわり: 20261028_night_cs25_fam_p5_r0_s0 / completed / 終了コード 0 / 実時間 3.4 分
2026-10-27 08:37:39  始めました（6本目・同時 3）: fam_p5_s4_r2_a42 種0 すれ違い切 → 20261028_night_cs25_fam_p5_r2_s0（pid 22238）
2026-10-27 08:37:44  始めました（7本目・同時 4）: fam_p1_s4_r2_a42 種0 すれ違い入 → 20261028_night_cs25_fam_p1_r2_s0_passon（pid 22240）
2026-10-27 08:37:49  おわり: 20261028_night_cs25_fam_p1_r2_s0 / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 08:37:49  始めました（8本目・同時 4）: fam_p1_s4_r0_a42 種1 すれ違い切 → 20261028_night_cs25_fam_p1_r0_s1（pid 22241）
2026-10-27 08:40:59  おわり: 20261028_night_cs25_fam_p5_r1_s0 / completed / 終了コード 0 / 実時間 3.6 分
2026-10-27 08:40:59  始めました（9本目・同時 4）: fam_p1_s4_r1_a42 種1 すれ違い切 → 20261028_night_cs25_fam_p1_r1_s1（pid 22304）
2026-10-27 08:41:19  おわり: 20261028_night_cs25_fam_p5_r2_s0 / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 08:41:19  始めました（10本目・同時 4）: fam_p1_s4_r2_a42 種1 すれ違い切 → 20261028_night_cs25_fam_p1_r2_s1（pid 22305）
2026-10-27 08:41:24  おわり: 20261028_night_cs25_fam_p1_r2_s0_passon / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 08:41:24  始めました（11本目・同時 4）: fam_p5_s4_r0_a42 種1 すれ違い切 → 20261028_night_cs25_fam_p5_r0_s1（pid 22307）
2026-10-27 08:41:29  おわり: 20261028_night_cs25_fam_p1_r0_s1 / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 08:41:29  始めました（12本目・同時 4）: fam_p5_s4_r1_a42 種1 すれ違い切 → 20261028_night_cs25_fam_p5_r1_s1（pid 22310）
2026-10-27 08:44:55  おわり: 20261028_night_cs25_fam_p1_r1_s1 / completed / 終了コード 0 / 実時間 3.9 分
2026-10-27 08:44:55  始めました（13本目・同時 4）: fam_p5_s4_r2_a42 種1 すれ違い切 → 20261028_night_cs25_fam_p5_r2_s1（pid 22420）
2026-10-27 08:45:15  おわり: 20261028_night_cs25_fam_p5_r0_s1 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 08:45:15  始めました（14本目・同時 4）: fam_p1_s4_r2_a42 種1 すれ違い入 → 20261028_night_cs25_fam_p1_r2_s1_passon（pid 22433）
2026-10-27 08:45:25  おわり: 20261028_night_cs25_fam_p1_r2_s1 / completed / 終了コード 0 / 実時間 4.1 分
2026-10-27 08:45:25  始めました（15本目・同時 4）: fam_p1_s4_r0_a42 種2 すれ違い切 → 20261028_night_cs25_fam_p1_r0_s2（pid 22434）
2026-10-27 08:45:30  おわり: 20261028_night_cs25_fam_p5_r1_s1 / completed / 終了コード 0 / 実時間 4.0 分
2026-10-27 08:45:30  始めました（16本目・同時 4）: fam_p1_s4_r1_a42 種2 すれ違い切 → 20261028_night_cs25_fam_p1_r1_s2（pid 22443）
2026-10-27 08:49:10  おわり: 20261028_night_cs25_fam_p5_r2_s1 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-27 08:49:10  おわり: 20261028_night_cs25_fam_p1_r2_s1_passon / completed / 終了コード 0 / 実時間 3.9 分
2026-10-27 08:49:10  始めました（17本目・同時 3）: fam_p1_s4_r2_a42 種2 すれ違い切 → 20261028_night_cs25_fam_p1_r2_s2（pid 22556）
2026-10-27 08:49:15  始めました（18本目・同時 4）: fam_p5_s4_r0_a42 種2 すれ違い切 → 20261028_night_cs25_fam_p5_r0_s2（pid 22559）
2026-10-27 08:49:45  おわり: 20261028_night_cs25_fam_p1_r0_s2 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-27 08:49:45  始めました（19本目・同時 4）: fam_p5_s4_r1_a42 種2 すれ違い切 → 20261028_night_cs25_fam_p5_r1_s2（pid 22570）
2026-10-27 08:50:05  おわり: 20261028_night_cs25_fam_p1_r1_s2 / completed / 終了コード 0 / 実時間 4.6 分
2026-10-27 08:50:05  始めました（20本目・同時 4）: fam_p5_s4_r2_a42 種2 すれ違い切 → 20261028_night_cs25_fam_p5_r2_s2（pid 22580）
2026-10-27 08:53:35  おわり: 20261028_night_cs25_fam_p5_r0_s2 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-27 08:53:35  始めました（21本目・同時 4）: fam_p1_s4_r2_a42 種2 すれ違い入 → 20261028_night_cs25_fam_p1_r2_s2_passon（pid 22667）
2026-10-27 08:54:05  おわり: 20261028_night_cs25_fam_p1_r2_s2 / completed / 終了コード 0 / 実時間 4.9 分
2026-10-27 08:54:05  始めました（22本目・同時 4）: fam_p1_s4_r0_a42 種3 すれ違い切 → 20261028_night_cs25_fam_p1_r0_s3（pid 22689）
2026-10-27 08:54:30  おわり: 20261028_night_cs25_fam_p5_r1_s2 / completed / 終了コード 0 / 実時間 4.8 分
2026-10-27 08:54:30  始めました（23本目・同時 4）: fam_p1_s4_r1_a42 種3 すれ違い切 → 20261028_night_cs25_fam_p1_r1_s3（pid 22704）
2026-10-27 08:54:55  おわり: 20261028_night_cs25_fam_p5_r2_s2 / completed / 終了コード 0 / 実時間 4.8 分
2026-10-27 08:54:55  始めました（24本目・同時 4）: fam_p1_s4_r2_a42 種3 すれ違い切 → 20261028_night_cs25_fam_p1_r2_s3（pid 22713）
2026-10-27 08:57:35  おわり: 20261028_night_cs25_fam_p1_r0_s3 / completed / 終了コード 0 / 実時間 3.5 分
2026-10-27 08:57:35  始めました（25本目・同時 4）: fam_p5_s4_r0_a42 種3 すれ違い切 → 20261028_night_cs25_fam_p5_r0_s3（pid 22760）
2026-10-27 08:58:05  おわり: 20261028_night_cs25_fam_p1_r1_s3 / completed / 終了コード 0 / 実時間 3.6 分
2026-10-27 08:58:05  始めました（26本目・同時 4）: fam_p5_s4_r1_a42 種3 すれ違い切 → 20261028_night_cs25_fam_p5_r1_s3（pid 22774）
2026-10-27 08:58:25  おわり: 20261028_night_cs25_fam_p1_r2_s2_passon / completed / 終了コード 0 / 実時間 4.8 分
2026-10-27 08:58:25  始めました（27本目・同時 4）: fam_p5_s4_r2_a42 種3 すれ違い切 → 20261028_night_cs25_fam_p5_r2_s3（pid 22783）
2026-10-27 08:58:50  おわり: 20261028_night_cs25_fam_p1_r2_s3 / completed / 終了コード 0 / 実時間 3.9 分
2026-10-27 08:58:50  始めました（28本目・同時 4）: fam_p1_s4_r2_a42 種3 すれ違い入 → 20261028_night_cs25_fam_p1_r2_s3_passon（pid 22798）
2026-10-27 09:01:05  おわり: 20261028_night_cs25_fam_p5_r0_s3 / completed / 終了コード 0 / 実時間 3.5 分
2026-10-27 09:01:05  始めました（29本目・同時 4）: fam_p1_s4_r0_a42 種4 すれ違い切 → 20261028_night_cs25_fam_p1_r0_s4（pid 22871）
2026-10-27 09:02:00  おわり: 20261028_night_cs25_fam_p5_r1_s3 / completed / 終了コード 0 / 実時間 3.9 分
2026-10-27 09:02:00  始めました（30本目・同時 4）: fam_p1_s4_r1_a42 種4 すれ違い切 → 20261028_night_cs25_fam_p1_r1_s4（pid 22911）
2026-10-27 09:02:15  おわり: 20261028_night_cs25_fam_p5_r2_s3 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 09:02:15  始めました（31本目・同時 4）: fam_p1_s4_r2_a42 種4 すれ違い切 → 20261028_night_cs25_fam_p1_r2_s4（pid 22918）
2026-10-27 09:02:45  おわり: 20261028_night_cs25_fam_p1_r2_s3_passon / completed / 終了コード 0 / 実時間 3.9 分
2026-10-27 09:02:45  始めました（32本目・同時 4）: fam_p5_s4_r0_a42 種4 すれ違い切 → 20261028_night_cs25_fam_p5_r0_s4（pid 22937）
2026-10-27 09:05:15  おわり: 20261028_night_cs25_fam_p1_r0_s4 / completed / 終了コード 0 / 実時間 4.2 分
2026-10-27 09:05:15  始めました（33本目・同時 4）: fam_p5_s4_r1_a42 種4 すれ違い切 → 20261028_night_cs25_fam_p5_r1_s4（pid 23014）
2026-10-27 09:06:30  おわり: 20261028_night_cs25_fam_p1_r1_s4 / completed / 終了コード 0 / 実時間 4.5 分
2026-10-27 09:06:30  始めました（34本目・同時 4）: fam_p5_s4_r2_a42 種4 すれ違い切 → 20261028_night_cs25_fam_p5_r2_s4（pid 23053）
2026-10-27 09:06:45  おわり: 20261028_night_cs25_fam_p1_r2_s4 / completed / 終了コード 0 / 実時間 4.5 分
2026-10-27 09:06:45  始めました（35本目・同時 4）: fam_p1_s4_r2_a42 種4 すれ違い入 → 20261028_night_cs25_fam_p1_r2_s4_passon（pid 23065）
2026-10-27 09:06:55  おわり: 20261028_night_cs25_fam_p5_r0_s4 / completed / 終了コード 0 / 実時間 4.2 分
2026-10-27 09:06:55  始めました（36本目・同時 4）: fam_p1_s4_r0_a42 種5 すれ違い切 → 20261028_night_cs25_fam_p1_r0_s5（pid 23073）
2026-10-27 09:09:36  おわり: 20261028_night_cs25_fam_p5_r1_s4 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-27 09:09:36  始めました（37本目・同時 4）: fam_p1_s4_r1_a42 種5 すれ違い切 → 20261028_night_cs25_fam_p1_r1_s5（pid 23158）
2026-10-27 09:10:26  おわり: 20261028_night_cs25_fam_p1_r0_s5 / completed / 終了コード 0 / 実時間 3.5 分
2026-10-27 09:10:26  始めました（38本目・同時 4）: fam_p1_s4_r2_a42 種5 すれ違い切 → 20261028_night_cs25_fam_p1_r2_s5（pid 23191）
2026-10-27 09:11:06  おわり: 20261028_night_cs25_fam_p5_r2_s4 / completed / 終了コード 0 / 実時間 4.6 分
2026-10-27 09:11:06  始めました（39本目・同時 4）: fam_p5_s4_r0_a42 種5 すれ違い切 → 20261028_night_cs25_fam_p5_r0_s5（pid 23212）
2026-10-27 09:11:21  おわり: 20261028_night_cs25_fam_p1_r2_s4_passon / completed / 終了コード 0 / 実時間 4.6 分
2026-10-27 09:11:21  始めました（40本目・同時 4）: fam_p5_s4_r1_a42 種5 すれ違い切 → 20261028_night_cs25_fam_p5_r1_s5（pid 23217）
2026-10-27 09:13:21  おわり: 20261028_night_cs25_fam_p1_r1_s5 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 09:13:21  始めました（41本目・同時 4）: fam_p5_s4_r2_a42 種5 すれ違い切 → 20261028_night_cs25_fam_p5_r2_s5（pid 23269）
2026-10-27 09:14:21  おわり: 20261028_night_cs25_fam_p1_r2_s5 / completed / 終了コード 0 / 実時間 3.9 分
2026-10-27 09:14:21  始めました（42本目・同時 4）: fam_p1_s4_r2_a42 種5 すれ違い入 → 20261028_night_cs25_fam_p1_r2_s5_passon（pid 23311）
2026-10-27 09:14:31  おわり: 20261028_night_cs25_fam_p5_r0_s5 / completed / 終了コード 0 / 実時間 3.4 分
2026-10-27 09:14:31  始めました（43本目・同時 4）: fam_p1_s4_r0_a42 種6 すれ違い切 → 20261028_night_cs25_fam_p1_r0_s6（pid 23314）
2026-10-27 09:15:01  おわり: 20261028_night_cs25_fam_p5_r1_s5 / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 09:15:01  始めました（44本目・同時 4）: fam_p1_s4_r1_a42 種6 すれ違い切 → 20261028_night_cs25_fam_p1_r1_s6（pid 23329）
2026-10-27 09:17:11  おわり: 20261028_night_cs25_fam_p5_r2_s5 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 09:17:11  始めました（45本目・同時 4）: fam_p1_s4_r2_a42 種6 すれ違い切 → 20261028_night_cs25_fam_p1_r2_s6（pid 23363）
2026-10-27 09:18:11  おわり: 20261028_night_cs25_fam_p1_r2_s5_passon / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 09:18:11  始めました（46本目・同時 4）: fam_p5_s4_r0_a42 種6 すれ違い切 → 20261028_night_cs25_fam_p5_r0_s6（pid 23395）
2026-10-27 09:18:46  おわり: 20261028_night_cs25_fam_p1_r0_s6 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-27 09:18:46  始めました（47本目・同時 4）: fam_p5_s4_r1_a42 種6 すれ違い切 → 20261028_night_cs25_fam_p5_r1_s6（pid 23407）
2026-10-27 09:19:31  おわり: 20261028_night_cs25_fam_p1_r1_s6 / completed / 終了コード 0 / 実時間 4.5 分
2026-10-27 09:19:31  始めました（48本目・同時 4）: fam_p5_s4_r2_a42 種6 すれ違い切 → 20261028_night_cs25_fam_p5_r2_s6（pid 23423）
2026-10-27 09:22:01  おわり: 20261028_night_cs25_fam_p1_r2_s6 / completed / 終了コード 0 / 実時間 4.8 分
2026-10-27 09:22:01  始めました（49本目・同時 4）: fam_p1_s4_r2_a42 種6 すれ違い入 → 20261028_night_cs25_fam_p1_r2_s6_passon（pid 23471）
2026-10-27 09:22:41  おわり: 20261028_night_cs25_fam_p5_r0_s6 / completed / 終了コード 0 / 実時間 4.5 分
2026-10-27 09:22:41  始めました（50本目・同時 4）: fam_p1_s4_r0_a42 種7 すれ違い切 → 20261028_night_cs25_fam_p1_r0_s7（pid 23483）
2026-10-27 09:23:26  おわり: 20261028_night_cs25_fam_p5_r1_s6 / completed / 終了コード 0 / 実時間 4.7 分
2026-10-27 09:23:26  始めました（51本目・同時 4）: fam_p1_s4_r1_a42 種7 すれ違い切 → 20261028_night_cs25_fam_p1_r1_s7（pid 23502）
2026-10-27 09:24:36  おわり: 20261028_night_cs25_fam_p5_r2_s6 / completed / 終了コード 0 / 実時間 5.1 分
2026-10-27 09:24:36  始めました（52本目・同時 4）: fam_p1_s4_r2_a42 種7 すれ違い切 → 20261028_night_cs25_fam_p1_r2_s7（pid 23528）
2026-10-27 09:26:41  おわり: 20261028_night_cs25_fam_p1_r0_s7 / completed / 終了コード 0 / 実時間 4.0 分
2026-10-27 09:26:41  始めました（53本目・同時 4）: fam_p5_s4_r0_a42 種7 すれ違い切 → 20261028_night_cs25_fam_p5_r0_s7（pid 23623）
2026-10-27 09:27:06  おわり: 20261028_night_cs25_fam_p1_r2_s6_passon / completed / 終了コード 0 / 実時間 5.1 分
2026-10-27 09:27:06  始めました（54本目・同時 4）: fam_p5_s4_r1_a42 種7 すれ違い切 → 20261028_night_cs25_fam_p5_r1_s7（pid 23641）
2026-10-27 09:27:46  おわり: 20261028_night_cs25_fam_p1_r1_s7 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-27 09:27:46  始めました（55本目・同時 4）: fam_p5_s4_r2_a42 種7 すれ違い切 → 20261028_night_cs25_fam_p5_r2_s7（pid 23654）
2026-10-27 09:28:56  おわり: 20261028_night_cs25_fam_p1_r2_s7 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-27 09:28:56  始めました（56本目・同時 4）: fam_p1_s4_r2_a42 種7 すれ違い入 → 20261028_night_cs25_fam_p1_r2_s7_passon（pid 23674）
2026-10-27 09:30:27  おわり: 20261028_night_cs25_fam_p5_r0_s7 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 09:30:27  始めました（57本目・同時 4）: fam_p1_s4_r0_a42 種8 すれ違い切 → 20261028_night_cs25_fam_p1_r0_s8（pid 23714）
2026-10-27 09:31:12  おわり: 20261028_night_cs25_fam_p5_r1_s7 / completed / 終了コード 0 / 実時間 4.1 分
2026-10-27 09:31:12  始めました（58本目・同時 4）: fam_p1_s4_r1_a42 種8 すれ違い切 → 20261028_night_cs25_fam_p1_r1_s8（pid 23759）
2026-10-27 09:32:12  おわり: 20261028_night_cs25_fam_p5_r2_s7 / completed / 終了コード 0 / 実時間 4.4 分
2026-10-27 09:32:12  始めました（59本目・同時 4）: fam_p1_s4_r2_a42 種8 すれ違い切 → 20261028_night_cs25_fam_p1_r2_s8（pid 23794）
2026-10-27 09:33:02  おわり: 20261028_night_cs25_fam_p1_r2_s7_passon / completed / 終了コード 0 / 実時間 4.1 分
2026-10-27 09:33:02  始めました（60本目・同時 4）: fam_p5_s4_r0_a42 種8 すれ違い切 → 20261028_night_cs25_fam_p5_r0_s8（pid 23826）
2026-10-27 09:34:07  おわり: 20261028_night_cs25_fam_p1_r0_s8 / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 09:34:07  始めました（61本目・同時 4）: fam_p5_s4_r1_a42 種8 すれ違い切 → 20261028_night_cs25_fam_p5_r1_s8（pid 23866）
2026-10-27 09:34:57  おわり: 20261028_night_cs25_fam_p1_r1_s8 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 09:34:57  始めました（62本目・同時 4）: fam_p5_s4_r2_a42 種8 すれ違い切 → 20261028_night_cs25_fam_p5_r2_s8（pid 23911）
2026-10-27 09:36:12  おわり: 20261028_night_cs25_fam_p1_r2_s8 / completed / 終了コード 0 / 実時間 4.0 分
2026-10-27 09:36:12  始めました（63本目・同時 4）: fam_p1_s4_r2_a42 種8 すれ違い入 → 20261028_night_cs25_fam_p1_r2_s8_passon（pid 23986）
2026-10-27 09:36:47  おわり: 20261028_night_cs25_fam_p5_r0_s8 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 09:36:47  始めました（64本目・同時 4）: fam_p1_s4_r0_a42 種9 すれ違い切 → 20261028_night_cs25_fam_p1_r0_s9（pid 24004）
2026-10-27 09:38:02  おわり: 20261028_night_cs25_fam_p5_r1_s8 / completed / 終了コード 0 / 実時間 3.9 分
2026-10-27 09:38:02  始めました（65本目・同時 4）: fam_p1_s4_r1_a42 種9 すれ違い切 → 20261028_night_cs25_fam_p1_r1_s9（pid 24032）
2026-10-27 09:39:02  おわり: 20261028_night_cs25_fam_p5_r2_s8 / completed / 終了コード 0 / 実時間 4.1 分
2026-10-27 09:39:02  始めました（66本目・同時 4）: fam_p1_s4_r2_a42 種9 すれ違い切 → 20261028_night_cs25_fam_p1_r2_s9（pid 24050）
2026-10-27 09:40:02  おわり: 20261028_night_cs25_fam_p1_r2_s8_passon / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 09:40:02  始めました（67本目・同時 4）: fam_p5_s4_r0_a42 種9 すれ違い切 → 20261028_night_cs25_fam_p5_r0_s9（pid 24086）
2026-10-27 09:40:37  おわり: 20261028_night_cs25_fam_p1_r0_s9 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 09:40:37  始めました（68本目・同時 4）: fam_p5_s4_r1_a42 種9 すれ違い切 → 20261028_night_cs25_fam_p5_r1_s9（pid 24094）
2026-10-27 09:42:07  おわり: 20261028_night_cs25_fam_p1_r1_s9 / completed / 終了コード 0 / 実時間 4.1 分
2026-10-27 09:42:07  始めました（69本目・同時 4）: fam_p5_s4_r2_a42 種9 すれ違い切 → 20261028_night_cs25_fam_p5_r2_s9（pid 24122）
2026-10-27 09:43:17  おわり: 20261028_night_cs25_fam_p1_r2_s9 / completed / 終了コード 0 / 実時間 4.3 分
2026-10-27 09:43:17  始めました（70本目・同時 4）: fam_p1_s4_r2_a42 種9 すれ違い入 → 20261028_night_cs25_fam_p1_r2_s9_passon（pid 24147）
2026-10-27 09:43:52  おわり: 20261028_night_cs25_fam_p5_r0_s9 / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 09:44:32  おわり: 20261028_night_cs25_fam_p5_r1_s9 / completed / 終了コード 0 / 実時間 3.9 分
2026-10-27 09:45:47  おわり: 20261028_night_cs25_fam_p5_r2_s9 / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 09:46:27  おわり: 20261028_night_cs25_fam_p1_r2_s9_passon / completed / 終了コード 0 / 実時間 3.2 分
2026-10-27 09:46:27  --- まとめ ---
2026-10-27 09:46:27    completed: 70 回
2026-10-27 09:46:27    completed の実時間: 平均 4.1 分 / 最長 5.1 分
2026-10-27 09:46:27  判定を呼びます（/Volumes/KIOXIA/r2r2r-data/service_sim/unity で）: /opt/homebrew/Caskroom/miniforge/base/bin/python3 /Users/shimazueiichi/Robotics_UX/ServiceSim/tools/judge_night.py --runs 20261028_night_* --out night/20261028
2026-10-27 09:46:28  判定を書きました: /Volumes/KIOXIA/r2r2r-data/service_sim/unity/night/20261028/judge.md
2026-10-27 09:46:28  スリープの設定を戻しました
2026-10-27 09:46:28  === 夜の実行係を終わります（終了コード 0）===
```

### night/20261028/judge.md

```markdown
# 夜の結果の振り分け（20261028_night_*）

基準データ: 20261027_night_cs21_*（*_passon* を除く）（60回）／振り分けた回: 70回

| 振り分け | 回数 |
|---|---|
| 正常 | 60 |
| 試算を見直す候補 | 0 |
| Unity の作り | 0 |
| 回の失敗 | 0 |
| 基準なし | 10 |

## 回の失敗

- なし

## 基準なし（基準データに同じ条件がないので判定していない）

- fam_p1 スタッフ4 ロボット2台・すれ違い入

## Unity の作り（種の中央値が、基準の同じ条件の最大を超えた）

- なし

## 試算を見直す候補

### ロボットを増やしたときの向きが、種の過半数で試算と逆

- なし

### Unity÷試算 の大きさが、基準の同じ条件の範囲から外れた

- なし

## 記録（印にしない）：試算との差の大きさ（条件ごとの種の中央値・Unity÷試算）

| 条件 | 種の数 | 入店待ち | 料理の待ち | 席は空いているのに待った |
|---|---|---|---|---|
| fam_p1 スタッフ4 ロボット0台 | 10 | 1.27 | 2.01 | 3.12 |
| fam_p1 スタッフ4 ロボット2台・すれ違い入 | 10 | 2.63 | 1.85 | 4.71 |
| fam_p1 スタッフ4 ロボット1台 | 10 | 1.17 | 1.23 | 2.10 |
| fam_p1 スタッフ4 ロボット2台 | 10 | 2.53 | 1.50 | 4.47 |
| fam_p5 スタッフ4 ロボット0台 | 10 | 1.63 | 2.14 | 3.68 |
| fam_p5 スタッフ4 ロボット1台 | 10 | 1.98 | 1.25 | 3.48 |
| fam_p5 スタッフ4 ロボット2台 | 10 | 1.82 | 1.45 | 4.23 |

## すれ違い 入 と 切 の比べ（同じ夜・同じ条件・種の中央値。印にしない）

| 条件 | 入店待ち | 料理の待ち | 席は空いているのに待った | 動けなくなった | あきらめた | ロボットが運んだ割合 |
|---|---|---|---|---|---|---|
| fam_p1 スタッフ4 ロボット2台（10回） | 2.98 | 1.42 | 1.72 | 26.50 | 64.00 | 0.93 |
| fam_p1 スタッフ4 ロボット2台・すれ違い入（10回） | 3.10 | 1.76 | 1.81 | 16.00 | 57.00 | 0.88 |

線: 立ち往生 30秒以上の止まり／向きは 5%・0.2分より小さい変化を数えない／外れは基準の同じ条件の最小〜最大
```

### night/20261028/runs.csv

```csv
folder,condition,seed,pass_by,tryno,launch_running,parallel,started_iso,ended_iso,started_unix,ended_unix,started_mono,ended_mono,wall_s,status,exit_code
20261028_night_cs25_fam_p1_r0_s0,fam_p1_s4_r0_a42,0,off,1,1,4,2026-10-27T08:33:59,2026-10-27T08:37:24,1793057639.433,1793057844.549,283442.711,283647.826,205.115,completed,0
20261028_night_cs25_fam_p1_r1_s0,fam_p1_s4_r1_a42,0,off,1,2,4,2026-10-27T08:34:04,2026-10-27T08:37:39,1793057644.440,1793057859.570,283447.717,283662.847,215.130,completed,0
20261028_night_cs25_fam_p5_r0_s0,fam_p5_s4_r0_a42,0,off,1,4,4,2026-10-27T08:34:14,2026-10-27T08:37:39,1793057654.451,1793057859.570,283457.728,283662.847,205.119,completed,0
20261028_night_cs25_fam_p1_r2_s0,fam_p1_s4_r2_a42,0,off,1,3,4,2026-10-27T08:34:09,2026-10-27T08:37:49,1793057649.444,1793057869.606,283452.721,283672.883,220.162,completed,0
20261028_night_cs25_fam_p5_r1_s0,fam_p5_s4_r1_a42,0,off,1,4,4,2026-10-27T08:37:24,2026-10-27T08:40:59,1793057844.562,1793058059.730,283647.838,283863.006,215.168,completed,0
20261028_night_cs25_fam_p5_r2_s0,fam_p5_s4_r2_a42,0,off,1,3,4,2026-10-27T08:37:39,2026-10-27T08:41:19,1793057859.579,1793058079.744,283662.855,283883.020,220.165,completed,0
20261028_night_cs25_fam_p1_r2_s0_passon,fam_p1_s4_r2_a42,0,on,1,4,4,2026-10-27T08:37:44,2026-10-27T08:41:24,1793057864.606,1793058084.755,283667.882,283888.030,220.148,completed,0
20261028_night_cs25_fam_p1_r0_s1,fam_p1_s4_r0_a42,1,off,1,4,4,2026-10-27T08:37:49,2026-10-27T08:41:29,1793057869.612,1793058089.759,283672.888,283893.034,220.146,completed,0
20261028_night_cs25_fam_p1_r1_s1,fam_p1_s4_r1_a42,1,off,1,4,4,2026-10-27T08:40:59,2026-10-27T08:44:55,1793058059.814,1793058295.012,283863.013,284098.211,235.198,completed,0
20261028_night_cs25_fam_p5_r0_s1,fam_p5_s4_r0_a42,1,off,1,4,4,2026-10-27T08:41:24,2026-10-27T08:45:15,1793058084.836,1793058315.031,283888.034,284118.230,230.196,completed,0
20261028_night_cs25_fam_p1_r2_s1,fam_p1_s4_r2_a42,1,off,1,4,4,2026-10-27T08:41:19,2026-10-27T08:45:25,1793058079.828,1793058325.045,283883.026,284128.244,245.218,completed,0
20261028_night_cs25_fam_p5_r1_s1,fam_p5_s4_r1_a42,1,off,1,4,4,2026-10-27T08:41:29,2026-10-27T08:45:30,1793058089.842,1793058330.052,283893.040,284133.250,240.210,completed,0
20261028_night_cs25_fam_p5_r2_s1,fam_p5_s4_r2_a42,1,off,1,4,4,2026-10-27T08:44:55,2026-10-27T08:49:10,1793058295.024,1793058550.200,284098.216,284353.392,255.176,completed,0
20261028_night_cs25_fam_p1_r2_s1_passon,fam_p1_s4_r2_a42,1,on,1,4,4,2026-10-27T08:45:15,2026-10-27T08:49:10,1793058315.045,1793058550.201,284118.236,284353.392,235.156,completed,0
20261028_night_cs25_fam_p1_r0_s2,fam_p1_s4_r0_a42,2,off,1,4,4,2026-10-27T08:45:25,2026-10-27T08:49:45,1793058325.058,1793058585.239,284128.249,284388.430,260.181,completed,0
20261028_night_cs25_fam_p1_r1_s2,fam_p1_s4_r1_a42,2,off,1,4,4,2026-10-27T08:45:30,2026-10-27T08:50:05,1793058330.063,1793058605.262,284133.254,284408.452,275.199,completed,0
20261028_night_cs25_fam_p5_r0_s2,fam_p5_s4_r0_a42,2,off,1,4,4,2026-10-27T08:49:15,2026-10-27T08:53:35,1793058555.223,1793058815.399,284358.407,284618.583,260.176,completed,0
20261028_night_cs25_fam_p1_r2_s2,fam_p1_s4_r2_a42,2,off,1,3,4,2026-10-27T08:49:10,2026-10-27T08:54:05,1793058550.216,1793058845.418,284353.401,284648.602,295.201,completed,0
20261028_night_cs25_fam_p5_r1_s2,fam_p5_s4_r1_a42,2,off,1,4,4,2026-10-27T08:49:45,2026-10-27T08:54:30,1793058585.256,1793058870.439,284388.439,284673.622,285.183,completed,0
20261028_night_cs25_fam_p5_r2_s2,fam_p5_s4_r2_a42,2,off,1,4,4,2026-10-27T08:50:05,2026-10-27T08:54:55,1793058605.274,1793058895.458,284408.457,284698.640,290.183,completed,0
20261028_night_cs25_fam_p1_r0_s3,fam_p1_s4_r0_a42,3,off,1,4,4,2026-10-27T08:54:05,2026-10-27T08:57:35,1793058845.432,1793059055.568,284648.607,284858.744,210.136,completed,0
20261028_night_cs25_fam_p1_r1_s3,fam_p1_s4_r1_a42,3,off,1,4,4,2026-10-27T08:54:30,2026-10-27T08:58:05,1793058870.454,1793059085.592,284673.629,284888.767,215.138,completed,0
20261028_night_cs25_fam_p1_r2_s2_passon,fam_p1_s4_r2_a42,2,on,1,4,4,2026-10-27T08:53:35,2026-10-27T08:58:25,1793058815.412,1793059105.603,284618.588,284908.780,290.191,completed,0
20261028_night_cs25_fam_p1_r2_s3,fam_p1_s4_r2_a42,3,off,1,4,4,2026-10-27T08:54:55,2026-10-27T08:58:50,1793058895.471,1793059130.631,284698.644,284933.805,235.161,completed,0
20261028_night_cs25_fam_p5_r0_s3,fam_p5_s4_r0_a42,3,off,1,4,4,2026-10-27T08:57:35,2026-10-27T09:01:05,1793059055.583,1793059265.729,284858.752,285068.898,210.146,completed,0
20261028_night_cs25_fam_p5_r1_s3,fam_p5_s4_r1_a42,3,off,1,4,4,2026-10-27T08:58:05,2026-10-27T09:02:00,1793059085.605,1793059320.763,284888.773,285123.931,235.158,completed,0
20261028_night_cs25_fam_p5_r2_s3,fam_p5_s4_r2_a42,3,off,1,4,4,2026-10-27T08:58:25,2026-10-27T09:02:15,1793059105.619,1793059335.778,284908.787,285138.946,230.159,completed,0
20261028_night_cs25_fam_p1_r2_s3_passon,fam_p1_s4_r2_a42,3,on,1,4,4,2026-10-27T08:58:50,2026-10-27T09:02:45,1793059130.652,1793059365.809,284933.810,285168.967,235.157,completed,0
20261028_night_cs25_fam_p1_r0_s4,fam_p1_s4_r0_a42,4,off,1,4,4,2026-10-27T09:01:05,2026-10-27T09:05:15,1793059265.759,1793059515.900,285068.904,285319.044,250.140,completed,0
20261028_night_cs25_fam_p1_r1_s4,fam_p1_s4_r1_a42,4,off,1,4,4,2026-10-27T09:02:00,2026-10-27T09:06:30,1793059320.793,1793059590.950,285123.936,285394.093,270.156,completed,0
20261028_night_cs25_fam_p1_r2_s4,fam_p1_s4_r2_a42,4,off,1,4,4,2026-10-27T09:02:15,2026-10-27T09:06:45,1793059335.808,1793059605.964,285138.951,285409.107,270.156,completed,0
20261028_night_cs25_fam_p5_r0_s4,fam_p5_s4_r0_a42,4,off,1,4,4,2026-10-27T09:02:45,2026-10-27T09:06:55,1793059365.832,1793059615.978,285168.974,285419.120,250.146,completed,0
20261028_night_cs25_fam_p5_r1_s4,fam_p5_s4_r1_a42,4,off,1,4,4,2026-10-27T09:05:15,2026-10-27T09:09:36,1793059515.912,1793059776.094,285319.050,285579.231,260.181,completed,0
20261028_night_cs25_fam_p1_r0_s5,fam_p1_s4_r0_a42,5,off,1,4,4,2026-10-27T09:06:55,2026-10-27T09:10:26,1793059615.990,1793059826.141,285419.124,285629.275,210.151,completed,0
20261028_night_cs25_fam_p5_r2_s4,fam_p5_s4_r2_a42,4,off,1,4,4,2026-10-27T09:06:30,2026-10-27T09:11:06,1793059590.962,1793059866.172,285394.097,285669.307,275.210,completed,0
20261028_night_cs25_fam_p1_r2_s4_passon,fam_p1_s4_r2_a42,4,on,1,4,4,2026-10-27T09:06:45,2026-10-27T09:11:21,1793059605.977,1793059881.189,285409.111,285684.323,275.212,completed,0
20261028_night_cs25_fam_p1_r1_s5,fam_p1_s4_r1_a42,5,off,1,4,4,2026-10-27T09:09:36,2026-10-27T09:13:21,1793059776.108,1793060001.282,285579.237,285804.411,225.174,completed,0
20261028_night_cs25_fam_p1_r2_s5,fam_p1_s4_r2_a42,5,off,1,4,4,2026-10-27T09:10:26,2026-10-27T09:14:21,1793059826.154,1793060061.327,285629.281,285864.455,235.174,completed,0
20261028_night_cs25_fam_p5_r0_s5,fam_p5_s4_r0_a42,5,off,1,4,4,2026-10-27T09:11:06,2026-10-27T09:14:31,1793059866.188,1793060071.341,285669.315,285874.468,205.153,completed,0
20261028_night_cs25_fam_p5_r1_s5,fam_p5_s4_r1_a42,5,off,1,4,4,2026-10-27T09:11:21,2026-10-27T09:15:01,1793059881.201,1793060101.361,285684.327,285904.488,220.160,completed,0
20261028_night_cs25_fam_p5_r2_s5,fam_p5_s4_r2_a42,5,off,1,4,4,2026-10-27T09:13:21,2026-10-27T09:17:11,1793060001.294,1793060231.440,285804.416,286034.562,230.146,completed,0
20261028_night_cs25_fam_p1_r2_s5_passon,fam_p1_s4_r2_a42,5,on,1,4,4,2026-10-27T09:14:21,2026-10-27T09:18:11,1793060061.339,1793060291.489,285864.460,286094.610,230.150,completed,0
20261028_night_cs25_fam_p1_r0_s6,fam_p1_s4_r0_a42,6,off,1,4,4,2026-10-27T09:14:31,2026-10-27T09:18:46,1793060071.351,1793060326.529,285874.472,286129.650,255.178,completed,0
20261028_night_cs25_fam_p1_r1_s6,fam_p1_s4_r1_a42,6,off,1,4,4,2026-10-27T09:15:01,2026-10-27T09:19:31,1793060101.373,1793060371.580,285904.492,286174.700,270.207,completed,0
20261028_night_cs25_fam_p1_r2_s6,fam_p1_s4_r2_a42,6,off,1,4,4,2026-10-27T09:17:11,2026-10-27T09:22:01,1793060231.436,1793060521.668,286034.569,286324.802,290.232,completed,0
20261028_night_cs25_fam_p5_r0_s6,fam_p5_s4_r0_a42,6,off,1,4,4,2026-10-27T09:18:11,2026-10-27T09:22:41,1793060291.490,1793060561.695,286094.623,286364.827,270.205,completed,0
20261028_night_cs25_fam_p5_r1_s6,fam_p5_s4_r1_a42,6,off,1,4,4,2026-10-27T09:18:46,2026-10-27T09:23:26,1793060326.522,1793060606.726,286129.654,286409.858,280.203,completed,0
20261028_night_cs25_fam_p5_r2_s6,fam_p5_s4_r2_a42,6,off,1,4,4,2026-10-27T09:19:31,2026-10-27T09:24:36,1793060371.577,1793060676.782,286174.709,286479.913,305.205,completed,0
20261028_night_cs25_fam_p1_r0_s7,fam_p1_s4_r0_a42,7,off,1,4,4,2026-10-27T09:22:41,2026-10-27T09:26:41,1793060561.705,1793060801.875,286364.833,286605.003,240.170,completed,0
20261028_night_cs25_fam_p1_r2_s6_passon,fam_p1_s4_r2_a42,6,on,1,4,4,2026-10-27T09:22:01,2026-10-27T09:27:06,1793060521.679,1793060826.899,286324.808,286630.028,305.220,completed,0
20261028_night_cs25_fam_p1_r1_s7,fam_p1_s4_r1_a42,7,off,1,4,4,2026-10-27T09:23:26,2026-10-27T09:27:46,1793060606.741,1793060866.928,286409.868,286670.055,260.187,completed,0
20261028_night_cs25_fam_p1_r2_s7,fam_p1_s4_r2_a42,7,off,1,4,4,2026-10-27T09:24:36,2026-10-27T09:28:56,1793060676.795,1793060936.977,286479.921,286740.103,260.181,completed,0
20261028_night_cs25_fam_p5_r0_s7,fam_p5_s4_r0_a42,7,off,1,4,4,2026-10-27T09:26:41,2026-10-27T09:30:27,1793060801.894,1793061027.035,286605.018,286830.159,225.141,completed,0
20261028_night_cs25_fam_p5_r1_s7,fam_p5_s4_r1_a42,7,off,1,4,4,2026-10-27T09:27:06,2026-10-27T09:31:12,1793060826.909,1793061072.061,286630.032,286875.185,245.152,completed,0
20261028_night_cs25_fam_p5_r2_s7,fam_p5_s4_r2_a42,7,off,1,4,4,2026-10-27T09:27:46,2026-10-27T09:32:12,1793060866.938,1793061132.104,286670.060,286935.226,265.166,completed,0
20261028_night_cs25_fam_p1_r2_s7_passon,fam_p1_s4_r2_a42,7,on,1,4,4,2026-10-27T09:28:56,2026-10-27T09:33:02,1793060936.992,1793061182.131,286740.113,286985.252,245.139,completed,0
20261028_night_cs25_fam_p1_r0_s8,fam_p1_s4_r0_a42,8,off,1,4,4,2026-10-27T09:30:27,2026-10-27T09:34:07,1793061027.044,1793061247.172,286830.164,287050.291,220.128,completed,0
20261028_night_cs25_fam_p1_r1_s8,fam_p1_s4_r1_a42,8,off,1,4,4,2026-10-27T09:31:12,2026-10-27T09:34:57,1793061072.072,1793061297.209,286875.191,287100.328,225.136,completed,0
20261028_night_cs25_fam_p1_r2_s8,fam_p1_s4_r2_a42,8,off,1,4,4,2026-10-27T09:32:12,2026-10-27T09:36:12,1793061132.115,1793061372.260,286935.233,287175.378,240.145,completed,0
20261028_night_cs25_fam_p5_r0_s8,fam_p5_s4_r0_a42,8,off,1,4,4,2026-10-27T09:33:02,2026-10-27T09:36:47,1793061182.140,1793061407.321,286985.257,287210.438,225.180,completed,0
20261028_night_cs25_fam_p5_r1_s8,fam_p5_s4_r1_a42,8,off,1,4,4,2026-10-27T09:34:07,2026-10-27T09:38:02,1793061247.184,1793061482.369,287050.299,287285.485,235.185,completed,0
20261028_night_cs25_fam_p5_r2_s8,fam_p5_s4_r2_a42,8,off,1,4,4,2026-10-27T09:34:57,2026-10-27T09:39:02,1793061297.219,1793061542.425,287100.334,287345.540,245.206,completed,0
20261028_night_cs25_fam_p1_r2_s8_passon,fam_p1_s4_r2_a42,8,on,1,4,4,2026-10-27T09:36:12,2026-10-27T09:40:02,1793061372.271,1793061602.463,287175.384,287405.577,230.193,completed,0
20261028_night_cs25_fam_p1_r0_s9,fam_p1_s4_r0_a42,9,off,1,4,4,2026-10-27T09:36:47,2026-10-27T09:40:37,1793061407.329,1793061637.488,287210.442,287440.601,230.159,completed,0
20261028_night_cs25_fam_p1_r1_s9,fam_p1_s4_r1_a42,9,off,1,4,4,2026-10-27T09:38:02,2026-10-27T09:42:07,1793061482.379,1793061727.544,287285.490,287530.656,245.165,completed,0
20261028_night_cs25_fam_p1_r2_s9,fam_p1_s4_r2_a42,9,off,1,4,4,2026-10-27T09:39:02,2026-10-27T09:43:17,1793061542.434,1793061797.575,287345.545,287600.686,255.141,completed,0
20261028_night_cs25_fam_p5_r0_s9,fam_p5_s4_r0_a42,9,off,1,4,4,2026-10-27T09:40:02,2026-10-27T09:43:52,1793061602.473,1793061832.607,287405.583,287635.717,230.134,completed,0
20261028_night_cs25_fam_p5_r1_s9,fam_p5_s4_r1_a42,9,off,1,4,4,2026-10-27T09:40:37,2026-10-27T09:44:32,1793061637.496,1793061872.644,287440.605,287675.753,235.148,completed,0
20261028_night_cs25_fam_p5_r2_s9,fam_p5_s4_r2_a42,9,off,1,4,4,2026-10-27T09:42:07,2026-10-27T09:45:47,1793061727.553,1793061947.720,287530.661,287750.828,220.167,completed,0
20261028_night_cs25_fam_p1_r2_s9_passon,fam_p1_s4_r2_a42,9,on,1,4,4,2026-10-27T09:43:17,2026-10-27T09:46:27,1793061797.584,1793061987.774,287600.690,287790.880,190.189,completed,0
```

### night/20261029（3回目の夜・基準データ v4・cs:26・60回）

### night/20261029/runner_log.txt

```text
2026-10-27 10:19:09  === 夜の実行係を始めます（night 20261029・同時 4 本・上限 90.0 分）===
2026-10-27 10:19:09  並べた回: queue.json（60 回）
2026-10-27 10:19:09  台帳: /Volumes/KIOXIA/r2r2r-data/service_sim/unity/unity_ledger_v1.csv（59 列）
2026-10-27 10:19:09  版: ServiceSim/cs:26（Builds/headless/build_info.json）→ 出力フォルダの字 cs26
2026-10-27 10:19:09  並べた回の試算の版: v2.8 / 店の形の版: 8a5b2f6a016a
2026-10-27 10:19:09  スリープを止めました（caffeinate pid 25702）
2026-10-27 10:19:09  並べた回を写しました: /Volumes/KIOXIA/r2r2r-data/service_sim/unity/night/20261029/queue.json
2026-10-27 10:19:09  並べた回 60 / もう completed の回を飛ばす 0 / 回す 60
2026-10-27 10:19:09  始めました（1本目・同時 1）: fam_p1_s4_r0_a42 種0 すれ違い入 → 20261029_night_cs26_fam_p1_r0_s0_passon（pid 25703）
2026-10-27 10:19:14  始めました（2本目・同時 2）: fam_p1_s4_r1_a42 種0 すれ違い入 → 20261029_night_cs26_fam_p1_r1_s0_passon（pid 25718）
2026-10-27 10:19:19  始めました（3本目・同時 3）: fam_p1_s4_r2_a42 種0 すれ違い入 → 20261029_night_cs26_fam_p1_r2_s0_passon（pid 25720）
2026-10-27 10:19:24  始めました（4本目・同時 4）: fam_p5_s4_r0_a42 種0 すれ違い入 → 20261029_night_cs26_fam_p5_r0_s0_passon（pid 25721）
2026-10-27 10:22:29  おわり: 20261029_night_cs26_fam_p1_r0_s0_passon / completed / 終了コード 0 / 実時間 3.3 分
2026-10-27 10:22:29  始めました（5本目・同時 4）: fam_p5_s4_r1_a42 種0 すれ違い入 → 20261029_night_cs26_fam_p5_r1_s0_passon（pid 25767）
2026-10-27 10:22:49  おわり: 20261029_night_cs26_fam_p1_r1_s0_passon / completed / 終了コード 0 / 実時間 3.6 分
2026-10-27 10:22:49  おわり: 20261029_night_cs26_fam_p5_r0_s0_passon / completed / 終了コード 0 / 実時間 3.4 分
2026-10-27 10:22:49  始めました（6本目・同時 3）: fam_p5_s4_r2_a42 種0 すれ違い入 → 20261029_night_cs26_fam_p5_r2_s0_passon（pid 25786）
2026-10-27 10:22:54  始めました（7本目・同時 4）: fam_p1_s4_r0_a42 種1 すれ違い入 → 20261029_night_cs26_fam_p1_r0_s1_passon（pid 25792）
2026-10-27 10:22:59  おわり: 20261029_night_cs26_fam_p1_r2_s0_passon / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 10:22:59  始めました（8本目・同時 4）: fam_p1_s4_r1_a42 種1 すれ違い入 → 20261029_night_cs26_fam_p1_r1_s1_passon（pid 25793）
2026-10-27 10:25:59  おわり: 20261029_night_cs26_fam_p5_r1_s0_passon / completed / 終了コード 0 / 実時間 3.5 分
2026-10-27 10:25:59  始めました（9本目・同時 4）: fam_p1_s4_r2_a42 種1 すれ違い入 → 20261029_night_cs26_fam_p1_r2_s1_passon（pid 25884）
2026-10-27 10:26:29  おわり: 20261029_night_cs26_fam_p1_r0_s1_passon / completed / 終了コード 0 / 実時間 3.6 分
2026-10-27 10:26:29  始めました（10本目・同時 4）: fam_p5_s4_r0_a42 種1 すれ違い入 → 20261029_night_cs26_fam_p5_r0_s1_passon（pid 25899）
2026-10-27 10:26:49  おわり: 20261029_night_cs26_fam_p1_r1_s1_passon / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 10:26:49  始めました（11本目・同時 4）: fam_p5_s4_r1_a42 種1 すれ違い入 → 20261029_night_cs26_fam_p5_r1_s1_passon（pid 25921）
2026-10-27 10:26:54  おわり: 20261029_night_cs26_fam_p5_r2_s0_passon / completed / 終了コード 0 / 実時間 4.1 分
2026-10-27 10:26:54  始めました（12本目・同時 4）: fam_p5_s4_r2_a42 種1 すれ違い入 → 20261029_night_cs26_fam_p5_r2_s1_passon（pid 25928）
2026-10-27 10:29:49  おわり: 20261029_night_cs26_fam_p1_r2_s1_passon / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 10:29:49  始めました（13本目・同時 4）: fam_p1_s4_r0_a42 種2 すれ違い入 → 20261029_night_cs26_fam_p1_r0_s2_passon（pid 25972）
2026-10-27 10:30:04  おわり: 20261029_night_cs26_fam_p5_r0_s1_passon / completed / 終了コード 0 / 実時間 3.6 分
2026-10-27 10:30:04  始めました（14本目・同時 4）: fam_p1_s4_r1_a42 種2 すれ違い入 → 20261029_night_cs26_fam_p1_r1_s2_passon（pid 25976）
2026-10-27 10:30:29  おわり: 20261029_night_cs26_fam_p5_r1_s1_passon / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 10:30:29  始めました（15本目・同時 4）: fam_p1_s4_r2_a42 種2 すれ違い入 → 20261029_night_cs26_fam_p1_r2_s2_passon（pid 25981）
2026-10-27 10:30:54  おわり: 20261029_night_cs26_fam_p5_r2_s1_passon / completed / 終了コード 0 / 実時間 4.0 分
2026-10-27 10:30:54  始めました（16本目・同時 4）: fam_p5_s4_r0_a42 種2 すれ違い入 → 20261029_night_cs26_fam_p5_r0_s2_passon（pid 25992）
2026-10-27 10:34:05  おわり: 20261029_night_cs26_fam_p1_r0_s2_passon / completed / 終了コード 0 / 実時間 4.3 分
2026-10-27 10:34:05  始めました（17本目・同時 4）: fam_p5_s4_r1_a42 種2 すれ違い入 → 20261029_night_cs26_fam_p5_r1_s2_passon（pid 26055）
2026-10-27 10:34:30  おわり: 20261029_night_cs26_fam_p1_r1_s2_passon / completed / 終了コード 0 / 実時間 4.4 分
2026-10-27 10:34:30  始めました（18本目・同時 4）: fam_p5_s4_r2_a42 種2 すれ違い入 → 20261029_night_cs26_fam_p5_r2_s2_passon（pid 26067）
2026-10-27 10:35:10  おわり: 20261029_night_cs26_fam_p5_r0_s2_passon / completed / 終了コード 0 / 実時間 4.3 分
2026-10-27 10:35:10  始めました（19本目・同時 4）: fam_p1_s4_r0_a42 種3 すれ違い入 → 20261029_night_cs26_fam_p1_r0_s3_passon（pid 26091）
2026-10-27 10:35:15  おわり: 20261029_night_cs26_fam_p1_r2_s2_passon / completed / 終了コード 0 / 実時間 4.8 分
2026-10-27 10:35:15  始めました（20本目・同時 4）: fam_p1_s4_r1_a42 種3 すれ違い入 → 20261029_night_cs26_fam_p1_r1_s3_passon（pid 26097）
2026-10-27 10:38:35  おわり: 20261029_night_cs26_fam_p1_r0_s3_passon / completed / 終了コード 0 / 実時間 3.4 分
2026-10-27 10:38:35  始めました（21本目・同時 4）: fam_p1_s4_r2_a42 種3 すれ違い入 → 20261029_night_cs26_fam_p1_r2_s3_passon（pid 26166）
2026-10-27 10:38:40  おわり: 20261029_night_cs26_fam_p5_r1_s2_passon / completed / 終了コード 0 / 実時間 4.6 分
2026-10-27 10:38:40  始めました（22本目・同時 4）: fam_p5_s4_r0_a42 種3 すれ違い入 → 20261029_night_cs26_fam_p5_r0_s3_passon（pid 26167）
2026-10-27 10:38:45  おわり: 20261029_night_cs26_fam_p1_r1_s3_passon / completed / 終了コード 0 / 実時間 3.5 分
2026-10-27 10:38:45  始めました（23本目・同時 4）: fam_p5_s4_r1_a42 種3 すれ違い入 → 20261029_night_cs26_fam_p5_r1_s3_passon（pid 26168）
2026-10-27 10:39:05  おわり: 20261029_night_cs26_fam_p5_r2_s2_passon / completed / 終了コード 0 / 実時間 4.6 分
2026-10-27 10:39:05  始めました（24本目・同時 4）: fam_p5_s4_r2_a42 種3 すれ違い入 → 20261029_night_cs26_fam_p5_r2_s3_passon（pid 26178）
2026-10-27 10:42:00  おわり: 20261029_night_cs26_fam_p5_r0_s3_passon / completed / 終了コード 0 / 実時間 3.3 分
2026-10-27 10:42:00  始めました（25本目・同時 4）: fam_p1_s4_r0_a42 種4 すれ違い入 → 20261029_night_cs26_fam_p1_r0_s4_passon（pid 26234）
2026-10-27 10:42:15  おわり: 20261029_night_cs26_fam_p1_r2_s3_passon / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 10:42:15  始めました（26本目・同時 4）: fam_p1_s4_r1_a42 種4 すれ違い入 → 20261029_night_cs26_fam_p1_r1_s4_passon（pid 26241）
2026-10-27 10:42:45  おわり: 20261029_night_cs26_fam_p5_r1_s3_passon / completed / 終了コード 0 / 実時間 4.0 分
2026-10-27 10:42:45  始めました（27本目・同時 4）: fam_p1_s4_r2_a42 種4 すれ違い入 → 20261029_night_cs26_fam_p1_r2_s4_passon（pid 26249）
2026-10-27 10:43:00  おわり: 20261029_night_cs26_fam_p5_r2_s3_passon / completed / 終了コード 0 / 実時間 3.9 分
2026-10-27 10:43:00  始めました（28本目・同時 4）: fam_p5_s4_r0_a42 種4 すれ違い入 → 20261029_night_cs26_fam_p5_r0_s4_passon（pid 26259）
2026-10-27 10:46:00  おわり: 20261029_night_cs26_fam_p1_r0_s4_passon / completed / 終了コード 0 / 実時間 4.0 分
2026-10-27 10:46:00  始めました（29本目・同時 4）: fam_p5_s4_r1_a42 種4 すれ違い入 → 20261029_night_cs26_fam_p5_r1_s4_passon（pid 26300）
2026-10-27 10:46:20  おわり: 20261029_night_cs26_fam_p1_r1_s4_passon / completed / 終了コード 0 / 実時間 4.1 分
2026-10-27 10:46:20  始めました（30本目・同時 4）: fam_p5_s4_r2_a42 種4 すれ違い入 → 20261029_night_cs26_fam_p5_r2_s4_passon（pid 26310）
2026-10-27 10:47:00  おわり: 20261029_night_cs26_fam_p5_r0_s4_passon / completed / 終了コード 0 / 実時間 4.0 分
2026-10-27 10:47:00  始めました（31本目・同時 4）: fam_p1_s4_r0_a42 種5 すれ違い入 → 20261029_night_cs26_fam_p1_r0_s5_passon（pid 26326）
2026-10-27 10:47:16  おわり: 20261029_night_cs26_fam_p1_r2_s4_passon / completed / 終了コード 0 / 実時間 4.5 分
2026-10-27 10:47:16  始めました（32本目・同時 4）: fam_p1_s4_r1_a42 種5 すれ違い入 → 20261029_night_cs26_fam_p1_r1_s5_passon（pid 26331）
2026-10-27 10:50:26  おわり: 20261029_night_cs26_fam_p5_r1_s4_passon / completed / 終了コード 0 / 実時間 4.4 分
2026-10-27 10:50:26  おわり: 20261029_night_cs26_fam_p1_r0_s5_passon / completed / 終了コード 0 / 実時間 3.4 分
2026-10-27 10:50:26  始めました（33本目・同時 3）: fam_p1_s4_r2_a42 種5 すれ違い入 → 20261029_night_cs26_fam_p1_r2_s5_passon（pid 26371）
2026-10-27 10:50:31  始めました（34本目・同時 4）: fam_p5_s4_r0_a42 種5 すれ違い入 → 20261029_night_cs26_fam_p5_r0_s5_passon（pid 26372）
2026-10-27 10:50:46  おわり: 20261029_night_cs26_fam_p5_r2_s4_passon / completed / 終了コード 0 / 実時間 4.4 分
2026-10-27 10:50:46  おわり: 20261029_night_cs26_fam_p1_r1_s5_passon / completed / 終了コード 0 / 実時間 3.5 分
2026-10-27 10:50:46  始めました（35本目・同時 3）: fam_p5_s4_r1_a42 種5 すれ違い入 → 20261029_night_cs26_fam_p5_r1_s5_passon（pid 26373）
2026-10-27 10:50:51  始めました（36本目・同時 4）: fam_p5_s4_r2_a42 種5 すれ違い入 → 20261029_night_cs26_fam_p5_r2_s5_passon（pid 26375）
2026-10-27 10:53:56  おわり: 20261029_night_cs26_fam_p5_r0_s5_passon / completed / 終了コード 0 / 実時間 3.4 分
2026-10-27 10:53:56  始めました（37本目・同時 4）: fam_p1_s4_r0_a42 種6 すれ違い入 → 20261029_night_cs26_fam_p1_r0_s6_passon（pid 26437）
2026-10-27 10:54:16  おわり: 20261029_night_cs26_fam_p1_r2_s5_passon / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 10:54:16  始めました（38本目・同時 4）: fam_p1_s4_r1_a42 種6 すれ違い入 → 20261029_night_cs26_fam_p1_r1_s6_passon（pid 26440）
2026-10-27 10:54:21  おわり: 20261029_night_cs26_fam_p5_r1_s5_passon / completed / 終了コード 0 / 実時間 3.6 分
2026-10-27 10:54:21  始めました（39本目・同時 4）: fam_p1_s4_r2_a42 種6 すれ違い入 → 20261029_night_cs26_fam_p1_r2_s6_passon（pid 26441）
2026-10-27 10:54:41  おわり: 20261029_night_cs26_fam_p5_r2_s5_passon / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 10:54:41  始めました（40本目・同時 4）: fam_p5_s4_r0_a42 種6 すれ違い入 → 20261029_night_cs26_fam_p5_r0_s6_passon（pid 26445）
2026-10-27 10:58:06  おわり: 20261029_night_cs26_fam_p1_r0_s6_passon / completed / 終了コード 0 / 実時間 4.2 分
2026-10-27 10:58:06  始めました（41本目・同時 4）: fam_p5_s4_r1_a42 種6 すれ違い入 → 20261029_night_cs26_fam_p5_r1_s6_passon（pid 26514）
2026-10-27 10:58:36  おわり: 20261029_night_cs26_fam_p1_r1_s6_passon / completed / 終了コード 0 / 実時間 4.3 分
2026-10-27 10:58:36  始めました（42本目・同時 4）: fam_p5_s4_r2_a42 種6 すれ違い入 → 20261029_night_cs26_fam_p5_r2_s6_passon（pid 26520）
2026-10-27 10:58:56  おわり: 20261029_night_cs26_fam_p5_r0_s6_passon / completed / 終了コード 0 / 実時間 4.3 分
2026-10-27 10:58:56  始めました（43本目・同時 4）: fam_p1_s4_r0_a42 種7 すれ違い入 → 20261029_night_cs26_fam_p1_r0_s7_passon（pid 26523）
2026-10-27 10:59:06  おわり: 20261029_night_cs26_fam_p1_r2_s6_passon / completed / 終了コード 0 / 実時間 4.8 分
2026-10-27 10:59:06  始めました（44本目・同時 4）: fam_p1_s4_r1_a42 種7 すれ違い入 → 20261029_night_cs26_fam_p1_r1_s7_passon（pid 26528）
2026-10-27 11:02:31  おわり: 20261029_night_cs26_fam_p5_r1_s6_passon / completed / 終了コード 0 / 実時間 4.4 分
2026-10-27 11:02:31  始めました（45本目・同時 4）: fam_p1_s4_r2_a42 種7 すれ違い入 → 20261029_night_cs26_fam_p1_r2_s7_passon（pid 26613）
2026-10-27 11:02:36  おわり: 20261029_night_cs26_fam_p1_r0_s7_passon / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 11:02:36  始めました（46本目・同時 4）: fam_p5_s4_r0_a42 種7 すれ違い入 → 20261029_night_cs26_fam_p5_r0_s7_passon（pid 26624）
2026-10-27 11:03:02  おわり: 20261029_night_cs26_fam_p1_r1_s7_passon / completed / 終了コード 0 / 実時間 3.9 分
2026-10-27 11:03:02  始めました（47本目・同時 4）: fam_p5_s4_r1_a42 種7 すれ違い入 → 20261029_night_cs26_fam_p5_r1_s7_passon（pid 26632）
2026-10-27 11:03:22  おわり: 20261029_night_cs26_fam_p5_r2_s6_passon / completed / 終了コード 0 / 実時間 4.8 分
2026-10-27 11:03:22  始めました（48本目・同時 4）: fam_p5_s4_r2_a42 種7 すれ違い入 → 20261029_night_cs26_fam_p5_r2_s7_passon（pid 26637）
2026-10-27 11:06:17  おわり: 20261029_night_cs26_fam_p5_r0_s7_passon / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 11:06:17  始めました（49本目・同時 4）: fam_p1_s4_r0_a42 種8 すれ違い入 → 20261029_night_cs26_fam_p1_r0_s8_passon（pid 26683）
2026-10-27 11:06:37  おわり: 20261029_night_cs26_fam_p1_r2_s7_passon / completed / 終了コード 0 / 実時間 4.1 分
2026-10-27 11:06:37  始めました（50本目・同時 4）: fam_p1_s4_r1_a42 種8 すれ違い入 → 20261029_night_cs26_fam_p1_r1_s8_passon（pid 26692）
2026-10-27 11:07:02  おわり: 20261029_night_cs26_fam_p5_r1_s7_passon / completed / 終了コード 0 / 実時間 4.0 分
2026-10-27 11:07:02  始めました（51本目・同時 4）: fam_p1_s4_r2_a42 種8 すれ違い入 → 20261029_night_cs26_fam_p1_r2_s8_passon（pid 26700）
2026-10-27 11:07:32  おわり: 20261029_night_cs26_fam_p5_r2_s7_passon / completed / 終了コード 0 / 実時間 4.2 分
2026-10-27 11:07:32  始めました（52本目・同時 4）: fam_p5_s4_r0_a42 種8 すれ違い入 → 20261029_night_cs26_fam_p5_r0_s8_passon（pid 26708）
2026-10-27 11:09:47  おわり: 20261029_night_cs26_fam_p1_r0_s8_passon / completed / 終了コード 0 / 実時間 3.5 分
2026-10-27 11:09:47  始めました（53本目・同時 4）: fam_p5_s4_r1_a42 種8 すれ違い入 → 20261029_night_cs26_fam_p5_r1_s8_passon（pid 26761）
2026-10-27 11:10:17  おわり: 20261029_night_cs26_fam_p1_r1_s8_passon / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 11:10:17  始めました（54本目・同時 4）: fam_p5_s4_r2_a42 種8 すれ違い入 → 20261029_night_cs26_fam_p5_r2_s8_passon（pid 26777）
2026-10-27 11:11:07  おわり: 20261029_night_cs26_fam_p5_r0_s8_passon / completed / 終了コード 0 / 実時間 3.6 分
2026-10-27 11:11:07  始めました（55本目・同時 4）: fam_p1_s4_r0_a42 種9 すれ違い入 → 20261029_night_cs26_fam_p1_r0_s9_passon（pid 26795）
2026-10-27 11:11:12  おわり: 20261029_night_cs26_fam_p1_r2_s8_passon / completed / 終了コード 0 / 実時間 4.2 分
2026-10-27 11:11:12  始めました（56本目・同時 4）: fam_p1_s4_r1_a42 種9 すれ違い入 → 20261029_night_cs26_fam_p1_r1_s9_passon（pid 26804）
2026-10-27 11:13:27  おわり: 20261029_night_cs26_fam_p5_r1_s8_passon / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 11:13:27  始めました（57本目・同時 4）: fam_p1_s4_r2_a42 種9 すれ違い入 → 20261029_night_cs26_fam_p1_r2_s9_passon（pid 26830）
2026-10-27 11:14:12  おわり: 20261029_night_cs26_fam_p5_r2_s8_passon / completed / 終了コード 0 / 実時間 3.9 分
2026-10-27 11:14:12  始めました（58本目・同時 4）: fam_p5_s4_r0_a42 種9 すれ違い入 → 20261029_night_cs26_fam_p5_r0_s9_passon（pid 26845）
2026-10-27 11:14:52  おわり: 20261029_night_cs26_fam_p1_r0_s9_passon / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 11:14:52  始めました（59本目・同時 4）: fam_p5_s4_r1_a42 種9 すれ違い入 → 20261029_night_cs26_fam_p5_r1_s9_passon（pid 26854）
2026-10-27 11:15:07  おわり: 20261029_night_cs26_fam_p1_r1_s9_passon / completed / 終了コード 0 / 実時間 3.9 分
2026-10-27 11:15:07  始めました（60本目・同時 4）: fam_p5_s4_r2_a42 種9 すれ違い入 → 20261029_night_cs26_fam_p5_r2_s9_passon（pid 26858）
2026-10-27 11:17:42  おわり: 20261029_night_cs26_fam_p1_r2_s9_passon / completed / 終了コード 0 / 実時間 4.3 分
2026-10-27 11:17:57  おわり: 20261029_night_cs26_fam_p5_r0_s9_passon / completed / 終了コード 0 / 実時間 3.8 分
2026-10-27 11:18:32  おわり: 20261029_night_cs26_fam_p5_r1_s9_passon / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 11:18:47  おわり: 20261029_night_cs26_fam_p5_r2_s9_passon / completed / 終了コード 0 / 実時間 3.7 分
2026-10-27 11:18:47  --- まとめ ---
2026-10-27 11:18:47    completed: 60 回
2026-10-27 11:18:47    completed の実時間: 平均 3.9 分 / 最長 4.8 分
2026-10-27 11:18:47  判定を呼びます（/Volumes/KIOXIA/r2r2r-data/service_sim/unity で）: /opt/homebrew/Caskroom/miniforge/base/bin/python3 /Users/shimazueiichi/Robotics_UX/ServiceSim/tools/judge_night.py --runs 20261029_night_* --out night/20261029
2026-10-27 11:18:48  判定を書きました: /Volumes/KIOXIA/r2r2r-data/service_sim/unity/night/20261029/judge.md
2026-10-27 11:18:48  スリープの設定を戻しました
2026-10-27 11:18:48  === 夜の実行係を終わります（終了コード 0）===
```

### night/20261029/judge.md

```markdown
# 夜の結果の振り分け（20261029_night_*）

基準データ: 20261027_night_cs21_*（*_passon* を除く）（60回）／振り分けた回: 60回

| 振り分け | 回数 |
|---|---|
| 正常 | 0 |
| 試算を見直す候補 | 0 |
| Unity の作り | 0 |
| 回の失敗 | 0 |
| 基準なし | 60 |

## 回の失敗

- なし

## 基準なし（基準データに同じ条件がないので判定していない）

- fam_p1 スタッフ4 ロボット0台・すれ違い入
- fam_p1 スタッフ4 ロボット1台・すれ違い入
- fam_p1 スタッフ4 ロボット2台・すれ違い入
- fam_p5 スタッフ4 ロボット0台・すれ違い入
- fam_p5 スタッフ4 ロボット1台・すれ違い入
- fam_p5 スタッフ4 ロボット2台・すれ違い入

## Unity の作り（種の中央値が、基準の同じ条件の最大を超えた）

- なし

## 試算を見直す候補

### ロボットを増やしたときの向きが、種の過半数で試算と逆

- fam_p1 スタッフ4・すれ違い入 1→2台: 入店待ち（試算は短くなる）— 逆 6/10 種 — 新しく出た

### Unity÷試算 の大きさが、基準の同じ条件の範囲から外れた

- なし

## 記録（印にしない）：試算との差の大きさ（条件ごとの種の中央値・Unity÷試算）

| 条件 | 種の数 | 入店待ち | 料理の待ち | 席は空いているのに待った |
|---|---|---|---|---|
| fam_p1 スタッフ4 ロボット0台・すれ違い入 | 10 | 1.28 | 2.04 | 2.91 |
| fam_p1 スタッフ4 ロボット1台・すれ違い入 | 10 | 1.16 | 1.23 | 2.28 |
| fam_p1 スタッフ4 ロボット2台・すれ違い入 | 10 | 3.54 | 1.70 | 6.68 |
| fam_p5 スタッフ4 ロボット0台・すれ違い入 | 10 | 1.63 | 2.18 | 3.58 |
| fam_p5 スタッフ4 ロボット1台・すれ違い入 | 10 | 2.11 | 1.24 | 4.47 |
| fam_p5 スタッフ4 ロボット2台・すれ違い入 | 10 | 2.44 | 1.55 | 6.04 |

## すれ違い 入 と 切 の比べ（同じ夜・同じ条件・種の中央値。印にしない）

| 条件 | 入店待ち | 料理の待ち | 席は空いているのに待った | 動けなくなった | あきらめた | ロボットが運んだ割合 |
|---|---|---|---|---|---|---|
| fam_p1 スタッフ4 ロボット0台（0回） | - | - | - | - | - | - |
| fam_p1 スタッフ4 ロボット0台・すれ違い入（10回） | 4.21 | 0.94 | 2.78 | 0.00 | 12.50 | 0.00 |
| fam_p1 スタッフ4 ロボット1台（0回） | - | - | - | - | - | - |
| fam_p1 スタッフ4 ロボット1台・すれ違い入（10回） | 3.01 | 2.40 | 1.30 | 4.00 | 39.50 | 0.71 |
| fam_p1 スタッフ4 ロボット2台（0回） | - | - | - | - | - | - |
| fam_p1 スタッフ4 ロボット2台・すれ違い入（10回） | 4.17 | 1.62 | 2.57 | 25.00 | 66.50 | 0.92 |
| fam_p5 スタッフ4 ロボット0台（0回） | - | - | - | - | - | - |
| fam_p5 スタッフ4 ロボット0台・すれ違い入（10回） | 4.48 | 0.98 | 3.20 | 0.00 | 15.00 | 0.00 |
| fam_p5 スタッフ4 ロボット1台（0回） | - | - | - | - | - | - |
| fam_p5 スタッフ4 ロボット1台・すれ違い入（10回） | 3.84 | 2.44 | 2.17 | 6.00 | 45.00 | 0.67 |
| fam_p5 スタッフ4 ロボット2台（0回） | - | - | - | - | - | - |
| fam_p5 スタッフ4 ロボット2台・すれ違い入（10回） | 4.81 | 1.52 | 2.70 | 17.00 | 50.50 | 0.92 |

線: 立ち往生 30秒以上の止まり／向きは 5%・0.2分より小さい変化を数えない／外れは基準の同じ条件の最小〜最大
```

### night/20261029/runs.csv

```csv
folder,condition,seed,pass_by,tryno,launch_running,parallel,started_iso,ended_iso,started_unix,ended_unix,started_mono,ended_mono,wall_s,status,exit_code
20261029_night_cs26_fam_p1_r0_s0_passon,fam_p1_s4_r0_a42,0,on,1,1,4,2026-10-27T10:19:09,2026-10-27T10:22:29,1793063949.266,1793064149.387,289752.372,289952.493,200.121,completed,0
20261029_night_cs26_fam_p1_r1_s0_passon,fam_p1_s4_r1_a42,0,on,1,2,4,2026-10-27T10:19:14,2026-10-27T10:22:49,1793063954.279,1793064169.407,289757.385,289972.513,215.128,completed,0
20261029_night_cs26_fam_p5_r0_s0_passon,fam_p5_s4_r0_a42,0,on,1,4,4,2026-10-27T10:19:24,2026-10-27T10:22:49,1793063964.292,1793064169.407,289767.398,289972.513,205.115,completed,0
20261029_night_cs26_fam_p1_r2_s0_passon,fam_p1_s4_r2_a42,0,on,1,3,4,2026-10-27T10:19:19,2026-10-27T10:22:59,1793063959.285,1793064179.421,289762.391,289982.527,220.137,completed,0
20261029_night_cs26_fam_p5_r1_s0_passon,fam_p5_s4_r1_a42,0,on,1,4,4,2026-10-27T10:22:29,2026-10-27T10:25:59,1793064149.399,1793064359.578,289952.503,290162.682,210.179,completed,0
20261029_night_cs26_fam_p1_r0_s1_passon,fam_p1_s4_r0_a42,1,on,1,4,4,2026-10-27T10:22:54,2026-10-27T10:26:29,1793064174.421,1793064389.612,289977.525,290192.716,215.191,completed,0
20261029_night_cs26_fam_p1_r1_s1_passon,fam_p1_s4_r1_a42,1,on,1,4,4,2026-10-27T10:22:59,2026-10-27T10:26:49,1793064179.428,1793064409.632,289982.532,290212.736,230.204,completed,0
20261029_night_cs26_fam_p5_r2_s0_passon,fam_p5_s4_r2_a42,0,on,1,3,4,2026-10-27T10:22:49,2026-10-27T10:26:54,1793064169.414,1793064414.639,289972.518,290217.743,245.225,completed,0
20261029_night_cs26_fam_p1_r2_s1_passon,fam_p1_s4_r2_a42,1,on,1,4,4,2026-10-27T10:25:59,2026-10-27T10:29:49,1793064359.591,1793064589.822,290162.693,290392.924,230.231,completed,0
20261029_night_cs26_fam_p5_r0_s1_passon,fam_p5_s4_r0_a42,1,on,1,4,4,2026-10-27T10:26:29,2026-10-27T10:30:04,1793064389.618,1793064604.844,290192.720,290407.946,215.226,completed,0
20261029_night_cs26_fam_p5_r1_s1_passon,fam_p5_s4_r1_a42,1,on,1,4,4,2026-10-27T10:26:49,2026-10-27T10:30:29,1793064409.639,1793064629.873,290212.740,290432.974,220.234,completed,0
20261029_night_cs26_fam_p5_r2_s1_passon,fam_p5_s4_r2_a42,1,on,1,4,4,2026-10-27T10:26:54,2026-10-27T10:30:54,1793064414.648,1793064654.928,290217.750,290458.030,240.280,completed,0
20261029_night_cs26_fam_p1_r0_s2_passon,fam_p1_s4_r0_a42,2,on,1,4,4,2026-10-27T10:29:49,2026-10-27T10:34:05,1793064589.825,1793064845.139,290392.932,290648.246,255.314,completed,0
20261029_night_cs26_fam_p1_r1_s2_passon,fam_p1_s4_r1_a42,2,on,1,4,4,2026-10-27T10:30:04,2026-10-27T10:34:30,1793064604.843,1793064870.177,290407.950,290673.284,265.334,completed,0
20261029_night_cs26_fam_p5_r0_s2_passon,fam_p5_s4_r0_a42,2,on,1,4,4,2026-10-27T10:30:54,2026-10-27T10:35:10,1793064654.929,1793064910.218,290458.035,290713.324,255.289,completed,0
20261029_night_cs26_fam_p1_r2_s2_passon,fam_p1_s4_r2_a42,2,on,1,4,4,2026-10-27T10:30:29,2026-10-27T10:35:15,1793064629.873,1793064915.228,290432.980,290718.334,285.355,completed,0
20261029_night_cs26_fam_p1_r0_s3_passon,fam_p1_s4_r0_a42,3,on,1,4,4,2026-10-27T10:35:10,2026-10-27T10:38:35,1793064910.225,1793065115.466,290713.330,290918.571,205.241,completed,0
20261029_night_cs26_fam_p5_r1_s2_passon,fam_p5_s4_r1_a42,2,on,1,4,4,2026-10-27T10:34:05,2026-10-27T10:38:40,1793064845.150,1793065120.479,290648.255,290923.584,275.329,completed,0
20261029_night_cs26_fam_p1_r1_s3_passon,fam_p1_s4_r1_a42,3,on,1,4,4,2026-10-27T10:35:15,2026-10-27T10:38:45,1793064915.233,1793065125.488,290718.338,290928.592,210.255,completed,0
20261029_night_cs26_fam_p5_r2_s2_passon,fam_p5_s4_r2_a42,2,on,1,4,4,2026-10-27T10:34:30,2026-10-27T10:39:05,1793064870.183,1793065145.512,290673.289,290948.617,275.329,completed,0
20261029_night_cs26_fam_p5_r0_s3_passon,fam_p5_s4_r0_a42,3,on,1,4,4,2026-10-27T10:38:40,2026-10-27T10:42:00,1793065120.484,1793065320.697,290923.588,291123.801,200.213,completed,0
20261029_night_cs26_fam_p1_r2_s3_passon,fam_p1_s4_r2_a42,3,on,1,4,4,2026-10-27T10:38:35,2026-10-27T10:42:15,1793065115.476,1793065335.708,290918.579,291138.811,220.233,completed,0
20261029_night_cs26_fam_p5_r1_s3_passon,fam_p5_s4_r1_a42,3,on,1,4,4,2026-10-27T10:38:45,2026-10-27T10:42:45,1793065125.496,1793065365.733,290928.599,291168.837,240.238,completed,0
20261029_night_cs26_fam_p5_r2_s3_passon,fam_p5_s4_r2_a42,3,on,1,4,4,2026-10-27T10:39:05,2026-10-27T10:43:00,1793065145.522,1793065380.753,290948.625,291183.856,235.231,completed,0
20261029_night_cs26_fam_p1_r0_s4_passon,fam_p1_s4_r0_a42,4,on,1,4,4,2026-10-27T10:42:00,2026-10-27T10:46:00,1793065320.707,1793065560.927,291123.809,291364.029,240.220,completed,0
20261029_night_cs26_fam_p1_r1_s4_passon,fam_p1_s4_r1_a42,4,on,1,4,4,2026-10-27T10:42:15,2026-10-27T10:46:20,1793065335.714,1793065580.955,291138.816,291384.057,245.241,completed,0
20261029_night_cs26_fam_p5_r0_s4_passon,fam_p5_s4_r0_a42,4,on,1,4,4,2026-10-27T10:43:00,2026-10-27T10:47:00,1793065380.760,1793065620.988,291183.862,291424.089,240.228,completed,0
20261029_night_cs26_fam_p1_r2_s4_passon,fam_p1_s4_r2_a42,4,on,1,4,4,2026-10-27T10:42:45,2026-10-27T10:47:16,1793065365.740,1793065636.013,291168.841,291439.115,270.273,completed,0
20261029_night_cs26_fam_p5_r1_s4_passon,fam_p5_s4_r1_a42,4,on,1,4,4,2026-10-27T10:46:00,2026-10-27T10:50:26,1793065560.936,1793065826.225,291364.037,291629.325,265.288,completed,0
20261029_night_cs26_fam_p1_r0_s5_passon,fam_p1_s4_r0_a42,5,on,1,4,4,2026-10-27T10:47:00,2026-10-27T10:50:26,1793065620.997,1793065826.225,291424.097,291629.325,205.228,completed,0
20261029_night_cs26_fam_p5_r2_s4_passon,fam_p5_s4_r2_a42,4,on,1,4,4,2026-10-27T10:46:20,2026-10-27T10:50:46,1793065580.963,1793065846.253,291384.063,291649.354,265.290,completed,0
20261029_night_cs26_fam_p1_r1_s5_passon,fam_p1_s4_r1_a42,5,on,1,4,4,2026-10-27T10:47:16,2026-10-27T10:50:46,1793065636.019,1793065846.254,291439.119,291649.354,210.235,completed,0
20261029_night_cs26_fam_p5_r0_s5_passon,fam_p5_s4_r0_a42,5,on,1,4,4,2026-10-27T10:50:31,2026-10-27T10:53:56,1793065831.243,1793066036.411,291634.341,291839.509,205.168,completed,0
20261029_night_cs26_fam_p1_r2_s5_passon,fam_p1_s4_r2_a42,5,on,1,3,4,2026-10-27T10:50:26,2026-10-27T10:54:16,1793065826.233,1793066056.432,291629.332,291859.530,230.198,completed,0
20261029_night_cs26_fam_p5_r1_s5_passon,fam_p5_s4_r1_a42,5,on,1,3,4,2026-10-27T10:50:46,2026-10-27T10:54:21,1793065846.262,1793066061.440,291649.360,291864.538,215.178,completed,0
20261029_night_cs26_fam_p5_r2_s5_passon,fam_p5_s4_r2_a42,5,on,1,4,4,2026-10-27T10:50:51,2026-10-27T10:54:41,1793065851.265,1793066081.461,291654.364,291884.559,230.196,completed,0
20261029_night_cs26_fam_p1_r0_s6_passon,fam_p1_s4_r0_a42,6,on,1,4,4,2026-10-27T10:53:56,2026-10-27T10:58:06,1793066036.418,1793066286.678,291839.515,292089.775,250.260,completed,0
20261029_night_cs26_fam_p1_r1_s6_passon,fam_p1_s4_r1_a42,6,on,1,4,4,2026-10-27T10:54:16,2026-10-27T10:58:36,1793066056.439,1793066316.717,291859.535,292119.814,260.279,completed,0
20261029_night_cs26_fam_p5_r0_s6_passon,fam_p5_s4_r0_a42,6,on,1,4,4,2026-10-27T10:54:41,2026-10-27T10:58:56,1793066081.467,1793066336.747,291884.564,292139.844,255.280,completed,0
20261029_night_cs26_fam_p1_r2_s6_passon,fam_p1_s4_r2_a42,6,on,1,4,4,2026-10-27T10:54:21,2026-10-27T10:59:06,1793066061.447,1793066346.763,291864.543,292149.860,285.316,completed,0
20261029_night_cs26_fam_p5_r1_s6_passon,fam_p5_s4_r1_a42,6,on,1,4,4,2026-10-27T10:58:06,2026-10-27T11:02:31,1793066286.694,1793066551.974,292089.789,292355.069,265.280,completed,0
20261029_night_cs26_fam_p1_r0_s7_passon,fam_p1_s4_r0_a42,7,on,1,4,4,2026-10-27T10:58:56,2026-10-27T11:02:36,1793066336.754,1793066556.984,292139.849,292360.078,220.230,completed,0
20261029_night_cs26_fam_p1_r1_s7_passon,fam_p1_s4_r1_a42,7,on,1,4,4,2026-10-27T10:59:06,2026-10-27T11:03:02,1793066346.769,1793066582.007,292149.863,292385.102,235.239,completed,0
20261029_night_cs26_fam_p5_r2_s6_passon,fam_p5_s4_r2_a42,6,on,1,4,4,2026-10-27T10:58:36,2026-10-27T11:03:22,1793066316.726,1793066602.024,292119.821,292405.118,285.298,completed,0
20261029_night_cs26_fam_p5_r0_s7_passon,fam_p5_s4_r0_a42,7,on,1,4,4,2026-10-27T11:02:36,2026-10-27T11:06:17,1793066556.990,1793066777.169,292360.083,292580.262,220.179,completed,0
20261029_night_cs26_fam_p1_r2_s7_passon,fam_p1_s4_r2_a42,7,on,1,4,4,2026-10-27T11:02:31,2026-10-27T11:06:37,1793066551.982,1793066797.199,292355.075,292600.292,245.218,completed,0
20261029_night_cs26_fam_p5_r1_s7_passon,fam_p5_s4_r1_a42,7,on,1,4,4,2026-10-27T11:03:02,2026-10-27T11:07:02,1793066582.014,1793066822.219,292385.107,292625.312,240.205,completed,0
20261029_night_cs26_fam_p5_r2_s7_passon,fam_p5_s4_r2_a42,7,on,1,4,4,2026-10-27T11:03:22,2026-10-27T11:07:32,1793066602.032,1793066852.251,292405.125,292655.344,250.219,completed,0
20261029_night_cs26_fam_p1_r0_s8_passon,fam_p1_s4_r0_a42,8,on,1,4,4,2026-10-27T11:06:17,2026-10-27T11:09:47,1793066777.181,1793066987.393,292580.272,292790.484,210.212,completed,0
20261029_night_cs26_fam_p1_r1_s8_passon,fam_p1_s4_r1_a42,8,on,1,4,4,2026-10-27T11:06:37,2026-10-27T11:10:17,1793066797.205,1793067017.423,292600.296,292820.514,220.218,completed,0
20261029_night_cs26_fam_p5_r0_s8_passon,fam_p5_s4_r0_a42,8,on,1,4,4,2026-10-27T11:07:32,2026-10-27T11:11:07,1793066852.258,1793067067.479,292655.349,292870.571,215.222,completed,0
20261029_night_cs26_fam_p1_r2_s8_passon,fam_p1_s4_r2_a42,8,on,1,4,4,2026-10-27T11:07:02,2026-10-27T11:11:12,1793066822.226,1793067072.487,292625.317,292875.579,250.262,completed,0
20261029_night_cs26_fam_p5_r1_s8_passon,fam_p5_s4_r1_a42,8,on,1,4,4,2026-10-27T11:09:47,2026-10-27T11:13:27,1793066987.399,1793067207.614,292790.489,293010.704,220.215,completed,0
20261029_night_cs26_fam_p5_r2_s8_passon,fam_p5_s4_r2_a42,8,on,1,4,4,2026-10-27T11:10:17,2026-10-27T11:14:12,1793067017.431,1793067252.667,292820.521,293055.757,235.237,completed,0
20261029_night_cs26_fam_p1_r0_s9_passon,fam_p1_s4_r0_a42,9,on,1,4,4,2026-10-27T11:11:07,2026-10-27T11:14:52,1793067067.487,1793067292.712,292870.576,293095.802,225.226,completed,0
20261029_night_cs26_fam_p1_r1_s9_passon,fam_p1_s4_r1_a42,9,on,1,4,4,2026-10-27T11:11:12,2026-10-27T11:15:07,1793067072.497,1793067307.740,292875.586,293110.830,235.243,completed,0
20261029_night_cs26_fam_p1_r2_s9_passon,fam_p1_s4_r2_a42,9,on,1,4,4,2026-10-27T11:13:27,2026-10-27T11:17:42,1793067207.626,1793067462.872,293010.714,293265.961,255.247,completed,0
20261029_night_cs26_fam_p5_r0_s9_passon,fam_p5_s4_r0_a42,9,on,1,4,4,2026-10-27T11:14:12,2026-10-27T11:17:57,1793067252.677,1793067477.889,293055.765,293280.977,225.213,completed,0
20261029_night_cs26_fam_p5_r1_s9_passon,fam_p5_s4_r1_a42,9,on,1,4,4,2026-10-27T11:14:52,2026-10-27T11:18:32,1793067292.720,1793067512.931,293095.808,293316.019,220.211,completed,0
20261029_night_cs26_fam_p5_r2_s9_passon,fam_p5_s4_r2_a42,9,on,1,4,4,2026-10-27T11:15:07,2026-10-27T11:18:47,1793067307.748,1793067527.941,293110.835,293331.028,220.193,completed,0
```


## 台帳の行（unity_ledger_v1.csv・この記事で回した回）

205行（ヘッダ1行と、122行目〜325行目。busycheck・fixcheck・3晩の夜・確かめの回）。

### unity_ledger_v1.csv の122〜325行目

```csv
timestamp,run_folder,status,unity_cs,layout_generation,params_model,store,staff,robots,arrivals_per_h,mode,trays,fragile_by,seed,staff_first_yield,yield_order_enabled,one_robot_per_aisle,robot_first_wait_s,everyone_left,total_minutes,wall_seconds,entry_wait_mean,staff_wait_mean,dish_wait_mean,cold_share,robot_share,staff_util,robot_util,walk_km_per_staff,site_entry_wait_mean,site_staff_wait_mean,site_dish_wait_mean,site_robot_share,robot_speed_mps,pause_rate,pause_s_mean,stuck_rate,trips,timeouts,stuck_count,stop_s_total,corner_stop_time_share,gave_up,warped,table_freed,rescues,late_staff_dishes,staff_yield_share_of_staff_time,ledger_schema_version,pay_wait_mean,site_pay_wait_mean,blocked_wait_mean,site_blocked_wait_mean,turn_mean,site_turn_mean,clear_wait_mean_s,clear_wait_max_s,clear_wait_over_count,pay_job_wait_mean_s
2026-10-26T22:14:17,20261027_night_cs21_fam_p1_r0_s0,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,0,1,1,1,120,1,172.5542,194.885,1.4864,0.2944,0.829,0,0,0.66,0,2.7885,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,6,9,0,0,0,0,2,2.1598,12.2785,0,0.7327,3.3458,2.4872,63.7974,302.9956,0,92.9196
2026-10-26T22:14:40,20261027_night_cs21_fam_p1_r1_s0,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,0,1,1,1,120,1,174.9465,212.5,1.6292,0.4055,2.1114,0.1688,0.7938,0.5909,0.741,2.2081,2.5992,0.572,1.9487,0.7204,0.5797,,3.8196,,67,0,4,981.1951,0,35,48,0,4,33,0.0335,2,2.7096,7.9893,0,0.4827,3.0394,2.1338,38.1375,459.8486,0,104.9457
2026-10-26T22:14:45,20261027_night_cs21_fam_p5_r0_s0,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,0,1,1,1,120,1,173.6043,207.8589,1.5122,0.331,0.8133,0,0,0.6661,0,2.8429,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,8,13,0,0,0,0,2,2.1229,11.7376,0,0.6821,3.723,2.5036,83.032,299.342,0,92.0491
2026-10-26T22:14:58,20261027_night_cs21_fam_p1_r2_s0,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,0,1,1,1,120,1,172.355,226.2467,1.5647,0.2622,1.6317,0.175,0.8563,0.6546,0.6775,2.2238,1.1788,0.3853,0.9488,0.9912,0.5583,,1.184,,108,3,33,4298.596,0,56,54,0,33,23,0.0485,2,2.6685,7.9844,0,0.322,4.0129,2.0672,84.3712,484.9482,0,105.955
2026-10-26T22:18:10,20261027_night_cs21_fam_p5_r1_s0,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,0,1,1,1,120,1,172.6156,227.2205,1.4068,0.1913,1.863,0.15,0.8125,0.5812,0.7441,2.2132,1.815,0.4851,1.9698,0.721,0.5827,,2.0972,,69,0,4,617.6973,0,23,41,0,4,30,0.0298,2,2.4838,8.0624,0,0.3915,2.5959,2.1404,12.0715,116.9414,0,101.5082
2026-10-26T22:18:45,20261027_night_cs21_fam_p5_r2_s0,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,0,1,1,1,120,1,174.2824,242.5067,1.5402,0.3483,1.0214,0.025,0.9875,0.5948,0.6474,2.0801,1.9669,0.4479,0.9839,0.991,0.5686,,1.595,,129,1,18,3427.149,0,38,49,1,18,2,0.0361,2,2.0406,7.9555,0,0.3816,4.5839,2.0967,129.2542,551.0391,0,82.6225
2026-10-26T22:18:58,20261027_night_cs21_fam_p1_r0_s1,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,1,1,1,1,120,1,184.9572,235.7075,3.8691,2.6631,1.0446,0,0,0.7743,0,3.2202,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,5,9,0,0,0,0,2,4.6625,12.2785,2.1342,0.7327,10.8906,2.4872,519.3842,1186.447,39,139.8171
2026-10-26T22:22:12,20261027_night_cs21_fam_p1_r1_s1,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,1,1,1,1,120,1,179.1409,238.9203,1.8721,0.6438,2.5657,0.267,0.6748,0.7846,0.7758,2.5504,2.5992,0.572,1.9487,0.7204,0.5791,,3.0583,,66,1,12,1252.741,0,62,47,3,12,67,0.0445,2,5.5292,7.9893,0.1967,0.4827,6.2632,2.1338,266.9652,925.3711,15,153.172
2026-10-26T22:23:03,20261027_night_cs21_fam_p5_r0_s1,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,1,1,1,1,120,1,185.8421,239.989,4.1538,2.9529,0.9895,0.0049,0,0.778,0,3.2708,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,6,12,0,0,0,0,2,4.4547,11.7376,2.489,0.6821,11.682,2.5036,565.4123,1251.542,42,132.7109
2026-10-26T22:23:10,20261027_night_cs21_fam_p1_r2_s1,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,1,1,1,1,120,1,189.3575,262.4983,1.506,0.2724,1.3163,0.068,0.9272,0.6751,0.5957,2.5471,1.1788,0.3853,0.9488,0.9912,0.5563,,1.05,,134,2,10,2994.822,0,44,50,0,10,15,0.0416,2,10.6629,7.9844,0,0.322,2.5894,2.0672,15.8935,165.5508,0,183.2435
2026-10-26T22:26:00,20261027_night_cs21_fam_p5_r1_s1,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,1,1,1,1,120,1,181.7656,227.3163,2.7728,1.5683,2.1968,0.2184,0.6942,0.7531,0.7124,2.5809,1.815,0.4851,1.9698,0.721,0.5818,,3.0384,,71,0,3,663.2465,0,46,29,0,3,63,0.0488,2,4.8936,8.0624,1.1969,0.3915,6.8421,2.1404,275.9318,806.2881,8,150.7892
2026-10-26T22:27:02,20261027_night_cs21_fam_p1_r2_s1_passon,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,1,1,1,1,120,1,182.1341,229.2601,1.9578,0.6968,2.3426,0.1942,0.8301,0.7476,0.6818,2.4482,1.1788,0.3853,0.9488,0.9912,0.3489,,1.558,,107,6,17,2744.222,0,50,50,0,17,35,0.0494,2,5.3734,7.9844,0.4244,0.322,5.4422,2.0672,187.0358,646.6143,1,152.13
2026-10-26T22:27:14,20261027_night_cs21_fam_p5_r2_s1,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,1,1,1,1,120,1,183.19,246.4411,2.1694,0.9208,1.9155,0.1845,0.9029,0.7565,0.6284,2.4744,1.9669,0.4479,0.9839,0.991,0.5682,,1.4264,,127,2,21,3390.592,0,80,63,0,21,20,0.0472,2,6.7145,7.9555,0.2003,0.3816,4.0305,2.0967,99.6279,360.8848,0,186.0323
2026-10-26T22:29:10,20261027_night_cs21_fam_p1_r2_s0_passon,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,0,1,1,1,120,0,720.0001,861.0263,1.4628,0.2508,1.1852,0.0506,0.9557,0.1403,0.553,2.0072,1.1788,0.3853,0.9488,0.9912,0.0935,,1.9255,,176,59,3,2198.085,0,29,48,0,3,7,0.0109,2,3.1884,7.9844,0,0.322,2.9024,2.0672,25.7785,389.8711,0,112.6262
2026-10-26T22:30:21,20261027_night_cs21_fam_p1_r0_s2,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,2,1,1,1,120,1,213.243,257.8144,13.061,7.6359,0.948,0,0,0.8296,0,3.997,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,27,11,0,0,0,0,2,5.9788,12.2785,7.26,0.7327,10.628,2.4872,502.2299,1137.838,47,150.5584
2026-10-26T22:31:36,20261027_night_cs21_fam_p1_r1_s2,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,2,1,1,1,120,1,210.1128,268.0979,11.3364,5.0314,2.2882,0.2441,0.7165,0.778,0.798,3.1662,2.5992,0.572,1.9487,0.7204,0.58,,3.2445,,85,0,3,942.5958,0,63,62,0,3,72,0.0447,2,7.5642,7.9893,4.8015,0.4827,6.6396,2.1338,257.939,837.2671,8,167.2857
2026-10-26T22:32:02,20261027_night_cs21_fam_p1_r2_s2,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,2,1,1,1,120,1,213.7435,284.3109,14.1054,5.6875,1.92,0.2126,0.8228,0.8475,0.7352,3.1698,1.1788,0.3853,0.9488,0.9912,0.5623,,1.8457,,144,5,43,5610.996,0,98,79,1,48,45,0.0603,2,9.2429,7.9844,5.2852,0.322,7.7729,2.0672,321.7958,898.2275,11,179.5564
2026-10-26T22:33:28,20261027_night_cs21_fam_p5_r0_s2,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,2,1,1,1,120,1,213.8581,255.0388,13.2605,7.9704,1.0783,0,0,0.8331,0,4.0492,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,24,24,0,0,0,0,2,5.6617,11.7376,7.5104,0.6821,11.2128,2.5036,537.0508,1183.11,52,146.4359
2026-10-26T22:34:51,20261027_night_cs21_fam_p5_r1_s2,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,2,1,1,1,120,1,211.0574,267.6163,11.3797,5.1784,2.3323,0.2441,0.6929,0.7795,0.8123,3.191,1.815,0.4851,1.9698,0.721,0.5798,,2.9457,,80,0,8,1193.244,0,58,49,1,8,78,0.0324,2,6.6278,8.0624,4.9526,0.3915,7.9117,2.1404,358.8793,1225.744,24,161.5986
2026-10-26T22:36:26,20261027_night_cs21_fam_p5_r2_s2,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,2,1,1,1,120,1,215.3165,287.8278,11.6796,4.0568,1.7843,0.1614,0.8504,0.8235,0.7478,3.1073,1.9669,0.4479,0.9839,0.991,0.564,,1.2164,,147,7,51,5619.501,0,116,67,0,51,38,0.0598,2,11.0803,7.9555,3.748,0.3816,6.1036,2.0967,207.2517,676.4971,2,185.6859
2026-10-26T22:36:39,20261027_night_cs21_fam_p1_r2_s2_passon,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,2,1,1,1,120,1,203.7842,271.2037,10.3902,4.7404,1.3215,0.0787,0.9567,0.7868,0.719,2.8395,1.1788,0.3853,0.9488,0.9912,0.5652,,1.8755,,166,1,22,4001.198,0,66,71,1,22,11,0.0558,2,6.9356,7.9844,4.5221,0.322,6.578,2.0672,254.1989,790.6499,4,164.7153
2026-10-26T22:36:55,20261027_night_cs21_fam_p1_r0_s3,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,3,1,1,1,120,1,177.5571,202.2568,1.6464,0.4537,0.9319,0,0,0.7722,0,3.1756,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,11,7,0,0,0,0,2,4.6537,12.2785,0.0078,0.7327,6.2741,2.4872,240.3882,897.6299,12,152.2017
2026-10-26T22:38:30,20261027_night_cs21_fam_p1_r1_s3,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,3,1,1,1,120,1,179.8655,216.664,1.4599,0.2233,2.3534,0.2694,0.6528,0.7329,0.7301,2.6262,2.5992,0.572,1.9487,0.7204,0.5778,,3.4725,,60,0,9,1210.642,0,48,49,0,9,67,0.0327,2,6.1979,7.9893,0,0.4827,4.0282,2.1338,99.3212,453.2227,0,152.0646
2026-10-26T22:40:05,20261027_night_cs21_fam_p5_r0_s3,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,3,1,1,1,120,1,178.7217,201.5592,1.7583,0.5581,1.0484,0,0,0.7765,0,3.2145,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,10,11,0,0,0,0,2,4.3283,11.7376,0.0769,0.6821,7.43,2.5036,310.3537,1049.385,18,142.7137
2026-10-26T22:40:11,20261027_night_cs21_fam_p1_r2_s3,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,3,1,1,1,120,1,179.2496,222.2021,1.6807,0.4471,1.2184,0.0933,0.943,0.7087,0.645,2.4579,1.1788,0.3853,0.9488,0.9912,0.5697,,1.8054,,139,1,19,3137.02,0,44,56,1,19,11,0.0507,2,6.9388,7.9844,0.0218,0.322,5.3914,2.0672,179.5193,575.1445,0,168.6912
2026-10-26T22:40:45,20261027_night_cs21_fam_p5_r1_s3,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,3,1,1,1,120,1,178.134,226.9901,1.6712,0.4556,2.6301,0.3264,0.6477,0.7545,0.7322,2.6552,1.815,0.4851,1.9698,0.721,0.5833,,3.0616,,61,1,10,1050.542,0,47,52,0,10,68,0.0254,2,4.927,8.0624,0,0.3915,4.9858,2.1404,156.7841,640.9753,1,147.4263
2026-10-26T22:42:21,20261027_night_cs21_fam_p5_r2_s3,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,3,1,1,1,120,1,176.776,227.0842,1.5232,0.2701,1.2635,0.0518,0.943,0.6832,0.6401,2.3672,1.9669,0.4479,0.9839,0.991,0.5657,,1.2559,,126,1,5,2857.179,0,30,49,0,5,11,0.0359,2,5.3479,7.9555,0,0.3816,3.0663,2.0967,37.8338,234.3311,0,150.6239
2026-10-26T22:44:10,20261027_night_cs21_fam_p1_r0_s4,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,4,1,1,1,120,1,203.5634,236.338,9.2547,6.7329,1.0239,0,0,0.802,0,3.7105,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,16,11,0,0,0,0,2,6.4009,12.2785,6.2467,0.7327,10.8456,2.4872,515.3531,1165.38,44,153.6452
2026-10-26T22:44:16,20261027_night_cs21_fam_p1_r2_s3_passon,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,3,1,1,1,120,1,192.9891,247.1949,3.8532,2.61,1.8623,0.2176,0.8912,0.7303,0.6389,2.4586,1.1788,0.3853,0.9488,0.9912,0.5617,,1.6816,,118,3,28,3685.803,0,84,65,0,28,21,0.0602,2,5.1092,7.9844,2.1279,0.322,14.0245,2.0672,758.9402,1621.541,61,132.6969
2026-10-26T22:44:47,20261027_night_cs21_fam_p1_r1_s4,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,4,1,1,1,120,1,199.5683,238.7284,5.7563,2.5006,2.4074,0.2996,0.7173,0.7414,0.7403,2.8411,2.5992,0.572,1.9487,0.7204,0.5794,,2.5296,,73,0,1,598.8486,0,37,51,0,1,67,0.0387,2,8.6079,7.9893,2.3379,0.4827,4.2801,2.1338,116.6938,361.4326,0,169.63
2026-10-26T22:46:29,20261027_night_cs21_fam_p1_r2_s4,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,4,1,1,1,120,1,200.5943,244.9382,8.279,4.6551,1.3598,0.1308,0.9409,0.7788,0.6937,2.7212,1.1788,0.3853,0.9488,0.9912,0.5602,,1.2052,,161,1,15,3991.949,0,73,68,1,15,14,0.0653,2,7.6369,7.9844,4.2708,0.322,7.8685,2.0672,325.2006,901.6646,20,163.9397
2026-10-26T22:48:01,20261027_night_cs21_fam_p5_r0_s4,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,4,1,1,1,120,1,206.1226,227.5142,9.5352,7.0703,0.968,0,0,0.7953,0,3.7515,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,15,14,0,0,0,0,2,6.3026,11.7376,6.6078,0.6821,11.5869,2.5036,558.5404,1247.608,47,146.9018
2026-10-26T22:48:29,20261027_night_cs21_fam_p5_r1_s4,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,4,1,1,1,120,1,207.2033,249.9056,9.9354,6.5061,2.5903,0.2996,0.6878,0.7738,0.7724,3.0467,1.815,0.4851,1.9698,0.721,0.5825,,2.6415,,74,0,8,979.644,0,55,54,0,8,74,0.0522,2,6.6654,8.0624,6.2011,0.3915,10.9998,2.1404,533.3358,1444.187,52,139.5238
2026-10-26T22:49:04,20261027_night_cs21_fam_p5_r2_s4,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,4,1,1,1,120,1,203.6249,255.3521,9.2776,5.6304,1.4167,0.1266,0.9409,0.7508,0.6925,2.7194,1.9669,0.4479,0.9839,0.991,0.5654,,1.1706,,156,1,19,3875.403,0,61,74,1,19,14,0.0546,2,7.4044,7.9555,5.3835,0.3816,8.0847,2.0967,336.4939,906.2461,26,155.2855
2026-10-26T22:50:46,20261027_night_cs21_fam_p1_r2_s4_passon,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,4,1,1,1,120,1,201.548,251.6384,7.9299,4.6231,1.2263,0.0759,0.9409,0.778,0.7046,2.8391,1.1788,0.3853,0.9488,0.9912,0.5397,,1.4644,,162,2,19,3854.497,0,58,75,0,19,14,0.057,2,7.8839,7.9844,4.4474,0.322,8.9351,2.0672,386.1646,1117.767,38,157.5567
2026-10-26T22:51:28,20261027_night_cs21_fam_p1_r0_s5,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,5,1,1,1,120,1,183.268,204.2018,1.6386,0.442,0.845,0,0,0.725,0,3.2379,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,11,13,0,0,0,0,2,3.6711,12.2785,0,0.7327,4.0614,2.4872,106.301,485.0479,0,151.5594
2026-10-26T22:52:07,20261027_night_cs21_fam_p1_r1_s5,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,5,1,1,1,120,1,182.8762,213.504,1.4286,0.1887,2.0733,0.1799,0.7196,0.6736,0.7556,2.5574,2.5992,0.572,1.9487,0.7204,0.5792,,3.6051,,75,0,7,1155.793,0,48,44,0,7,53,0.0292,2,3.157,7.9893,0,0.4827,3.2274,2.1338,52.2394,506.3145,0,132.3809
2026-10-26T22:52:54,20261027_night_cs21_fam_p1_r2_s5,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,5,1,1,1,120,1,182.8239,224.5622,1.541,0.2701,1.5283,0.0952,0.9048,0.6896,0.7181,2.4323,1.1788,0.3853,0.9488,0.9912,0.5661,,1.8906,,126,4,47,5058.852,0,40,60,1,47,18,0.0382,2,3.2122,7.9844,0,0.322,3.0149,2.0672,41.9386,550.8896,0,145.0892
2026-10-26T22:54:14,20261027_night_cs21_fam_p5_r0_s5,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,5,1,1,1,120,1,182.6662,205.2777,1.5612,0.3713,0.7955,0,0,0.7285,0,3.206,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,13,12,0,0,0,0,2,3.5999,11.7376,0,0.6821,4.705,2.5036,144.2915,658.916,3,151.125
2026-10-26T22:55:18,20261027_night_cs21_fam_p5_r1_s5,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,5,1,1,1,120,1,187.2366,223.7047,1.8091,0.56,2.2473,0.254,0.6825,0.6888,0.7186,2.6551,1.815,0.4851,1.9698,0.721,0.5827,,2.9205,,71,1,10,1070.693,0,49,49,0,10,60,0.0443,2,5.3924,8.0624,0,0.3915,4.5237,2.1404,128.9964,1088.133,2,167.5141
2026-10-26T22:55:58,20261027_night_cs21_fam_p5_r2_s5,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,5,1,1,1,120,1,183.1095,228.5524,1.6249,0.3953,1.2707,0.1005,0.9365,0.6796,0.6847,2.3986,1.9669,0.4479,0.9839,0.991,0.5615,,1.1327,,134,1,22,4206.054,0,60,47,1,22,12,0.0383,2,4.1185,7.9555,0,0.3816,4.2401,2.0967,122.8728,874.3711,2,164.3311
2026-10-26T22:56:43,20261027_night_cs21_fam_p1_r2_s5_passon,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,5,1,1,1,120,1,183.3668,223.3978,1.4662,0.2085,1.3896,0.1005,0.9048,0.6923,0.7052,2.4225,1.1788,0.3853,0.9488,0.9912,0.5647,,1.8286,,129,2,42,4848.043,0,55,50,0,42,18,0.0457,2,3.8537,7.9844,0,0.322,3.398,2.0672,55.5586,371.7422,0,154.8568
2026-10-26T22:58:34,20261027_night_cs21_fam_p1_r0_s6,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,6,1,1,1,120,1,220.3725,254.6861,14.8162,7.4466,1.0398,0,0,0.7814,0,3.9114,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,11,14,0,0,0,0,2,4.156,12.2785,6.9896,0.7327,11.3178,2.4872,543.8284,1155.07,48,132.8686
2026-10-26T22:59:52,20261027_night_cs21_fam_p1_r1_s6,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,6,1,1,1,120,1,227.4124,272.5607,14.3024,6.6608,2.5289,0.3682,0.6628,0.7418,0.7913,3.1437,2.5992,0.572,1.9487,0.7204,0.5808,,4.3688,,86,0,10,1631.537,0,71,63,0,10,87,0.0533,2,4.0409,7.9893,6.3128,0.4827,11.5463,2.1338,556.1459,1543.248,45,116.7311
2026-10-26T23:00:38,20261027_night_cs21_fam_p1_r2_s6,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,6,1,1,1,120,1,218.3978,278.9178,13.0508,4.9976,1.5744,0.1512,0.8953,0.7509,0.697,2.9252,1.1788,0.3853,0.9488,0.9912,0.5651,,1.2573,,162,3,30,4406.668,0,73,60,2,30,27,0.0639,2,6.3549,7.9844,4.7357,0.322,7.1908,2.0672,291.337,1046.994,5,159.7834
2026-10-26T23:01:03,20261027_night_cs21_fam_p5_r0_s6,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,6,1,1,1,120,1,221.9472,258.3525,15.5624,8.5722,0.9996,0,0,0.7877,0,4.0302,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,18,13,0,0,0,0,2,3.5186,11.7376,8.0755,0.6821,12.7958,2.5036,630.9105,1268.376,56,121.6181
2026-10-26T23:03:22,20261027_night_cs21_fam_p5_r1_s6,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,6,1,1,1,120,1,218.0774,282.2868,12.0516,4.1902,2.4264,0.2907,0.6899,0.7295,0.7818,3.1015,1.815,0.4851,1.9698,0.721,0.5832,,3.1115,,83,0,5,922.5452,0,47,45,0,5,80,0.0342,2,5.8771,8.0624,3.9308,0.3915,5.8204,2.1404,211.8048,696.2695,5,151.4906
2026-10-26T23:04:59,20261027_night_cs21_fam_p5_r2_s6,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,6,1,1,1,120,1,221.7911,304.5093,10.0385,1.9535,1.3393,0.0853,0.9535,0.7029,0.6614,2.8779,1.9669,0.4479,0.9839,0.991,0.5698,,1.2864,,172,2,20,3562.784,0,54,76,1,20,12,0.0426,2,10.6351,7.9555,1.7219,0.3816,3.3379,2.0967,65.4196,532.6611,0,161.4007
2026-10-26T23:05:00,20261027_night_cs21_fam_p1_r0_s7,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,7,1,1,1,120,1,196.0089,234.6153,3.8562,1.8501,1.0486,0.0144,0,0.7394,0,3.4569,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,19,12,0,0,0,0,2,8.3709,12.2785,1.391,0.7327,5.4506,2.4872,190.7637,671.7656,1,150.5718
2026-10-26T23:05:38,20261027_night_cs21_fam_p1_r2_s6_passon,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,6,1,1,1,120,1,222.3938,298.4749,10.8173,3.0931,1.5722,0.1357,0.938,0.7538,0.7034,2.8285,1.1788,0.3853,0.9488,0.9912,0.5629,,1.4765,,164,2,24,4262.728,0,95,91,2,24,16,0.0627,2,7.7101,7.9844,2.78,0.322,6.8728,2.0672,259.8622,1051.377,17,168.0138
2026-10-26T23:07:24,20261027_night_cs21_fam_p1_r1_s7,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,7,1,1,1,120,1,197.7454,238.4985,3.371,1.1785,2.4516,0.3397,0.6746,0.6803,0.7029,2.7471,2.5992,0.572,1.9487,0.7204,0.5789,,2.8624,,67,0,4,852.7463,0,44,52,0,4,68,0.0442,2,8.7521,7.9893,0.8971,0.4827,3.6172,2.1338,71.3051,379.4619,0,151.7628
2026-10-26T23:08:56,20261027_night_cs21_fam_p5_r0_s7,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,7,1,1,1,120,1,194.2367,225.8777,3.9886,2.0091,0.8669,0,0,0.7473,0,3.4693,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,18,17,0,0,0,0,2,8.0783,11.7376,1.6738,0.6821,6.1125,2.5036,230.1493,709.916,3,145.6996
2026-10-26T23:09:18,20261027_night_cs21_fam_p1_r2_s7,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,7,1,1,1,120,1,199.2728,253.282,2.9642,0.8707,1.5436,0.1196,0.933,0.6586,0.6398,2.6534,1.1788,0.3853,0.9488,0.9912,0.5595,,1.2003,,132,1,17,3877.567,0,63,70,0,17,14,0.05,2,10.471,7.9844,0.6195,0.322,3.31,2.0672,49.8439,304.4561,0,155.9928
2026-10-26T23:09:50,20261027_night_cs21_fam_p5_r1_s7,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,7,1,1,1,120,1,204.4259,249.6207,6.1207,3.7103,2.7704,0.4354,0.6603,0.7262,0.74,2.6904,1.815,0.4851,1.9698,0.721,0.5807,,2.9688,,63,0,15,1452.938,0,86,72,1,15,71,0.0727,2,6.0795,8.0624,3.144,0.3915,9.2399,2.1404,402.6342,1029.513,25,139.7068
2026-10-26T23:11:40,20261027_night_cs21_fam_p5_r2_s7,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,7,1,1,1,120,1,197.3279,255.229,3.6193,1.517,1.7371,0.1531,0.9091,0.688,0.6667,2.6836,1.9669,0.4479,0.9839,0.991,0.5657,,1.2957,,117,1,30,4141.58,0,83,74,1,30,19,0.0483,2,8.8349,7.9555,1.2101,0.3816,3.8556,2.0967,90.7986,580.4238,0,143.9318
2026-10-26T23:12:57,20261027_night_cs21_fam_p1_r0_s8,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,8,1,1,1,120,1,186.8581,217.007,4.298,3.092,0.8563,0,0,0.7557,0,3.264,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,15,10,0,0,0,0,2,3.5411,12.2785,2.8331,0.7327,11.6643,2.4872,563.7341,1210.901,41,126.906
2026-10-26T23:13:18,20261027_night_cs21_fam_p1_r2_s7_passon,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,7,1,1,1,120,1,199.9601,257.393,6.6727,4.4762,1.8129,0.1675,0.9043,0.7153,0.6856,2.5466,1.1788,0.3853,0.9488,0.9912,0.5603,,1.3448,,133,1,33,4632.172,0,74,75,1,33,20,0.0463,2,5.0662,7.9844,4.1405,0.322,10.1387,2.0672,500.0966,1416.495,51,118.4322
2026-10-26T23:13:53,20261027_night_cs21_fam_p1_r1_s8,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,8,1,1,1,120,1,186.0264,237.5712,6.1634,4.9025,2.8771,0.3776,0.648,0.7503,0.7597,2.5948,2.5992,0.572,1.9487,0.7204,0.5799,,4.2864,,59,1,14,1582.237,0,69,57,0,14,69,0.041,2,4.8995,7.9893,4.5711,0.4827,8.9155,2.1338,368.0914,825.3135,32,152.6053
2026-10-26T23:15:44,20261027_night_cs21_fam_p1_r2_s8,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,8,1,1,1,120,1,181.8685,239.0378,3.112,1.8534,1.253,0.0561,0.9745,0.7204,0.6643,2.3888,1.1788,0.3853,0.9488,0.9912,0.5671,,1.9272,,133,1,22,3448.72,0,67,76,2,22,5,0.053,2,7.3426,7.9844,1.7114,0.322,5.3644,2.0672,190.4198,652.292,3,170.2358
2026-10-26T23:16:43,20261027_night_cs21_fam_p5_r0_s8,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,8,1,1,1,120,1,186.6,222.8344,4.2946,3.096,0.9264,0,0,0.7601,0,3.2625,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,14,15,0,0,0,0,2,3.4094,11.7376,2.7729,0.6821,11.8115,2.5036,570.6215,1199.994,39,121.6913
2026-10-26T23:17:16,20261027_night_cs21_fam_p5_r1_s8,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,8,1,1,1,120,1,184.294,235.3056,3.8469,2.6274,2.4674,0.3061,0.699,0.7142,0.7379,2.5479,1.815,0.4851,1.9698,0.721,0.5809,,3.759,,64,0,5,991.8951,0,50,63,0,5,59,0.0371,2,5.0049,8.0624,2.3002,0.3915,6.7834,2.1404,259.9404,745.9746,9,156.5213
2026-10-26T23:17:57,20261027_night_cs21_fam_p5_r2_s8,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,8,1,1,1,120,1,182.8854,241.6526,1.754,0.502,1.2853,0.0714,0.9643,0.6795,0.6507,2.3106,1.9669,0.4479,0.9839,0.991,0.5689,,1.6624,,138,2,22,3307.612,0,52,71,0,22,7,0.0212,2,8.7259,7.9555,0.2575,0.3816,3.4629,2.0967,66.2054,506.1152,0,173.7215
2026-10-26T23:19:47,20261027_night_cs21_fam_p1_r2_s8_passon,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,8,1,1,1,120,1,185.2428,235.9261,5.4225,4.1965,1.5358,0.1224,0.9337,0.7262,0.645,2.2952,1.1788,0.3853,0.9488,0.9912,0.5639,,2.1145,,127,2,17,3260.067,0,84,79,2,17,13,0.0647,2,5.5368,7.9844,3.9015,0.322,9.1789,2.0672,420.8375,1235.854,37,156.1481
2026-10-26T23:20:41,20261027_night_cs21_fam_p1_r0_s9,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,9,1,1,1,120,1,201.6194,235.7205,8.2251,6.8971,0.9331,0,0,0.781,0,3.5604,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,11,18,0,0,0,0,2,3.1203,12.2785,6.462,0.7327,13.8316,2.4872,693.1204,1382.031,52,124.6805
2026-10-26T23:21:21,20261027_night_cs21_fam_p1_r1_s9,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,9,1,1,1,120,1,195.6976,240.6133,6.265,4.5806,2.65,0.3584,0.6549,0.7819,0.774,2.957,2.5992,0.572,1.9487,0.7204,0.5802,,4.3064,,61,1,14,1577.087,0,55,54,0,14,78,0.0528,2,4.2305,7.9893,4.1185,0.4827,9.0308,2.1338,420.0292,1215.234,35,154.1143
2026-10-26T23:22:09,20261027_night_cs21_fam_p1_r2_s9,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,9,1,1,1,120,1,191.5024,248.7614,4.2512,2.3498,1.46,0.1018,0.9646,0.7194,0.6939,2.4472,1.1788,0.3853,0.9488,0.9912,0.5541,,0.9613,,147,2,12,3637.191,0,55,79,0,12,8,0.0462,2,8.038,7.9844,2.059,0.322,4.7751,2.0672,155.2933,671.3677,1,186.1983
2026-10-26T23:23:42,20261027_night_cs21_fam_p5_r0_s9,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,9,1,1,1,120,1,199.9709,231.6151,8.5186,7.1352,1.0581,0.0044,0,0.788,0,3.5925,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,13,12,0,0,0,0,2,3.4275,11.7376,6.615,0.6821,13.5926,2.5036,679.1164,1400.209,53,139.4039
2026-10-26T23:24:33,20261027_night_cs21_fam_p5_r1_s9,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,9,1,1,1,120,1,195.6586,227.5362,6.5085,4.7225,2.5076,0.3009,0.7345,0.7645,0.7724,2.7893,1.815,0.4851,1.9698,0.721,0.5804,,4.8143,,66,0,3,1070.794,0,60,60,0,3,60,0.0418,2,4.3733,8.0624,4.2604,0.3915,8.743,2.1404,395.0591,981.4512,30,160.2475
2026-10-26T23:25:19,20261027_night_cs21_fam_p5_r2_s9,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,9,1,1,1,120,1,193.6241,233.3675,6.3498,4.6694,1.5339,0.1283,0.9204,0.7642,0.7138,2.6044,1.9669,0.4479,0.9839,0.991,0.5635,,1.7673,,137,2,27,4090.291,0,75,78,0,27,18,0.0539,2,5.1557,7.9555,4.2599,0.3816,8.6595,2.0967,381.478,1059.545,31,169.3794
2026-10-26T23:25:42,20261027_night_cs21_fam_p1_r2_s9_passon,completed,ServiceSim/cs:21,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,9,1,1,1,120,1,188.9441,211.7434,6.1935,4.5339,1.4165,0.1018,0.9115,0.752,0.7422,2.5719,1.1788,0.3853,0.9488,0.9912,0.5641,,1.441,,150,2,23,4023.888,0,49,58,0,30,20,0.049,2,4.5707,7.9844,4.1942,0.322,7.6084,2.0672,319.0985,812.2148,22,160.6347
2026-10-27T07:53:31,20261027_detcheck_cs22_fam_p1_r2_s0_passon,completed,ServiceSim/cs:22,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,0,1,1,1,120,1,172.3243,138.0129,1.3853,0.1665,1.4457,0.0625,0.925,0.5794,0.6358,2.0859,1.1788,0.3853,0.9488,0.9912,0.4978,,1.6127,,116,3,20,3365.204,0,19,46,0,20,12,0.0288,2,2.6834,7.9844,0,0.322,2.7615,2.0672,19.701,154.5938,0,103.0334
2026-10-27T07:59:22,20261027_fixcheck2_cs23_fam_p1_r2_s1_passon,completed,ServiceSim/cs:23,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,1,1,1,1,120,1,184.2616,129.5077,2.3043,1.0553,2.6809,0.165,0.6796,0.7175,0.6443,2.6922,1.1788,0.3853,0.9488,0.9912,0.2615,,1.5177,,107,8,12,2112.331,0,21,40,0,12,66,0.023,2,5.2376,7.9844,0.7046,0.322,5.2159,2.0672,169.267,560.8008,0,165.5681
2026-10-27T08:03:51,20261027_fixcheck3_cs24_fam_p5_r2_s2,completed,ServiceSim/cs:24,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,2,1,1,1,120,1,209.0967,172.7759,12.2148,5.8106,1.2473,0.0512,0.9094,0.7846,0.7324,3.027,1.9669,0.4479,0.9839,0.991,0.5707,,1.8302,,164,4,38,4608.089,0,69,65,1,38,23,0.0475,2,7.8246,7.9555,5.6421,0.3816,7.4087,2.0967,311.3584,871.4824,23,160.7741
2026-10-27T08:22:59,20261027_trackcheck_cs25_fam_p1_r1_s1,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,1,1,1,1,120,1,183.1261,125.6925,2.9037,1.6813,2.4823,0.2718,0.665,0.7565,0.7162,2.6231,2.5992,0.572,1.9487,0.7204,0.5767,,2.7302,,64,0,8,1018.244,0,44,35,0,8,69,0.0403,2,4.4431,7.9893,1.451,0.4827,8.355,2.1338,365.1943,922.8315,23,139.6559
2026-10-27T08:37:19,20261028_night_cs25_fam_p1_r0_s0,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,0,1,1,1,120,1,172.6347,196.5237,1.4618,0.2798,0.788,0,0,0.6609,0,2.8011,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,8,8,0,0,0,0,2,2.1994,12.2785,0,0.7327,3.1648,2.4872,52.2301,256.4001,0,93.2523
2026-10-27T08:37:35,20261028_night_cs25_fam_p1_r1_s0,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,0,1,1,1,120,1,175.7774,209.7774,1.6111,0.3505,2.2381,0.25,0.7563,0.609,0.7075,2.1851,2.5992,0.572,1.9487,0.7204,0.5795,,3.0045,,62,1,3,741.9966,0,35,37,0,3,39,0.0418,2,2.1235,7.9893,0,0.4827,4.1433,2.1338,98.3315,579.3281,0,91.9406
2026-10-27T08:37:37,20261028_night_cs25_fam_p5_r0_s0,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,0,1,1,1,120,1,172.3069,201.8288,1.5529,0.3602,0.9405,0,0,0.6744,0,2.8855,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,8,13,0,0,0,0,2,1.9624,11.7376,0,0.6821,4.0651,2.5036,103.2801,341.4609,0,87.2181
2026-10-27T08:37:48,20261028_night_cs25_fam_p1_r2_s0,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,0,1,1,1,120,1,173.1427,218.0931,1.5943,0.3861,1.1508,0.0375,0.9688,0.6436,0.6596,2.1377,1.1788,0.3853,0.9488,0.9912,0.5666,,2.1757,,121,1,26,3804.456,0,57,70,1,26,5,0.0733,2,2.9211,7.9844,0,0.322,3.7072,2.0672,81.4795,646.2158,1,116.7214
2026-10-27T08:40:55,20261028_night_cs25_fam_p5_r1_s0,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,0,1,1,1,120,1,176.6125,209.8494,1.555,0.3112,1.9822,0.1625,0.8313,0.6029,0.7591,2.1932,1.815,0.4851,1.9698,0.721,0.5837,,3.2491,,66,1,3,729.246,0,40,47,0,3,27,0.0416,2,2.3314,8.0624,0,0.3915,4.1182,2.1404,109.8608,565.8809,0,99.3415
2026-10-27T08:41:18,20261028_night_cs25_fam_p5_r2_s0,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,0,1,1,1,120,1,173.1668,217.8789,1.4458,0.2316,0.9091,0.025,1,0.5669,0.6372,2.079,1.9669,0.4479,0.9839,0.991,0.5686,,1.1909,,133,1,16,2997.195,0,22,50,0,16,0,0.0341,2,2.4919,7.9555,0,0.3816,2.7973,2.0967,21.7062,439.377,0,97.9607
2026-10-27T08:41:20,20261028_night_cs25_fam_p1_r2_s0_passon,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,0,1,1,1,120,1,172.1608,215.0386,1.3969,0.1574,1.2915,0.075,0.95,0.5926,0.6291,2.1107,1.1788,0.3853,0.9488,0.9912,0.4367,,1.6177,,118,3,9,2282.677,0,27,47,1,9,8,0.0522,2,2.8085,7.9844,0,0.322,2.8067,2.0672,27.6685,393.8555,0,103.5159
2026-10-27T08:41:25,20261028_night_cs25_fam_p1_r0_s1,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,1,1,1,1,120,1,186.0106,214.3918,4.0354,2.8258,0.954,0.0049,0,0.7724,0,3.2508,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,5,10,0,0,0,0,2,4.746,12.2785,2.3349,0.7327,11.1369,2.4872,534.4997,1199.197,39,143.0209
2026-10-27T08:44:49,20261028_night_cs25_fam_p1_r1_s1,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,1,1,1,1,120,1,182.2537,228.8926,2.024,0.778,2.428,0.2913,0.6893,0.733,0.704,2.6384,2.5992,0.572,1.9487,0.7204,0.5782,,2.6927,,66,0,4,735.647,0,30,31,0,4,64,0.035,2,5.9513,7.9893,0.3184,0.4827,5.1263,2.1338,176.2984,647.3115,2,167.7364
2026-10-27T08:45:11,20261028_night_cs25_fam_p5_r0_s1,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,1,1,1,1,120,1,186.5717,225.4272,4.3021,3.1005,0.9856,0.0049,0,0.7783,0,3.2769,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,8,10,0,0,0,0,2,4.5618,11.7376,2.6304,0.6821,11.7251,2.5036,567.0696,1235.455,44,142.4459
2026-10-27T08:45:22,20261028_night_cs25_fam_p1_r2_s1,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,1,1,1,1,120,1,184.6086,241.4546,3.1991,1.9157,1.7066,0.1893,0.8738,0.7581,0.6496,2.5137,1.1788,0.3853,0.9488,0.9912,0.5662,,1.7369,,129,3,31,3933.648,0,70,56,0,31,26,0.0586,2,7.1743,7.9844,1.6533,0.322,6.0355,2.0672,213.6047,661.7051,1,184.7126
2026-10-27T08:45:25,20261028_night_cs25_fam_p5_r1_s1,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,1,1,1,1,120,1,183.0597,234.1934,2.2927,1.0881,2.3396,0.267,0.6796,0.7289,0.698,2.6501,1.815,0.4851,1.9698,0.721,0.5808,,2.9415,,70,0,4,740.2466,0,41,25,1,4,66,0.0296,2,6.2604,8.0624,0.7475,0.3915,4.7632,2.1404,148.6746,739.1016,1,168.4102
2026-10-27T08:49:07,20261028_night_cs25_fam_p5_r2_s1,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,1,1,1,1,120,1,191.509,251.7204,3.8848,2.6538,1.6953,0.1456,0.9078,0.7533,0.6677,2.4716,1.9669,0.4479,0.9839,0.991,0.5648,,1.4202,,128,3,31,4176.456,0,68,45,3,31,19,0.0388,2,5.3579,7.9555,2.2567,0.3816,11.8863,2.0967,591.3564,1551.018,52,145.468
2026-10-27T08:49:08,20261028_night_cs25_fam_p1_r2_s1_passon,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,1,1,1,1,120,1,181.1987,232.6555,3.1154,1.8693,1.8028,0.165,0.8689,0.7475,0.657,2.4119,1.1788,0.3853,0.9488,0.9912,0.536,,1.7137,,120,1,25,3759.854,0,51,44,0,25,27,0.0479,2,4.8314,7.9844,1.6224,0.322,7.6377,2.0672,313.7254,738.5537,14,150.7714
2026-10-27T08:49:44,20261028_night_cs25_fam_p1_r0_s2,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,2,1,1,1,120,1,209.8073,258.2436,13.1326,8.1795,0.9193,0.0039,0,0.8432,0,4.0193,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,17,11,0,0,0,0,2,5.5623,12.2785,7.8055,0.7327,11.3421,2.4872,545.1446,1187.294,51,149.9862
2026-10-27T08:50:03,20261028_night_cs25_fam_p1_r1_s2,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,2,1,1,1,120,1,209.506,272.1619,12.7329,6.3653,2.3591,0.248,0.6693,0.7793,0.8191,3.1026,2.5992,0.572,1.9487,0.7204,0.5789,,3.8495,,77,1,9,1406.589,0,62,64,0,9,84,0.0327,2,5.6328,7.9893,6.1476,0.4827,8.302,2.1338,357.1835,880.0991,30,144.5356
2026-10-27T08:53:33,20261028_night_cs25_fam_p5_r0_s2,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,2,1,1,1,120,1,211.5588,257.5569,13.248,8.1048,0.9497,0,0,0.8412,0,4.0575,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,19,20,0,0,0,0,2,5.8639,11.7376,7.7031,0.6821,11.2652,2.5036,538.9647,1169.166,49,151.0317
2026-10-27T08:54:05,20261028_night_cs25_fam_p1_r2_s2,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,2,1,1,1,120,1,212.9765,293.8643,11.1928,3.1694,1.6358,0.1299,0.8543,0.7901,0.7172,3.157,1.1788,0.3853,0.9488,0.9912,0.5582,,1.2393,,154,4,36,5375.365,0,82,70,1,36,37,0.05,2,11.3295,7.9844,2.9552,0.322,4.323,2.0672,111.5675,503.5254,0,176.8281
2026-10-27T08:54:27,20261028_night_cs25_fam_p5_r1_s2,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,2,1,1,1,120,1,211.5289,280.8163,11.7659,5.4407,2.3331,0.2323,0.7087,0.778,0.7984,3.161,1.815,0.4851,1.9698,0.721,0.5806,,3.2346,,81,0,3,956.4461,0,58,41,1,3,74,0.0374,2,6.2251,8.0624,5.276,0.3915,8.5871,2.1404,415.2756,1305.033,38,149.1486
2026-10-27T08:54:55,20261028_night_cs25_fam_p5_r2_s2,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,2,1,1,1,120,1,208.6626,288.6986,11.6727,5.0781,1.4096,0.0591,0.9094,0.786,0.7367,3.0333,1.9669,0.4479,0.9839,0.991,0.5655,,1.3256,,159,4,38,4874.758,0,67,72,0,38,23,0.047,2,8.3021,7.9555,4.7994,0.3816,6.4008,2.0967,238.0139,743.4346,11,166.2569
2026-10-27T08:57:30,20261028_night_cs25_fam_p1_r0_s3,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,3,1,1,1,120,1,178.017,203.9522,1.572,0.3722,0.8974,0,0,0.7717,0,3.1851,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,12,11,0,0,0,0,2,4.5936,12.2785,0.056,0.7327,6.9787,2.4872,283.4813,1007.997,17,151.0264
2026-10-27T08:58:03,20261028_night_cs25_fam_p1_r1_s3,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,3,1,1,1,120,1,178.8578,212.2847,1.516,0.2714,2.124,0.1917,0.7306,0.698,0.745,2.6739,2.5992,0.572,1.9487,0.7204,0.58,,2.932,,67,0,2,691.8478,0,27,48,0,2,52,0.0289,2,6.7437,7.9893,0,0.4827,2.6814,2.1338,21.3307,197.7744,0,170.729
2026-10-27T08:58:25,20261028_night_cs25_fam_p1_r2_s2_passon,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,2,1,1,1,120,1,214.7654,288.7967,13.3439,6.9359,1.251,0.0551,0.9646,0.7674,0.713,2.9693,1.1788,0.3853,0.9488,0.9912,0.5514,,1.379,,172,2,19,4268.546,0,85,81,3,19,9,0.0677,2,8.3822,7.9844,6.732,0.322,8.4806,2.0672,383.0889,1209.706,42,165.9583
2026-10-27T08:58:49,20261028_night_cs25_fam_p1_r2_s3,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,3,1,1,1,120,1,183.2639,232.7858,2.7456,1.5258,1.4198,0.0777,0.9275,0.7317,0.673,2.3538,1.1788,0.3853,0.9488,0.9912,0.5681,,1.9964,,128,3,29,3942.057,0,70,88,1,29,14,0.0745,2,4.9785,7.9844,1.069,0.322,9.4542,2.0672,405.8437,1264.591,34,134.0345
2026-10-27T09:01:02,20261028_night_cs25_fam_p5_r0_s3,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,3,1,1,1,120,1,178.7383,205.8384,1.6427,0.4492,0.9646,0,0,0.7741,0,3.2095,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,7,14,0,0,0,0,2,4.1759,11.7376,0.0798,0.6821,7.6105,2.5036,319.6687,1027.471,20,136.1674
2026-10-27T09:01:56,20261028_night_cs25_fam_p5_r1_s3,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,3,1,1,1,120,1,177.5662,229.5509,1.661,0.448,2.4814,0.2694,0.6788,0.7391,0.742,2.6195,1.815,0.4851,1.9698,0.721,0.583,,2.4963,,62,1,10,972.4432,0,47,48,0,10,62,0.0272,2,4.8878,8.0624,0,0.3915,3.7012,2.1404,78.7103,638.9233,1,149.2729
2026-10-27T09:02:14,20261028_night_cs25_fam_p5_r2_s3,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,3,1,1,1,120,1,179.575,228.0192,2.3705,1.1268,2.1403,0.2073,0.8912,0.7514,0.6935,2.4486,1.9669,0.4479,0.9839,0.991,0.5715,,1.3548,,121,6,40,4001.994,0,60,53,1,40,21,0.0372,2,5.013,7.9555,0.2979,0.3816,7.9905,2.0967,313.1499,1333.471,13,140.0884
2026-10-27T09:02:43,20261028_night_cs25_fam_p1_r2_s3_passon,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,3,1,1,1,120,1,186.6166,231.67,1.8023,0.552,1.4891,0.0933,0.9585,0.7257,0.6821,2.4006,1.1788,0.3853,0.9488,0.9912,0.4906,,2.3597,,129,3,16,3325.223,0,66,51,1,16,8,0.0814,2,6.0994,7.9844,0.0005,0.322,5.587,2.0672,173.9348,791.5957,2,162.6187
2026-10-27T09:05:13,20261028_night_cs25_fam_p1_r0_s4,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,4,1,1,1,120,1,206.1566,246.7045,9.2638,6.7946,0.94,0,0,0.7934,0,3.7152,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,9,12,0,0,0,0,2,6.581,12.2785,6.3998,0.7327,11.3843,2.4872,548.161,1161.595,47,149.3632
2026-10-27T09:06:30,20261028_night_cs25_fam_p1_r1_s4,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,4,1,1,1,120,1,211.406,268.5033,10.7797,7.1948,2.7004,0.3544,0.6793,0.754,0.7642,3.101,2.5992,0.572,1.9487,0.7204,0.5788,,2.7607,,71,0,11,1254.092,0,65,55,0,11,76,0.0425,2,6.9835,7.9893,6.9793,0.4827,10.4622,2.1338,479.8122,1249.151,43,142.3601
2026-10-27T09:06:45,20261028_night_cs25_fam_p1_r2_s4,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,4,1,1,1,120,1,199.8995,268.3056,7.6165,4.7891,1.4265,0.1013,0.9198,0.7841,0.7128,2.6649,1.1788,0.3853,0.9488,0.9912,0.5613,,1.3401,,151,4,22,4255.943,0,63,79,2,22,19,0.0532,2,7.1455,7.9844,4.5403,0.322,8.0613,2.0672,336.1945,886.2744,25,155.4538
2026-10-27T09:06:55,20261028_night_cs25_fam_p5_r0_s4,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,4,1,1,1,120,1,205.5789,248.4453,9.6724,7.1165,1.057,0,0,0.7982,0,3.7206,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,16,12,0,0,0,0,2,6.4174,11.7376,6.6298,0.6821,11.5007,2.5036,552.5996,1193.122,45,150.3953
2026-10-27T09:09:33,20261028_night_cs25_fam_p5_r1_s4,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,4,1,1,1,120,1,204.191,256.0919,8.7688,5.1181,2.474,0.3122,0.7004,0.7544,0.7575,3.0116,1.815,0.4851,1.9698,0.721,0.5825,,3.1878,,76,0,2,709.847,0,48,46,0,2,71,0.038,2,7.3477,8.0624,4.7712,0.3915,7.364,2.1404,295.1877,957.3955,25,153.6491
2026-10-27T09:10:22,20261028_night_cs25_fam_p1_r0_s5,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,5,1,1,1,120,1,183.0779,205.9042,1.6234,0.4266,0.7993,0,0,0.7252,0,3.2194,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,14,13,0,0,0,0,2,3.6021,12.2785,0,0.7327,4.314,2.4872,121.8523,548.7979,0,150.6477
2026-10-27T09:11:03,20261028_night_cs25_fam_p5_r2_s4,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,4,1,1,1,120,1,200.1327,271.7069,8.3157,5.1878,1.5344,0.1983,0.8987,0.7737,0.6952,2.7275,1.9669,0.4479,0.9839,0.991,0.5719,,1.4599,,156,2,24,3493.746,0,61,56,1,24,24,0.0519,2,6.8094,7.9555,4.9247,0.3816,8.673,2.0967,383.427,982.2983,19,162.1606
2026-10-27T09:11:18,20261028_night_cs25_fam_p1_r2_s4_passon,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,4,1,1,1,120,1,203.7452,270.9117,8.3112,5.0139,1.823,0.1899,0.8945,0.7575,0.6986,2.7995,1.1788,0.3853,0.9488,0.9912,0.4357,,1.6346,,147,4,16,3149.056,0,63,64,0,16,25,0.0502,2,7.4091,7.9844,4.7432,0.322,7.8118,2.0672,319.5347,900.2202,18,162.9396
2026-10-27T09:13:18,20261028_night_cs25_fam_p1_r1_s5,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,5,1,1,1,120,1,183.83,221.3334,1.4401,0.2044,2.0872,0.1905,0.746,0.6583,0.7241,2.5385,2.5992,0.572,1.9487,0.7204,0.5792,,3.8896,,75,0,2,905.7955,0,36,38,0,2,48,0.0398,2,3.5329,7.9893,0,0.4827,3.2591,2.1338,58.2046,585.8525,0,146.6605
2026-10-27T09:14:18,20261028_night_cs25_fam_p1_r2_s5,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,5,1,1,1,120,1,182.8654,230.7416,1.524,0.2862,1.3584,0.1429,0.9259,0.6873,0.681,2.4533,1.1788,0.3853,0.9488,0.9912,0.56,,1.4037,,133,1,31,4433.18,0,53,77,0,31,14,0.051,2,3.913,7.9844,0,0.322,3.2251,2.0672,44.3372,356.6514,0,155.0207
2026-10-27T09:14:31,20261028_night_cs25_fam_p5_r0_s5,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,5,1,1,1,120,1,182.1018,203.9053,1.617,0.4336,0.8521,0,0,0.7301,0,3.2296,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,14,15,0,0,0,0,2,3.3542,11.7376,0,0.6821,4.3723,2.5036,124.0441,591.8789,0,144.7686
2026-10-27T09:15:00,20261028_night_cs25_fam_p5_r1_s5,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,5,1,1,1,120,1,182.8804,218.3045,1.6071,0.3371,2.2714,0.3122,0.672,0.703,0.7324,2.6743,1.815,0.4851,1.9698,0.721,0.5802,,3.5747,,69,0,7,1106.693,0,40,54,0,7,62,0.048,2,3.1994,8.0624,0,0.3915,4.1355,2.1404,101.82,443.1621,0,142.3139
2026-10-27T09:17:10,20261028_night_cs25_fam_p5_r2_s5,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,5,1,1,1,120,1,184.2791,228.0566,1.4877,0.2752,1.4483,0.1323,0.8624,0.6648,0.6656,2.5291,1.9669,0.4479,0.9839,0.991,0.5626,,1.319,,123,3,29,4452.075,0,43,47,0,29,26,0.0329,2,3.89,7.9555,0,0.3816,3.2809,2.0967,50.1424,391.1162,0,145.8426
2026-10-27T09:18:09,20261028_night_cs25_fam_p1_r2_s5_passon,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,5,1,1,1,120,1,186.8772,226.6235,1.5362,0.2981,1.713,0.0847,0.836,0.662,0.6694,2.468,1.1788,0.3853,0.9488,0.9912,0.3573,,2.0437,,132,6,11,2753.319,0,40,51,1,11,31,0.0312,2,3.7388,7.9844,0,0.322,3.3278,2.0672,66.0408,499.2422,0,133.5433
2026-10-27T09:18:44,20261028_night_cs25_fam_p1_r0_s6,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,6,1,1,1,120,1,220.5626,252.2012,15.1545,8.3333,1.058,0,0,0.7818,0,3.9158,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,9,12,0,0,0,0,2,3.3705,12.2785,7.8488,0.7327,12.0339,2.4872,587.3316,1119.858,54,125.7656
2026-10-27T09:19:26,20261028_night_cs25_fam_p1_r1_s6,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,6,1,1,1,120,1,216.7841,264.0376,10.2568,2.6593,2.2873,0.2481,0.7287,0.7199,0.7648,3.0534,2.5992,0.572,1.9487,0.7204,0.5824,,2.9094,,87,0,0,605.1487,0,35,48,0,0,70,0.0367,2,7.235,7.9893,2.3757,0.4827,4.0487,2.1338,105.252,481.9102,0,155.5107
2026-10-27T09:22:00,20261028_night_cs25_fam_p1_r2_s6,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,6,1,1,1,120,1,223.8871,287.8711,13.2734,5.1285,1.9133,0.2326,0.8605,0.7504,0.6746,2.8686,1.1788,0.3853,0.9488,0.9912,0.5663,,1.6198,,159,4,27,4542.748,0,74,55,1,27,36,0.0467,2,5.491,7.9844,4.7887,0.322,8.1543,2.0672,335.194,890.9063,13,156.7546
2026-10-27T09:22:37,20261028_night_cs25_fam_p5_r0_s6,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,6,1,1,1,120,1,224.1153,265.0516,15.7662,8.743,1.0454,0,0,0.7847,0,4.0496,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,16,17,0,0,0,0,2,3.6979,11.7376,8.2977,0.6821,13.1184,2.5036,650.5156,1269.92,56,128.9422
2026-10-27T09:23:22,20261028_night_cs25_fam_p5_r1_s6,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,6,1,1,1,120,1,217.7412,274.9905,10.2662,2.4607,2.4571,0.3256,0.6705,0.7184,0.7477,3.0909,1.815,0.4851,1.9698,0.721,0.5805,,3.0876,,77,0,2,858.0468,0,46,56,0,2,85,0.0309,2,7.6275,8.0624,2.1975,0.3915,3.626,2.1404,80.6692,492.7676,0,158.3461
2026-10-27T09:24:35,20261028_night_cs25_fam_p5_r2_s6,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,6,1,1,1,120,1,230.6945,302.725,16.8699,9.2989,1.4045,0.1124,0.938,0.7409,0.6949,2.9566,1.9669,0.4479,0.9839,0.991,0.5721,,1.4781,,174,2,40,4536.388,0,108,101,2,40,16,0.0459,2,5.2339,7.9555,9.0367,0.3816,12.6897,2.0967,619.9012,1649.482,57,138.1627
2026-10-27T09:26:37,20261028_night_cs25_fam_p1_r0_s7,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,7,1,1,1,120,1,194.6069,234.7591,3.835,1.9148,0.928,0,0,0.7447,0,3.4472,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,19,9,0,0,0,0,2,7.9805,12.2785,1.5347,0.7327,5.9742,2.4872,222.4965,678.5391,1,146.4675
2026-10-27T09:27:06,20261028_night_cs25_fam_p1_r2_s6_passon,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,6,1,1,1,120,1,226.2661,303.687,12.576,5.1436,1.8573,0.1512,0.8643,0.7329,0.6799,2.8783,1.1788,0.3853,0.9488,0.9912,0.4826,,1.5631,,155,5,25,3952.19,0,81,69,0,25,35,0.0688,2,5.0326,7.9844,4.8236,0.322,10.6516,2.0672,493.1157,1291.635,41,130.9515
2026-10-27T09:27:44,20261028_night_cs25_fam_p1_r1_s7,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,7,1,1,1,120,1,199.8904,256.0596,3.848,1.4009,2.5467,0.3206,0.6746,0.6937,0.7218,2.8067,2.5992,0.572,1.9487,0.7204,0.5776,,4.0846,,66,0,6,1279.342,0,55,55,0,6,68,0.0379,2,9.3569,7.9893,1.0344,0.4827,3.9041,2.1338,96.219,788.0596,1,155.5482
2026-10-27T09:28:56,20261028_night_cs25_fam_p1_r2_s7,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,7,1,1,1,120,1,196.9992,258.4565,2.755,0.7599,1.368,0.0718,0.9617,0.6497,0.6336,2.6327,1.1788,0.3853,0.9488,0.9912,0.565,,1.6757,,131,1,19,3674.652,0,49,67,0,19,8,0.051,2,9.9193,7.9844,0.5803,0.322,2.9178,2.0672,30.7093,354.2141,0,151.0927
2026-10-27T09:30:25,20261028_night_cs25_fam_p5_r0_s7,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,7,1,1,1,120,1,195.4519,221.9857,4.2093,2.2544,1.0644,0.0144,0,0.7475,0,3.5046,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,10,13,0,0,0,0,2,7.5411,11.7376,1.6961,0.6821,6.1689,2.5036,232.5011,677.2441,1,146.4905
2026-10-27T09:31:09,20261028_night_cs25_fam_p5_r1_s7,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,7,1,1,1,120,1,199.2919,241.0587,4.3462,1.767,2.6872,0.3445,0.6651,0.7082,0.7241,2.9119,1.815,0.4851,1.9698,0.721,0.579,,3.4875,,61,0,12,1404.14,0,61,65,0,12,70,0.0319,2,9.6073,8.0624,1.1543,0.3915,3.8932,2.1404,94.1723,535.7988,0,157.5121
2026-10-27T09:32:08,20261028_night_cs25_fam_p5_r2_s7,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,7,1,1,1,120,1,199.1275,260.424,3.2631,1.131,2.1206,0.2584,0.8995,0.7455,0.677,2.774,1.9669,0.4479,0.9839,0.991,0.5709,,2.0193,,111,3,34,4361.808,0,87,70,2,34,21,0.0571,2,9.725,7.9555,0.8206,0.3816,5.5561,2.0967,212.5244,1147.55,10,153.2666
2026-10-27T09:33:00,20261028_night_cs25_fam_p1_r2_s7_passon,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,7,1,1,1,120,1,194.7281,242.397,3.0925,1.2147,1.5648,0.1148,0.9426,0.7036,0.6847,2.5884,1.1788,0.3853,0.9488,0.9912,0.5492,,1.6266,,125,1,29,4200.803,0,70,71,0,29,12,0.053,2,7.724,7.9844,1.0759,0.322,4.8793,2.0672,139.1214,656.9243,2,140.9731
2026-10-27T09:34:03,20261028_night_cs25_fam_p1_r0_s8,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,8,1,1,1,120,1,187.2939,215.2723,4.3363,3.1307,0.8546,0,0,0.7541,0,3.2524,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,12,11,0,0,0,0,2,3.3753,12.2785,2.8573,0.7327,11.5839,2.4872,559.3043,1220.663,42,118.8923
2026-10-27T09:34:52,20261028_night_cs25_fam_p1_r1_s8,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,8,1,1,1,120,1,179.9635,219.0967,2.2222,1.0063,2.4736,0.3214,0.7602,0.7279,0.7622,2.4919,2.5992,0.572,1.9487,0.7204,0.5789,,2.9238,,64,0,5,858.6962,0,47,56,0,5,47,0.0351,2,5.8633,7.9893,0.6768,0.4827,4.7975,2.1338,146.7886,558.5098,0,165.0625
2026-10-27T09:36:09,20261028_night_cs25_fam_p1_r2_s8,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,8,1,1,1,120,1,184.6152,236.3701,2.6665,1.4254,1.47,0.0816,0.9337,0.7149,0.6695,2.3004,1.1788,0.3853,0.9488,0.9912,0.5616,,1.3235,,133,3,23,3775.333,0,65,81,1,23,13,0.0453,2,6.9639,7.9844,1.1517,0.322,5.9514,2.0672,214.0995,1021.096,13,173.3417
2026-10-27T09:36:46,20261028_night_cs25_fam_p5_r0_s8,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,8,1,1,1,120,1,187.8584,223.2976,4.6765,3.4663,0.9468,0,0,0.7567,0,3.2866,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,17,23,0,0,0,0,2,3.2142,11.7376,3.1055,0.6821,12.3161,2.5036,602.8151,1269.82,44,110.5175
2026-10-27T09:38:00,20261028_night_cs25_fam_p5_r1_s8,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,8,1,1,1,120,1,185.6744,231.8844,2.8451,1.6124,2.5951,0.2653,0.6837,0.7401,0.7563,2.5529,1.815,0.4851,1.9698,0.721,0.5809,,3.4622,,58,1,11,1242.192,0,65,54,0,11,62,0.037,2,5.5195,8.0624,1.2949,0.3915,9.9257,2.1404,481.8567,1288.996,37,155.6196
2026-10-27T09:38:59,20261028_night_cs25_fam_p5_r2_s8,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,8,1,1,1,120,1,182.6172,241.6702,1.7497,0.5236,1.2232,0.0459,0.9694,0.6664,0.6322,2.276,1.9669,0.4479,0.9839,0.991,0.5711,,1.6326,,135,1,20,2877.623,0,47,68,0,20,6,0.0306,2,7.7341,7.9555,0.1684,0.3816,3.4249,2.0967,65.6362,556.6753,0,171.1435
2026-10-27T09:39:59,20261028_night_cs25_fam_p1_r2_s8_passon,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,8,1,1,1,120,1,186.3484,226.3647,3.0206,1.7601,3.8233,0.2857,0.6582,0.7062,0.672,2.5672,1.1788,0.3853,0.9488,0.9912,0.2038,,1.4509,,87,12,5,970.548,0,35,42,0,5,67,0.0292,2,4.2435,7.9844,1.4815,0.322,6.2973,2.0672,243.8826,903.4072,3,150.0369
2026-10-27T09:40:34,20261028_night_cs25_fam_p1_r0_s9,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,9,1,1,1,120,1,199.2246,226.3644,8.1356,6.7169,1.0187,0,0,0.7857,0,3.5089,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,9,12,0,0,0,0,2,3.519,12.2785,6.2135,0.7327,13.4209,2.4872,669.8577,1333.023,54,133.7554
2026-10-27T09:42:05,20261028_night_cs25_fam_p1_r1_s9,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,9,1,1,1,120,1,203.2356,241.5674,8.8248,7.1273,2.585,0.3451,0.6327,0.7789,0.757,2.9709,2.5992,0.572,1.9487,0.7204,0.5804,,5.5916,,67,1,12,1687.936,0,78,63,0,12,83,0.0693,2,3.811,7.9893,6.638,0.4827,12.6069,2.1338,616.715,1499.968,48,145.5561
2026-10-27T09:43:12,20261028_night_cs25_fam_p1_r2_s9,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,9,1,1,1,120,1,195.4411,248.9114,6.6491,5.0766,1.3103,0.0664,0.9735,0.7466,0.724,2.4412,1.1788,0.3853,0.9488,0.9912,0.5666,,2.1025,,154,1,25,4265.994,0,60,61,2,25,6,0.0575,2,4.7628,7.9844,4.6683,0.322,10.5423,2.0672,502.3737,1406.933,43,163.232
2026-10-27T09:43:52,20261028_night_cs25_fam_p5_r0_s9,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,9,1,1,1,120,1,199.6837,228.5111,8.4323,7.1271,0.9266,0.0044,0,0.788,0,3.5551,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,15,11,0,0,0,0,2,3.1949,11.7376,6.6881,0.6821,14.1127,2.5036,710.1993,1397.022,54,125.6039
2026-10-27T09:44:27,20261028_night_cs25_fam_p5_r1_s9,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,9,1,1,1,120,1,194.6916,229.0842,5.7269,3.4648,2.4745,0.3363,0.7212,0.7399,0.74,2.8112,1.815,0.4851,1.9698,0.721,0.5822,,3.0442,,69,0,1,589.7477,0,37,50,0,1,63,0.0335,2,6.6432,8.0624,3.1257,0.3915,6.1954,2.1404,241.3935,801.0591,3,181.9825
2026-10-27T09:45:45,20261028_night_cs25_fam_p5_r2_s9,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,9,1,1,1,120,1,190.6383,216.9909,5.327,3.7802,1.2966,0.0664,0.9646,0.7364,0.7176,2.5457,1.9669,0.4479,0.9839,0.991,0.5711,,1.4363,,149,2,27,3655.097,0,59,64,0,27,8,0.0454,2,5.3068,7.9555,3.4634,0.3816,6.5703,2.0967,250.9523,619.0723,4,173.7408
2026-10-27T09:46:25,20261028_night_cs25_fam_p1_r2_s9_passon,completed,ServiceSim/cs:25,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,9,1,1,1,120,1,209.2935,186.6458,6.1969,4.3792,3.1551,0.2478,0.792,0.6881,0.7437,2.7865,1.1788,0.3853,0.9488,0.9912,0.2396,,1.9532,,117,14,9,1694.792,0,35,45,0,9,47,0.0354,2,5.3324,7.9844,3.8912,0.322,6.533,2.0672,248.4787,912.5713,14,161.7044
2026-10-27T10:22:28,20261029_night_cs26_fam_p1_r0_s0_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,0,1,1,1,120,1,173.7711,197.0167,1.5032,0.3185,0.8155,0,0,0.6599,0,2.8324,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,8,12,0,0,0,0,2,2.4026,12.2785,0,0.7327,3.7436,2.4872,85.234,304.7473,0,102.3379
2026-10-27T10:22:47,20261029_night_cs26_fam_p5_r0_s0_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,0,1,1,1,120,1,173.7512,201.873,1.6535,0.468,0.9586,0,0,0.6705,0,2.8759,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,9,18,0,0,0,0,2,1.983,11.7376,0,0.6821,4.3836,2.5036,122.5439,352.0195,0,90.3212
2026-10-27T10:22:47,20261029_night_cs26_fam_p1_r1_s0_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,0,1,1,1,120,1,176.0248,212.3025,1.8206,0.5824,2.641,0.275,0.7625,0.6234,0.7334,2.1796,2.5992,0.572,1.9487,0.7204,0.5812,,3.5658,,61,1,10,1185.292,0,46,39,0,10,38,0.0271,2,2.988,7.9893,0.0424,0.4827,6.9603,2.1338,270.6426,1174.533,8,117.211
2026-10-27T10:22:58,20261029_night_cs26_fam_p1_r2_s0_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,0,1,1,1,120,1,172.5459,218.0163,1.4491,0.2176,1.1154,0.05,0.975,0.6127,0.6529,2.1388,1.1788,0.3853,0.9488,0.9912,0.5653,,1.341,,124,1,24,3443.347,0,42,61,0,24,4,0.0497,2,2.5507,7.9844,0,0.322,3.3622,2.0672,51.5419,441.7178,0,102.6294
2026-10-27T10:25:58,20261029_night_cs26_fam_p5_r1_s0_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,0,1,1,1,120,1,174.4061,208.0055,1.5339,0.3066,1.7593,0.1063,0.825,0.5839,0.7155,2.2331,1.815,0.4851,1.9698,0.721,0.5835,,2.2961,,69,0,1,443.4988,0,22,40,0,1,28,0.032,2,2.7093,8.0624,0,0.3915,3.0825,2.1404,36.9381,283.2891,0,107.7884
2026-10-27T10:26:29,20261029_night_cs26_fam_p1_r0_s1_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,1,1,1,1,120,1,184.3289,213.9112,3.6684,2.4656,0.9394,0,0,0.7739,0,3.1789,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,10,10,0,0,0,0,2,4.8246,12.2785,2.1266,0.7327,10.7114,2.4872,509.8187,1163.238,36,145.1644
2026-10-27T10:26:45,20261029_night_cs26_fam_p1_r1_s1_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,1,1,1,1,120,1,184.7746,224.6161,1.6617,0.4335,2.228,0.2136,0.7136,0.6983,0.6835,2.6443,2.5992,0.572,1.9487,0.7204,0.5781,,2.555,,70,0,0,513.5494,0,30,24,0,0,59,0.0319,2,7.8807,7.9893,0.0306,0.4827,2.9741,2.1338,42.1154,263.0684,0,174.1595
2026-10-27T10:26:51,20261029_night_cs26_fam_p5_r2_s0_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,0,1,1,1,120,1,193.2323,240.7371,1.4288,0.2233,1.9015,0.1125,0.8625,0.5279,0.6201,2.2363,1.9669,0.4479,0.9839,0.991,0.3132,,1.7675,,116,7,6,1712.44,0,30,37,0,6,22,0.0307,2,3.4611,7.9555,0,0.3816,2.7731,2.0967,24.5765,301.1689,0,107.4982
2026-10-27T10:29:46,20261029_night_cs26_fam_p1_r2_s1_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,1,1,1,1,120,1,182.1673,225.466,2.8108,1.5614,2.8661,0.2087,0.7282,0.7548,0.6838,2.5525,1.1788,0.3853,0.9488,0.9912,0.2493,,1.9262,,100,10,8,1641.889,0,41,43,2,8,56,0.037,2,5.1494,7.9844,1.28,0.322,6.6359,2.0672,254.1494,626.8916,1,150.5212
2026-10-27T10:30:01,20261029_night_cs26_fam_p5_r0_s1_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,1,1,1,1,120,1,187.2225,210.9261,4.2881,3.0861,0.9702,0,0,0.7758,0,3.2966,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,8,15,0,0,0,0,2,4.6885,11.7376,2.6366,0.6821,11.7351,2.5036,566.5652,1257.817,42,144.3941
2026-10-27T10:30:28,20261029_night_cs26_fam_p5_r1_s1_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,1,1,1,1,120,1,181.9416,218.1018,1.8762,0.6623,2.31,0.2621,0.6699,0.7363,0.7012,2.6185,1.815,0.4851,1.9698,0.721,0.5809,,3.671,,66,0,3,782.1962,0,43,29,0,3,68,0.0402,2,5.8718,8.0624,0.2383,0.3915,4.9465,2.1404,160.3104,618.0762,1,165.8285
2026-10-27T10:30:53,20261029_night_cs26_fam_p5_r2_s1_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,1,1,1,1,120,1,186.3949,237.494,4.5564,3.301,1.8667,0.1456,0.8641,0.7358,0.6595,2.4223,1.9669,0.4479,0.9839,0.991,0.4541,,1.4345,,124,3,22,2967.858,0,51,35,1,22,28,0.0499,2,6.2067,7.9555,2.643,0.3816,9.0682,2.0967,404.5897,1163.886,25,159.8649
2026-10-27T10:34:03,20261029_night_cs26_fam_p1_r0_s2_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,2,1,1,1,120,1,211.8219,252.0683,13.3498,8.338,1.0758,0.0079,0,0.8383,0,4.0442,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,18,11,0,0,0,0,2,5.5298,12.2785,7.8281,0.7327,11.2269,2.4872,537.8159,1217.525,46,148.1956
2026-10-27T10:34:29,20261029_night_cs26_fam_p1_r1_s2_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,2,1,1,1,120,1,207.9205,263.451,10.8427,4.3884,2.521,0.3386,0.689,0.7947,0.7922,3.1857,2.5992,0.572,1.9487,0.7204,0.5801,,3.4407,,76,0,4,997.1453,0,69,58,0,4,79,0.042,2,6.7138,7.9893,4.0999,0.4827,6.83,2.1338,269.5074,842.2471,10,160.9508
2026-10-27T10:35:07,20261029_night_cs26_fam_p5_r0_s2_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,2,1,1,1,120,1,212.6528,250.9482,13.4294,8.3478,0.9799,0.0118,0,0.8382,0,4.0678,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,28,14,0,0,0,0,2,5.7546,11.7376,7.9718,0.6821,11.412,2.5036,548.3873,1173.349,49,149.361
2026-10-27T10:35:12,20261029_night_cs26_fam_p1_r2_s2_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,2,1,1,1,120,1,212.8055,281.4077,12.6131,6.5604,1.7668,0.185,0.9055,0.7876,0.7348,2.9389,1.1788,0.3853,0.9488,0.9912,0.5176,,1.802,,158,2,32,4696.684,0,71,86,3,32,24,0.0553,2,6.7441,7.9844,6.2348,0.322,8.819,2.0672,404.9121,1044.454,43,153.2992
2026-10-27T10:38:30,20261029_night_cs26_fam_p1_r0_s3_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,3,1,1,1,120,1,180.0938,199.045,1.588,0.391,0.9375,0,0,0.7654,0,3.2411,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,11,9,0,0,0,0,2,5.8164,12.2785,0.0262,0.7327,6.1177,2.4872,231.5621,904.4531,13,165.812
2026-10-27T10:38:36,20261029_night_cs26_fam_p5_r1_s2_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,2,1,1,1,120,1,218.4642,270.6233,13.5359,6.7337,2.6261,0.3346,0.6575,0.7756,0.7707,3.2088,1.815,0.4851,1.9698,0.721,0.5813,,3.2502,,87,0,6,1081.844,0,59,54,0,6,87,0.0309,2,6.4301,8.0624,6.4935,0.3915,9.8255,2.1404,458.7709,1424.614,47,157.1422
2026-10-27T10:38:42,20261029_night_cs26_fam_p1_r1_s3_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,3,1,1,1,120,1,179.6149,206.3344,1.6469,0.4182,2.4532,0.3005,0.6477,0.7219,0.7275,2.6559,2.5992,0.572,1.9487,0.7204,0.5778,,3.8062,,61,1,5,1069.094,0,33,39,0,5,68,0.0377,2,5.5109,7.9893,0.0394,0.4827,6.628,2.1338,255.7086,788.1592,14,155.7478
2026-10-27T10:39:04,20261029_night_cs26_fam_p5_r2_s2_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,2,1,1,1,120,1,209.0038,273.1274,10.8578,3.854,2.5693,0.1654,0.7677,0.7679,0.7237,3.1489,1.9669,0.4479,0.9839,0.991,0.2623,,2.233,,130,10,3,1479.743,0,50,52,0,3,59,0.0343,2,8.1459,7.9555,3.6417,0.3816,5.0234,2.0967,156.8799,553.3799,0,169.224
2026-10-27T10:41:58,20261029_night_cs26_fam_p5_r0_s3_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,3,1,1,1,120,1,179.8581,197.1602,1.6554,0.4624,1.0101,0,0,0.7715,0,3.2297,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,9,12,0,0,0,0,2,4.7611,11.7376,0.074,0.6821,7.2171,2.5036,296.1057,1043.508,17,152.3286
2026-10-27T10:42:11,20261029_night_cs26_fam_p1_r2_s3_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,3,1,1,1,120,1,179.0255,214.6568,1.8443,0.6115,1.2917,0.0674,0.9326,0.7012,0.664,2.375,1.1788,0.3853,0.9488,0.9912,0.5403,,1.6319,,134,3,17,3191.254,0,48,75,0,17,13,0.0544,2,5.5694,7.9844,0.1291,0.322,4.8421,2.0672,139.6599,622.2598,1,158.7419
2026-10-27T10:42:41,20261029_night_cs26_fam_p5_r1_s3_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,3,1,1,1,120,1,190.7786,234.4879,3.72,2.4846,3.1543,0.4611,0.544,0.7788,0.7101,2.8,1.815,0.4851,1.9698,0.721,0.5829,,3.869,,55,2,22,1851.133,0,94,66,0,22,88,0.0619,2,5.0373,8.0624,1.7755,0.3915,12.0611,2.1404,558.416,1488.563,37,130.9835
2026-10-27T10:43:00,20261029_night_cs26_fam_p5_r2_s3_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,3,1,1,1,120,1,188.134,233.8226,1.6802,0.4305,1.2912,0.0415,0.9637,0.6792,0.6708,2.4496,1.9669,0.4479,0.9839,0.991,0.5045,,1.6431,,136,3,15,2773.883,0,42,55,0,15,7,0.0565,2,6.41,7.9555,0,0.3816,3.2347,2.0967,46.704,410.6396,0,155.4579
2026-10-27T10:45:56,20261029_night_cs26_fam_p1_r0_s4_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,4,1,1,1,120,1,204.9721,235.1405,9.4232,6.7961,0.9634,0,0,0.7959,0,3.6906,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,14,12,0,0,0,0,2,6.6864,12.2785,6.3348,0.7327,11.0656,2.4872,529.6302,1143.815,43,152.4889
2026-10-27T10:46:16,20261029_night_cs26_fam_p1_r1_s4_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,4,1,1,1,120,1,198.9407,239.7691,6.5818,3.6735,2.389,0.2996,0.7257,0.7443,0.7573,2.8312,2.5992,0.572,1.9487,0.7204,0.5794,,2.5595,,76,0,4,767.4474,0,36,48,0,4,65,0.0363,2,6.8576,7.9893,3.3254,0.4827,5.5063,2.1338,196.4933,623.3867,1,160.8538
2026-10-27T10:46:58,20261029_night_cs26_fam_p5_r0_s4_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,4,1,1,1,120,1,205.8362,236.2452,9.9733,7.3579,1.0031,0,0,0.7992,0,3.7305,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,18,13,0,0,0,0,2,6.2562,11.7376,6.9066,0.6821,11.9696,2.5036,581.0195,1211.549,45,144.6207
2026-10-27T10:47:12,20261029_night_cs26_fam_p1_r2_s4_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,4,1,1,1,120,1,211.5463,265.5677,12.2065,9.673,1.4115,0.1266,0.9409,0.7797,0.6808,2.8162,1.1788,0.3853,0.9488,0.9912,0.5346,,1.8203,,157,3,23,4133.396,0,84,70,4,23,14,0.0629,2,5.5976,7.9844,9.4595,0.322,13.3011,2.0672,627.3191,1442.792,69,137.9752
2026-10-27T10:50:21,20261029_night_cs26_fam_p5_r1_s4_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,4,1,1,1,120,1,214.7147,259.081,13.5233,10.468,2.9246,0.4219,0.6287,0.7648,0.7452,3.052,1.815,0.4851,1.9698,0.721,0.5825,,3.3043,,69,1,18,1662.436,0,76,44,1,18,88,0.0443,2,5.6206,8.0624,10.1794,0.3915,14.1555,2.1404,704.1917,1568.848,54,135.3641
2026-10-27T10:50:24,20261029_night_cs26_fam_p1_r0_s5_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,5,1,1,1,120,1,183.5992,202.4163,1.589,0.4007,0.8024,0,0,0.7216,0,3.2253,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,11,12,0,0,0,0,2,3.8796,12.2785,0,0.7327,4.2619,2.4872,118.6252,540.1318,0,152.5137
2026-10-27T10:50:44,20261029_night_cs26_fam_p5_r2_s4_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,4,1,1,1,120,1,205.8121,262.8899,9.1055,5.7859,1.669,0.1519,0.8903,0.7621,0.6854,2.8589,1.9669,0.4479,0.9839,0.991,0.5072,,1.8541,,147,3,26,3404.211,0,63,62,0,26,26,0.0581,2,7.3059,7.9555,5.5497,0.3816,8.988,2.0967,392.4956,950.0742,42,158.1305
2026-10-27T10:50:46,20261029_night_cs26_fam_p1_r1_s5_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,5,1,1,1,120,1,183.4946,208.9489,1.4376,0.1963,2.0123,0.164,0.746,0.6485,0.716,2.4968,2.5992,0.572,1.9487,0.7204,0.5784,,3.2764,,71,0,2,795.0467,0,34,42,0,2,48,0.0374,2,3.4649,7.9893,0,0.4827,2.8645,2.1338,32.6645,339.3193,0,143.9883
2026-10-27T10:53:51,20261029_night_cs26_fam_p5_r0_s5_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,5,1,1,1,120,1,182.3624,199.1213,1.5825,0.3961,0.8441,0,0,0.7327,0,3.2344,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,16,11,0,0,0,0,2,3.3531,11.7376,0,0.6821,4.4224,2.5036,125.493,542.124,0,146.4912
2026-10-27T10:54:13,20261029_night_cs26_fam_p1_r2_s5_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,5,1,1,1,120,1,184.3289,226.521,1.6067,0.3832,1.5505,0.1164,0.9048,0.6725,0.6886,2.4185,1.1788,0.3853,0.9488,0.9912,0.5533,,1.0905,,126,3,27,4996.977,0,57,53,0,27,18,0.0411,2,3.8433,7.9844,0,0.322,4.7099,2.0672,128.5881,780.9741,6,145.0843
2026-10-27T10:54:20,20261029_night_cs26_fam_p5_r1_s5_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,5,1,1,1,120,1,182.223,213.4983,1.5827,0.3523,2.276,0.291,0.709,0.6943,0.742,2.5746,1.815,0.4851,1.9698,0.721,0.5816,,3.1201,,70,0,7,963.2442,0,39,49,0,7,55,0.0458,2,2.7585,8.0624,0,0.3915,3.4408,2.1404,67.0631,418.8081,0,126.3093
2026-10-27T10:54:38,20261029_night_cs26_fam_p5_r2_s5_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,5,1,1,1,120,1,182.5185,226.0046,1.4999,0.2575,1.0246,0.0317,0.9788,0.6373,0.6764,2.3357,1.9669,0.4479,0.9839,0.991,0.5675,,1.4356,,141,1,19,3829.721,0,39,60,0,19,4,0.0339,2,3.6585,7.9555,0,0.3816,3.0221,2.0967,34.9776,278.1592,0,149.451
2026-10-27T10:58:05,20261029_night_cs26_fam_p1_r0_s6_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,6,1,1,1,120,1,220.9735,247.8662,15.2468,8.2252,1.0488,0,0,0.7825,0,3.9639,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,14,13,0,0,0,0,2,3.6043,12.2785,7.6945,0.7327,12.2401,2.4872,600.492,1178.578,56,126.9352
2026-10-27T10:58:34,20261029_night_cs26_fam_p1_r1_s6_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,6,1,1,1,120,1,216.9924,256.8341,11.4582,3.7705,2.3699,0.3023,0.7054,0.7321,0.765,3.0603,2.5992,0.572,1.9487,0.7204,0.5825,,2.4927,,90,0,4,738.7473,0,45,58,0,4,76,0.0347,2,6.1834,7.9893,3.4387,0.4827,5.6015,2.1338,200.1301,709.0693,5,149.6198
2026-10-27T10:58:53,20261029_night_cs26_fam_p5_r0_s6_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,6,1,1,1,120,1,222.3589,250.7431,15.3594,8.4463,0.969,0,0,0.7833,0,4.0014,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,16,16,0,0,0,0,2,3.8148,11.7376,7.9652,0.6821,12.4775,2.5036,611.8116,1206.369,56,133.3101
2026-10-27T10:59:04,20261029_night_cs26_fam_p1_r2_s6_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,6,1,1,1,120,1,223.3176,281.731,12.7791,5.2959,1.641,0.155,0.907,0.7361,0.6997,2.8324,1.1788,0.3853,0.9488,0.9912,0.5662,,1.9593,,164,4,43,4809.899,0,75,67,1,43,24,0.0515,2,5.4354,7.9844,5.0013,0.322,7.9026,2.0672,332.9763,809.7744,17,151.2899
2026-10-27T11:02:30,20261029_night_cs26_fam_p5_r1_s6_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,6,1,1,1,120,1,217.9645,262.8974,12.645,5.0731,2.461,0.3178,0.6628,0.7298,0.7687,3.0986,1.815,0.4851,1.9698,0.721,0.5816,,2.7394,,82,0,6,965.6952,0,47,45,0,6,87,0.036,2,4.7156,8.0624,4.8026,0.3915,6.8043,2.1404,275.3153,729.7383,6,148.3156
2026-10-27T11:02:35,20261029_night_cs26_fam_p1_r0_s7_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,7,1,1,1,120,1,194.4849,217.3776,4.1268,2.1803,1.0105,0,0,0.748,0,3.4564,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,15,13,0,0,0,0,2,7.6507,12.2785,1.7501,0.7327,6.2964,2.4872,242.1349,750.5566,3,148.5238
2026-10-27T11:02:58,20261029_night_cs26_fam_p1_r1_s7_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,7,1,1,1,120,1,198.6693,230.9713,4.0324,1.8658,2.4061,0.3062,0.7033,0.6911,0.7237,2.8292,2.5992,0.572,1.9487,0.7204,0.5789,,3.1826,,69,0,5,987.6953,0,45,52,0,5,62,0.0432,2,8.4157,7.9893,1.5229,0.4827,4.4078,2.1338,122.1653,527.4316,0,157.179
2026-10-27T11:03:19,20261029_night_cs26_fam_p5_r2_s6_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,6,1,1,1,120,1,217.9387,282.0772,10.0328,2.5135,1.2254,0.0659,0.9535,0.7033,0.6527,2.8881,1.9669,0.4479,0.9839,0.991,0.5685,,1.4037,,174,2,12,3038.161,0,48,66,0,12,12,0.0437,2,8.5189,7.9555,2.2881,0.3816,3.8093,2.0967,85.0916,503.4756,0,157.7515
2026-10-27T11:06:14,20261029_night_cs26_fam_p5_r0_s7_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,7,1,1,1,120,1,193.7453,216.465,4.4454,2.5485,1.0012,0,0,0.7539,0,3.4731,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,15,14,0,0,0,0,2,7.0517,11.7376,2.0176,0.6821,6.8351,2.5036,272.7883,733.8223,3,144.6375
2026-10-27T11:06:34,20261029_night_cs26_fam_p1_r2_s7_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,7,1,1,1,120,1,196.1152,241.07,3.8798,1.922,1.5986,0.1244,0.933,0.7175,0.6856,2.6129,1.1788,0.3853,0.9488,0.9912,0.5587,,1.4813,,125,1,26,4365.438,0,78,86,1,26,14,0.0648,2,7.7581,7.9844,1.6487,0.322,5.2082,2.0672,178.5104,916.8047,4,145.9756
2026-10-27T11:06:57,20261029_night_cs26_fam_p5_r1_s7_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,7,1,1,1,120,1,198.2011,234.7617,3.953,1.853,2.6974,0.3636,0.6507,0.7031,0.7103,2.8151,1.815,0.4851,1.9698,0.721,0.5846,,3.7613,,65,1,10,1149.641,0,61,55,0,10,73,0.0281,2,7.4397,8.0624,1.4566,0.3915,5.1681,2.1404,171.1839,797.124,8,147.9544
2026-10-27T11:07:27,20261029_night_cs26_fam_p5_r2_s7_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,7,1,1,1,120,1,195.1722,244.3149,5.0567,2.8958,1.7505,0.1914,0.8995,0.7161,0.689,2.5679,1.9669,0.4479,0.9839,0.991,0.4728,,1.3684,,119,5,28,3673.698,0,67,76,0,28,21,0.0625,2,7.0971,7.9555,2.5022,0.3816,6.802,2.0967,260.6968,770.9766,9,156.2428
2026-10-27T11:09:46,20261029_night_cs26_fam_p1_r0_s8_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,8,1,1,1,120,1,185.5151,208.248,4.2936,3.0893,0.8693,0,0,0.7605,0,3.2493,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,17,15,0,0,0,0,2,3.6153,12.2785,2.7779,0.7327,11.3707,2.4872,547.1569,1232.716,38,126.5111
2026-10-27T11:10:13,20261029_night_cs26_fam_p1_r1_s8_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,8,1,1,1,120,1,182.907,215.5228,1.9918,0.7377,2.3736,0.2245,0.7296,0.6947,0.7107,2.5229,2.5992,0.572,1.9487,0.7204,0.5796,,2.6101,,60,0,2,600.3484,0,29,52,0,2,53,0.0288,2,7.1922,7.9893,0.4802,0.4827,3.8323,2.1338,90.9902,482.1094,0,170.9261
2026-10-27T11:11:06,20261029_night_cs26_fam_p5_r0_s8_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,8,1,1,1,120,1,187.7654,213.1249,4.5221,3.3112,0.9349,0,0,0.7616,0,3.3282,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,15,17,0,0,0,0,2,3.3958,11.7376,2.9709,0.6821,12.327,2.5036,601.7565,1280.18,45,118.8287
2026-10-27T11:11:08,20261029_night_cs26_fam_p1_r2_s8_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,8,1,1,1,120,1,201.8037,244.708,4.47,3.223,3.3988,0.2041,0.7092,0.6673,0.7302,2.5881,1.1788,0.3853,0.9488,0.9912,0.1861,,2.0447,,95,15,6,1380.594,0,62,52,0,6,57,0.0375,2,3.9748,7.9844,3.0054,0.322,7.9428,2.0672,337.8692,882.8379,26,139.1964
2026-10-27T11:13:25,20261029_night_cs26_fam_p5_r1_s8_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,8,1,1,1,120,1,183.3593,217.4462,2.2213,0.9816,2.2008,0.1837,0.7449,0.6838,0.7198,2.6147,1.815,0.4851,1.9698,0.721,0.5825,,2.7419,,66,0,1,515.1984,0,37,47,0,1,50,0.0248,2,7.5706,8.0624,0.6965,0.3915,3.8165,2.1404,87.9934,443.7598,0,174.9157
2026-10-27T11:14:11,20261029_night_cs26_fam_p5_r2_s8_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,8,1,1,1,120,1,184.0848,232.7661,1.7534,0.5355,1.2885,0.0306,0.9694,0.684,0.6376,2.3195,1.9669,0.4479,0.9839,0.991,0.4963,,1.2782,,136,2,2,2381.299,0,55,76,1,2,6,0.0503,2,8.6664,7.9555,0.2116,0.3816,3.7114,2.0967,92.0354,556.7168,0,180.3828
2026-10-27T11:14:52,20261029_night_cs26_fam_p1_r0_s9_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,0,42,serve,1,any,9,1,1,1,120,1,198.4128,223.7707,7.9854,6.6941,0.9257,0,0,0.7837,0,3.4933,3.301,0.9547,0.459,0,0,,0,,0,0,0,0,0,10,19,0,0,0,0,2,2.9659,12.2785,6.3214,0.7327,13.499,2.4872,676.2562,1383.027,52,120.7007
2026-10-27T11:15:04,20261029_night_cs26_fam_p1_r1_s9_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,1,42,serve,3,any,9,1,1,1,120,1,194.0889,231.3195,6.178,4.5151,2.5169,0.3584,0.677,0.779,0.7734,2.9061,2.5992,0.572,1.9487,0.7204,0.5806,,2.9884,,67,1,12,1249.541,0,43,61,1,12,73,0.0535,2,4.452,7.9893,4.0617,0.4827,7.8971,2.1338,343.0463,832.8345,25,164.8402
2026-10-27T11:17:39,20261029_night_cs26_fam_p1_r2_s9_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p1,4,2,42,serve,3,any,9,1,1,1,120,1,197.7745,250.7039,7.7541,5.3839,1.6364,0.1372,0.9381,0.744,0.701,2.5195,1.1788,0.3853,0.9488,0.9912,0.5652,,1.7371,,145,2,26,3899.647,0,72,65,0,26,14,0.0634,2,5.6064,7.9844,4.9048,0.322,8.983,2.0672,402.6203,751.752,20,173.1176
2026-10-27T11:17:55,20261029_night_cs26_fam_p5_r0_s9_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,0,42,serve,1,any,9,1,1,1,120,1,200.3237,221.457,8.6904,7.3334,1.0109,0.0044,0,0.7902,0,3.6068,2.7562,0.8929,0.4478,0,0,,0,,0,0,0,0,0,13,13,0,0,0,0,2,3.3425,11.7376,6.8179,0.6821,14.0568,2.5036,707.0592,1425.66,53,129.5542
2026-10-27T11:18:28,20261029_night_cs26_fam_p5_r1_s9_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,1,42,serve,3,any,9,1,1,1,120,1,194.0441,214.2239,6.1345,4.3964,2.4255,0.2655,0.7389,0.7438,0.7676,2.8308,1.815,0.4851,1.9698,0.721,0.5804,,2.7041,,72,0,1,617.0983,0,38,54,0,1,59,0.0339,2,4.5491,8.0624,4.0396,0.3915,7.8784,2.1404,339.6511,832.0874,26,155.053
2026-10-27T11:18:47,20261029_night_cs26_fam_p5_r2_s9_passon,completed,ServiceSim/cs:26,8a5b2f6a016a,v2.8,fam_p5,4,2,42,serve,3,any,9,1,1,1,120,1,194.147,218.2769,5.974,4.3424,1.3793,0.0885,0.9381,0.775,0.7044,2.6196,1.9669,0.4479,0.9839,0.991,0.5679,,1.5405,,149,1,24,3805.884,0,79,82,1,24,14,0.0837,2,6.1995,7.9555,3.9708,0.3816,8.7174,2.0967,376.0291,911.4761,11,183.7688
```


## Unity のコード（ServiceSim/cs:27）

前回（cs:17）から変わったファイルの全文です。

### Assets/ServiceSim/Experiment/ServiceSimLedger.cs

```csharp
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>
    /// 段階Bの通し台帳（2026-10-23 エイイチと決めた。依頼 v7）。
    ///
    /// 実体: {kioxiaOutputRoot}/unity_ledger_v1.csv
    ///       （MASTER の保存先「service_sim/unity/」の中。新しい置き場所は作らない）
    ///
    /// 決まり:
    ///   ・1回 = 1行・**追記のみ**。既存の行は書き換えない・消さない
    ///   ・同じ出力フォルダの行を2度書かない（run_folder が照合キー）
    ///   ・**追記ガード**：実体のヘッダとこのコードの列の並びが1字でも違えば追記しない
    ///   ・止めた回・失敗した回も status を付けて1行残す
    ///   ・書くときは **鍵をかける**（依頼 v12 の 3 章）。夜は実行ファイルを
    ///     何本も同時に走らせるので、同時に追記すると行が混ざる
    ///
    /// 列の名前と並びを変えるときは、先にエイイチに聞く（依頼 v7 の 2）。
    ///
    /// なぜ Editor/ の外にあるか（2026-10-26・依頼 v12 の 3 章）
    ///   画面なしの実行ファイル（BuildPipeline で作る player）には
    ///   Editor/ の中のコードが入らない。けれど夜の回も**同じコード**で
    ///   台帳に書かないと、列の作り方が2通りに分かれてしまう。
    ///   このクラスは UnityEditor の API を1つも使っていなかったので、
    ///   Experiment/ に移して player からも呼べるようにした。
    ///   中身（列・ガード・2度書かない決まり）は移すときに変えていない。
    ///   名前空間も ServiceSim にした。呼んでいたのは ServiceSimStageB だけで、
    ///   そちらは ServiceSim.EditorTools の中（= ServiceSim の内側）にあるので、
    ///   呼ぶ側の字は1つも変わらない。
    /// </summary>
    public static class ServiceSimLedger
    {
        /// <summary>
        /// 台帳の版。列を足したら 1 つ上げる。
        ///   1 … 依頼 v7（49 列）
        ///   2 … 依頼 v10b（会計待ち・片付け待ちの 10 列を後ろに足した。59 列）
        /// </summary>
        public const int SchemaVersion = 2;
        public const string FileName = "unity_ledger_v1.csv";

        /// <summary>
        /// 鍵ファイルの名前（依頼 v12 の 3 章）。
        /// tools/ledger.py の LOCK_NAME と同じ字。
        /// O_EXCL / FileMode.CreateNew で作れたら持ち主、閉じるときに消す。
        /// 中身は「pid ホスト名 unix秒」の1行（Python 側と同じ形）
        /// </summary>
        public const string LockFileName = "unity_ledger_v1.lock";

        /// <summary>鍵を待つ上限（秒）。これを超えたら書かずにあきらめる</summary>
        public const float LockTimeoutS = 60f;

        /// <summary>これより古い鍵は、持ち主が落ちたと見て1度だけ壊す（Python 側と同じ 120 秒）</summary>
        public const float LockStaleS = 120f;

        /// <summary>
        /// unity_cs の頭に付ける字（依頼 v11 の 2）。
        ///
        /// このプロジェクト（Robotics_UX_New から分けた ServiceSim 専用）は
        /// changeset の番号が 1 から振り直されるので、
        /// 台帳の中で前のプロジェクトの "cs:144" などと混ざらないように
        /// "ServiceSim/cs:1" の形で書く。
        /// 台帳の列は増やさない（data_manifest_rules.md 10節）
        /// </summary>
        public const string CsPrefix = "ServiceSim/";

        /// <summary>
        /// 列（この順。名前を変えるときは先にエイイチに聞く）。
        ///
        /// 足すときは **いちばん後ろ**（ledger_schema_version より後ろ）に足す。
        /// 途中に入れると前の行の列の位置がずれて、古い行が読めなくなる（依頼 v10b の 1）。
        /// 前の行は新しい列が無いまま（短い行）で置いておく。書き換えない
        /// </summary>
        public static readonly string[] Columns =
        {
            "timestamp", "run_folder", "status", "unity_cs", "layout_generation", "params_model",
            "store", "staff", "robots", "arrivals_per_h", "mode", "trays", "fragile_by", "seed",
            "staff_first_yield", "yield_order_enabled", "one_robot_per_aisle", "robot_first_wait_s",
            "everyone_left", "total_minutes", "wall_seconds",
            "entry_wait_mean", "staff_wait_mean", "dish_wait_mean", "cold_share", "robot_share",
            "staff_util", "robot_util", "walk_km_per_staff",
            "site_entry_wait_mean", "site_staff_wait_mean", "site_dish_wait_mean", "site_robot_share",
            "robot_speed_mps", "pause_rate", "pause_s_mean", "stuck_rate",
            "trips", "timeouts", "stuck_count", "stop_s_total", "corner_stop_time_share",
            "gave_up", "warped", "table_freed", "rescues",
            "late_staff_dishes", "staff_yield_share_of_staff_time",
            "ledger_schema_version",
            // ---- 依頼 v10b の 1（2026-10-25）。ここから後ろは schema 2 で足した列 ----
            // 会計待ち・片付け待ちを試算と並べるための 10 列。
            // clear_* / pay_job_* は「仕事が板に載ってから取られるまで」の秒数
            "pay_wait_mean", "site_pay_wait_mean",
            "blocked_wait_mean", "site_blocked_wait_mean",
            "turn_mean", "site_turn_mean",
            "clear_wait_mean_s", "clear_wait_max_s", "clear_wait_over_count",
            "pay_job_wait_mean_s"
        };

        public static string HeaderLine { get { return string.Join(",", Columns); } }

        /// <summary>台帳の実体の場所</summary>
        public static string PathOf(ExperimentPlan plan)
        {
            string root = plan != null && !string.IsNullOrEmpty(plan.kioxiaOutputRoot)
                ? plan.kioxiaOutputRoot
                : "/Volumes/KIOXIA/r2r2r-data/service_sim/unity";
            return Path.Combine(root, FileName);
        }

        // ---------------- 追記ガード ----------------

        /// <summary>
        /// 追記していいか。
        ///   ・実体が無い … いい（header を書いて作る）
        ///   ・1行めがコードの列と同じ … いい
        ///   ・違う … だめ（why に理由）
        /// </summary>
        public static bool CanAppend(string path, out string why)
        {
            why = "";
            if (!File.Exists(path)) return true;

            string first;
            try
            {
                using (StreamReader r = new StreamReader(path, new UTF8Encoding(false)))
                    first = r.ReadLine();
            }
            catch (Exception e)
            {
                why = "台帳を読めません: " + path + "\n  " + e.Message;
                return false;
            }

            if (first == null)
            {
                why = "台帳が空です（ヘッダがありません）: " + path +
                      "\n  いちど消すか、ヘッダを書いてください";
                return false;
            }

            string have = first.TrimEnd('\r', '\n');
            if (have == HeaderLine) return true;

            why = "台帳のヘッダがコードの列と違うので追記しません（追記ガード）: " + path +
                  "\n  実体: " + have +
                  "\n  コード: " + HeaderLine +
                  "\n  列をそろえるか、エイイチに聞いてから直してください";
            return false;
        }

        /// <summary>その出力フォルダの行がもうあるか（run_folder が照合キー）</summary>
        public static bool HasRow(string path, string runFolder)
        {
            bool unreadable;
            return HasRow(path, runFolder, out unreadable);
        }

        /// <summary>
        /// 同じ出力フォルダの行がもうあるか。
        ///
        /// 2026-10-26（codex レビュー A5）：読めなかったときに false（＝行なし）を
        /// 返していたので、読めないまま2行目を書く道があった。
        /// unreadable で「分からない」を返し、呼ぶ側は **書かない** ようにする
        /// </summary>
        public static bool HasRow(string path, string runFolder, out bool unreadable)
        {
            unreadable = false;
            if (string.IsNullOrEmpty(runFolder)) return false;
            if (!File.Exists(path)) return false;

            int col = IndexOf("run_folder");
            try
            {
                using (StreamReader r = new StreamReader(path, new UTF8Encoding(false)))
                {
                    r.ReadLine();                       // ヘッダ
                    string line;
                    while ((line = r.ReadLine()) != null)
                    {
                        if (line.Length == 0) continue;
                        string[] f = line.Split(',');
                        if (col < f.Length && f[col] == runFolder) return true;
                    }
                }
            }
            catch (Exception e)
            {
                // 読めないときは「行が無い」と決めつけない（2026-10-26・A5）
                unreadable = true;
                Debug.LogWarning("[ServiceSim] 台帳を読めませんでした: " + e.Message);
            }
            return false;
        }

        /// <summary>
        /// 台帳の最後が改行で終わっているか（2026-10-26・codex レビュー A4）。
        ///
        /// 追記の途中で落ちると、最後の行が半端なまま残りうる。
        /// そこへ次の行を足すと **2つの行がつながって1行**になり、
        /// 列がずれた行が台帳に入る。
        /// なので足す前に末尾を見て、改行で終わっていなければ **書かない**。
        /// 自動で切り詰めることはしない（台帳は消さない・書き換えないのが決まり）
        /// </summary>
        public static bool EndsWithNewline(string path, out string why)
        {
            why = "";
            try
            {
                if (!File.Exists(path)) return true;
                using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {
                    if (fs.Length == 0) return true;
                    fs.Seek(-1, SeekOrigin.End);
                    int last = fs.ReadByte();
                    if (last == '\n') return true;
                    why = "台帳の最後が改行で終わっていません（前の追記が途中で切れた疑い）: " + path +
                          "\n  つなげて書くと列がずれるので、何も足しませんでした。" +
                          "\n  最後の行を見て、エイイチに相談してから直してください" +
                          "\n  （自動では直しません。台帳は書き換えない決まりです）";
                    return false;
                }
            }
            catch (Exception e)
            {
                why = "台帳の末尾を読めません: " + path + "\n  " + e.Message;
                return false;
            }
        }

        /// <summary>もう台帳にある出力フォルダの名前をぜんぶ返す（2度書きの歯止めに使う）</summary>
        public static HashSet<string> DoneFolders(string path)
        {
            HashSet<string> done = new HashSet<string>();
            if (!File.Exists(path)) return done;

            int col = IndexOf("run_folder");
            int statusCol = IndexOf("status");
            try
            {
                using (StreamReader r = new StreamReader(path, new UTF8Encoding(false)))
                {
                    r.ReadLine();
                    string line;
                    while ((line = r.ReadLine()) != null)
                    {
                        if (line.Length == 0) continue;
                        string[] f = line.Split(',');
                        if (col >= f.Length) continue;
                        // status に関係なく、1行あるものは「回した」とみなす
                        // （止めた回・失敗した回を何度も回し直さないため）
                        if (statusCol < f.Length && f[statusCol].Length == 0) continue;
                        done.Add(f[col]);
                    }
                }
            }
            catch (Exception e)
            {
                Debug.LogWarning("[ServiceSim] 台帳を読めませんでした: " + e.Message);
            }
            return done;
        }

        /// <summary>
        /// 回した回を照らし合わせるキー（依頼 v8 の 3）。
        ///
        /// **店・ロボットの台数・乱数の種・Unity の版** でそろえる。
        /// 出力フォルダ名（日付入り）では照らし合わせない。
        ///   ・同じ版・同じ条件・同じ種の行があれば、日付が違っても回さない
        ///   ・版が違えば回す（直したあとに同じ18回をもう一度回すため）
        /// </summary>
        public static string RunKey(string store, int robots, int seed, string unityCs)
        {
            return (store ?? "") + "|r" + robots + "|s" + seed + "|" + (unityCs ?? "");
        }

        /// <summary>台帳にある回のキーをぜんぶ返す（続きから回すのに使う）</summary>
        public static HashSet<string> DoneKeys(string path) { return DoneKeys(path, null); }

        /// <summary>
        /// 台帳にある回のキーを返す（続きから回すのに使う）。
        ///
        /// folderLabel を渡すと、run_folder に "_{名札}_" が入っている行だけを見る（依頼 v11 の 3）。
        /// 名札は「まとめて回した種類」で、stageB（基準データの18回）と
        /// split_check（移せたかの確かめ）を分けるために使う。
        /// こうしておくと、確かめで ③1台・種1 を2回回しても、
        /// そのあとの Run All が同じ条件を「済」として飛ばさない。
        /// 台帳の列は増やしていない（名札は run_folder の中にある）
        /// </summary>
        public static HashSet<string> DoneKeys(string path, string folderLabel)
        {
            HashSet<string> done = new HashSet<string>();
            if (!File.Exists(path)) return done;

            int cStore = IndexOf("store");
            int cRobots = IndexOf("robots");
            int cSeed = IndexOf("seed");
            int cCs = IndexOf("unity_cs");
            int cStatus = IndexOf("status");
            int cFolder = IndexOf("run_folder");
            string mark = string.IsNullOrEmpty(folderLabel) ? null : "_" + folderLabel + "_";

            try
            {
                using (StreamReader r = new StreamReader(path, new UTF8Encoding(false)))
                {
                    r.ReadLine();
                    string line;
                    while ((line = r.ReadLine()) != null)
                    {
                        if (line.Length == 0) continue;
                        string[] f = line.Split(',');
                        int need = Mathf.Max(Mathf.Max(cStore, cRobots), Mathf.Max(cSeed, cCs));
                        if (need >= f.Length) continue;
                        if (cStatus < f.Length && f[cStatus].Length == 0) continue;
                        if (mark != null)
                        {
                            if (cFolder >= f.Length) continue;
                            if (f[cFolder].IndexOf(mark, StringComparison.Ordinal) < 0) continue;
                        }

                        int robots, seed;
                        if (!int.TryParse(f[cRobots], out robots)) continue;
                        if (!int.TryParse(f[cSeed], out seed)) continue;
                        done.Add(RunKey(f[cStore], robots, seed, f[cCs]));
                    }
                }
            }
            catch (Exception e)
            {
                Debug.LogWarning("[ServiceSim] 台帳を読めませんでした: " + e.Message);
            }
            return done;
        }

        static int IndexOf(string name)
        {
            for (int i = 0; i < Columns.Length; i++) if (Columns[i] == name) return i;
            return -1;
        }

        // ---------------- 1行足す ----------------

        /// <summary>
        /// 1行追記する。ヘッダが合わない・同じ出力フォルダの行がもうある場合は足さない。
        /// row は列名 → 値。足りない列は空欄にする
        /// </summary>
        public static bool Append(string path, IDictionary<string, string> row, out string why)
        {
            why = "";

            string folder = Get(row, "run_folder");
            if (string.IsNullOrEmpty(folder))
            {
                why = "run_folder が空なので足しません（照合キーです）";
                return false;
            }

            // 書く字は鍵を取る前に作る（鍵を持っている時間を短くする）
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < Columns.Length; i++)
            {
                if (i > 0) sb.Append(',');
                sb.Append(Clean(Get(row, Columns[i])));
            }

            // ★ 本当にマウントされているかを確かめる（依頼 v14 の 7）。
            //   Directory.Exists だけだと、KIOXIA が外れたあとに残った
            //   /Volumes/KIOXIA というフォルダ（Mac の中）にも書けてしまう
            string mountWhy;
            if (!IsOnMountedVolume(path, out mountWhy))
            {
                why = mountWhy;
                return false;
            }

            // 2026-10-26（codex レビュー D6）：置き場所を **作らない**。
            // 作ってしまうと、KIOXIA が外れているときに Mac の中へ
            // 同じ道のフォルダを作って書いてしまう。
            // KIOXIA の置き場所は前からあるのが正しい形なので、無ければ書かない
            string dir = Path.GetDirectoryName(path);
            if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir))
            {
                why = "台帳の置き場所がありません（作りません）: " + dir +
                      "\n  KIOXIA がつながっているか確かめてください（Mac の中には書きません）";
                return false;
            }

            // ---- ここから鍵の中（依頼 v12 の 3 章）----
            // ヘッダの確かめと「もうあるか」の確かめも **鍵の中** でやる。
            // 外でやると、2本が同時に「まだ無い」と見てから両方書いてしまう
            string lockWhy;
            IDisposable key = TakeLock(path, out lockWhy);
            if (key == null)
            {
                why = lockWhy;
                return false;
            }
            try
            {
                if (!CanAppend(path, out why)) return false;

                // 末尾が半端なら足さない（A4）
                if (!EndsWithNewline(path, out why)) return false;

                bool unreadable;
                if (HasRow(path, folder, out unreadable))
                {
                    why = "もう台帳にあるので足しません（2度書かない）: " + folder;
                    return false;
                }
                if (unreadable)
                {
                    // 読めないまま書くと2行目になるかもしれない（A5）
                    why = "台帳を読めないので、2度書きを避けて足しませんでした: " + folder;
                    return false;
                }

                try
                {
                    // 1行ぶんを UTF-8 の byte にしてから **1回で** 書く（依頼 v14 の 2）。
                    // 何度も書くと、途中で落ちたときに半端な行が残りやすい
                    bool makeHeader = !File.Exists(path);
                    string text = (makeHeader ? HeaderLine + "\n" : "") + sb.ToString() + "\n";
                    byte[] bytes = new UTF8Encoding(false).GetBytes(text);
                    using (FileStream fs = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read))
                    {
                        fs.Write(bytes, 0, bytes.Length);
                        fs.Flush();
                        fs.Flush(true);          // 落ちても行が消えないよう、ディスクまで落とす
                    }
                }
                catch (Exception e)
                {
                    why = "台帳に書けません: " + path + "\n  " + e.Message;
                    return false;
                }
                return true;
            }
            finally
            {
                key.Dispose();               // 鍵ファイルを消す
            }
        }

        // ---------------- 本当にマウントされているか（依頼 v14 の 7）----------------

        /// <summary>
        /// その道が **マウントされたボリュームの上** にあるかを確かめる。
        ///
        /// なぜ Directory.Exists では足りないか：
        ///   KIOXIA を抜いたあと /Volumes/KIOXIA というフォルダが
        ///   Mac の中に残ることがある。Directory.Exists はそれを通すので、
        ///   外付けに書いたつもりで Mac の中に書いてしまう。
        ///
        /// やり方：DriveInfo.GetDrives()（＝いまマウントされている所の一覧）に、
        ///   その道の頭と一致する所があるかを見る。
        ///   一覧が取れない場合（player の実行環境による）は、
        ///   **台帳の実体があること**を代わりの印にして、そのことをログに出す
        /// </summary>
        public static bool IsOnMountedVolume(string path, out string why)
        {
            why = "";
            string full;
            try { full = Path.GetFullPath(path); }
            catch (Exception e)
            {
                why = "道を読めません: " + path + "\n  " + e.Message;
                return false;
            }

            // "/" だけのときは Mac の中。外付けに書く約束と合わない
            string best = null;
            bool listed = false;
            try
            {
                DriveInfo[] drives = DriveInfo.GetDrives();
                for (int i = 0; i < drives.Length; i++)
                {
                    DirectoryInfo root = null;
                    try { root = drives[i].RootDirectory; } catch (Exception) { }
                    if (root == null) continue;
                    listed = true;
                    string r = root.FullName;
                    if (r.Length > 1 && r.EndsWith("/", StringComparison.Ordinal))
                        r = r.Substring(0, r.Length - 1);
                    if (r.Length == 0) continue;
                    bool under = full == r ||
                                 full.StartsWith(r == "/" ? "/" : r + "/", StringComparison.Ordinal);
                    if (under && (best == null || r.Length > best.Length)) best = r;
                }
            }
            catch (Exception e)
            {
                Debug.LogWarning("[ServiceSim] マウントの一覧が取れません（代わりに台帳の実体で見ます）: " +
                                 e.Message);
            }

            if (listed)
            {
                if (best != null && best != "/") return true;
                why = "ここはマウントされた外付けではありません（Mac の中です）: " + full +
                      "\n  いちばん近いマウント地点: " + (best ?? "（見つかりません）") +
                      "\n  KIOXIA がつながっているか確かめてください（Mac の中には書きません）";
                return false;
            }

            // 一覧が取れなかったとき：台帳の実体があることを代わりの印にする
            if (File.Exists(path))
            {
                Debug.LogWarning("[ServiceSim] マウントの一覧が取れないので、" +
                                 "台帳の実体があることで代わりに確かめました: " + path);
                return true;
            }
            why = "マウントの一覧が取れず、台帳の実体もありません: " + full +
                  "\n  KIOXIA がつながっているか確かめてください";
            return false;
        }

        // ---------------- 鍵（依頼 v12 の 3 章）----------------

        /// <summary>
        /// 台帳の鍵ファイルの置き場所（台帳と同じフォルダ）。
        /// tools/ledger.py と同じ場所・同じ名前なので、
        /// Unity と Python が同じ鍵を取り合える
        /// </summary>
        public static string LockPathOf(string ledgerPath)
        {
            string dir = Path.GetDirectoryName(ledgerPath);
            return string.IsNullOrEmpty(dir) ? LockFileName : Path.Combine(dir, LockFileName);
        }

        /// <summary>
        /// 鍵を取る。取れたら Dispose で消える入れ物を返し、取れなければ null と理由を返す。
        ///
        /// やり方（tools/ledger.py と同じ）:
        ///   ・FileMode.CreateNew で作れたら持ち主。中身は「pid ホスト名 unix秒」
        ///   ・作れなければ少し待って何度も試す（上限 LockTimeoutS）
        ///   ・鍵が LockStaleS より古ければ、持ち主が落ちたと見て **1度だけ** 壊す
        /// </summary>
        public static IDisposable TakeLock(string ledgerPath, out string why)
        {
            why = "";
            string lockPath = LockPathOf(ledgerPath);

            // この回だけの札。鍵の中身の先頭に書き、返すときに
            // 「自分の鍵か」を確かめるのに使う
            string tag = NewTag();

            // 待つ長さは単調時計で測る（時計合わせで伸び縮みしないように）
            System.Diagnostics.Stopwatch clock = System.Diagnostics.Stopwatch.StartNew();

            while (true)
            {
                try
                {
                    FileStream fs = new FileStream(lockPath, FileMode.CreateNew,
                                                   FileAccess.Write, FileShare.Read);
                    bool mine = false;
                    try
                    {
                        byte[] bytes = new UTF8Encoding(false).GetBytes(LockBody(tag));
                        fs.Write(bytes, 0, bytes.Length);
                        fs.Flush();
                        mine = TagOf(lockPath) == tag;     // 取れたあと読み直して確かめる
                    }
                    catch (Exception) { mine = false; }

                    if (mine) return new LedgerKey(fs, lockPath, tag);

                    try { fs.Dispose(); } catch (Exception) { }
                    Debug.LogWarning("[ServiceSim] 台帳の鍵を取り直します" +
                                     "（取った直後に中身が自分の札ではありませんでした）: " + lockPath);
                }
                catch (IOException)
                {
                    // もう誰かが持っている（か、作れない）
                }
                catch (Exception e)
                {
                    why = "台帳の鍵が作れません: " + lockPath + "\n  " + e.Message;
                    return null;
                }

                // ★ 古い鍵を **年だけ見て壊すことはしない**（依頼 v14 の 1）。
                //   2本が同時に「古い」と見ると、先に取り直した側の新しい鍵を
                //   もう一方が消して二重所有になりうる。
                //   鍵が残ったときは、待ちの上限を過ぎて「台帳に書けなかった」として
                //   その回を失敗にする（残った鍵は人が消す）
                if (clock.Elapsed.TotalSeconds >= LockTimeoutS)
                {
                    why = "台帳の鍵を " + LockTimeoutS + " 秒待っても取れません: " + lockPath +
                          "\n  ほかの回が書いている途中か、前の回の鍵が残っています" +
                          "\n  残っているなら、中身（札 pid ホスト名 時刻）を見てから" +
                          "手で消してください（自動では消しません）";
                    return null;
                }
                System.Threading.Thread.Sleep(250);
            }
        }

        /// <summary>鍵の中身。「札 pid ホスト名 unix秒」の1行</summary>
        static string LockBody(string tag)
        {
            return tag + " " + Pid() + " " + HostName() + " " +
                   UnixNow().ToString("0.000", CultureInfo.InvariantCulture) + "\n";
        }

        /// <summary>鍵の中身の先頭（札）。読めなければ null</summary>
        static string TagOf(string lockPath)
        {
            try
            {
                if (!File.Exists(lockPath)) return null;
                using (FileStream fs = new FileStream(lockPath, FileMode.Open, FileAccess.Read,
                                                      FileShare.ReadWrite))
                using (StreamReader r = new StreamReader(fs, new UTF8Encoding(false)))
                {
                    string line = r.ReadLine();
                    if (line == null) return "";
                    int sp = line.IndexOf(' ');
                    return sp > 0 ? line.Substring(0, sp) : line;
                }
            }
            catch (Exception) { return null; }
        }

        /// <summary>この回だけの札（ほかの回とぶつからない字）</summary>
        static string NewTag()
        {
            return "k" + Pid().ToString(CultureInfo.InvariantCulture) + "-" +
                   Guid.NewGuid().ToString("N").Substring(0, 12);
        }

        static int Pid()
        {
            try { return System.Diagnostics.Process.GetCurrentProcess().Id; }
            catch (Exception) { return 0; }
        }

        static string HostName()
        {
            // Environment.MachineName（Python 側の socket.gethostname と同じ意味）。
            // SystemInfo はエンジンが要るので使わない（player でも Editor でも動く形にする）
            try { return Environment.MachineName; }
            catch (Exception) { return "unity"; }
        }

        static double UnixNow()
        {
            return (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds;
        }

        /// <summary>取った鍵。Dispose で閉じて消す</summary>
        sealed class LedgerKey : IDisposable
        {
            FileStream _fs;
            readonly string _path;
            readonly string _tag;

            public LedgerKey(FileStream fs, string path, string tag)
            {
                _fs = fs; _path = path; _tag = tag;
            }

            public void Dispose()
            {
                if (_fs != null)
                {
                    try { _fs.Dispose(); } catch (Exception) { }
                    _fs = null;
                }
                // ★ 自分の札のときだけ消す（2026-10-26・codex レビュー A2）。
                //   前は中身を見ずに消していたので、
                //   自分の鍵が壊されたあとに他人の鍵を消す道があった
                try
                {
                    string now = TagOf(_path);
                    if (now == null) return;                  // もう無い・読めない
                    if (now != _tag)
                    {
                        Debug.LogWarning("[ServiceSim] 台帳の鍵が自分のものではないので消しません: " +
                                         _path + "\n  いま: " + now + " / 自分: " + _tag);
                        return;
                    }
                    File.Delete(_path);
                }
                catch (Exception) { }
            }
        }

        static string Get(IDictionary<string, string> row, string key)
        {
            string v;
            return row != null && row.TryGetValue(key, out v) && v != null ? v : "";
        }

        /// <summary>CSV を壊さないように、カンマ・改行・引用符は取り除く（値は数と短い名前だけ）</summary>
        static string Clean(string s)
        {
            if (string.IsNullOrEmpty(s)) return "";
            s = s.Replace(',', ' ').Replace('"', ' ');
            s = s.Replace("\r", " ").Replace("\n", " ");
            return s.Trim();
        }

        // ---------------- summary.json から1行を作る ----------------

        /// <summary>数を台帳の書き方にそろえる（小数4桁まで・指数表記にしない）</summary>
        public static string Num(float v)
        {
            return v.ToString("0.####", CultureInfo.InvariantCulture);
        }

        public static string Num(int v)
        {
            return v.ToString(CultureInfo.InvariantCulture);
        }

        public static string Flag(bool v) { return v ? "1" : "0"; }

        /// <summary>summary.json を読む（読めなければ null）</summary>
        public static JsonValue ReadSummary(string runDir)
        {
            string p = Path.Combine(runDir, "summary.json");
            if (!File.Exists(p)) return null;
            try
            {
                string err;
                JsonValue v = JsonValue.Parse(File.ReadAllText(p), out err);
                if (v == null || !v.IsObject)
                {
                    Debug.LogWarning("[ServiceSim] summary.json を読めません: " + p + "\n  " + err);
                    return null;
                }
                return v;
            }
            catch (Exception e)
            {
                Debug.LogWarning("[ServiceSim] summary.json を読めません: " + p + "\n  " + e.Message);
                return null;
            }
        }
    }
}
```

### Assets/ServiceSim/Experiment/LedgerRow.cs

```csharp
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>
    /// 通し台帳の1行を、その回の summary.json / run_info.json から作って足す
    /// （2026-10-26・依頼 v12 の 2〜3 章）。
    ///
    /// なぜ分けたか
    ///   段階Bの Run All（Editor）と、夜の画面なしの実行ファイル（player）の
    ///   両方が台帳に1行書く。列の作り方が2通りに分かれると、
    ///   同じ回でも書く値が違う、という最悪の壊れ方をする。
    ///   なので **行を作る所はここ1つだけ** にして、
    ///   Editor だけで取れる値（.asset・EditorPrefs）は呼ぶ側が Inputs に入れて渡す。
    ///
    ///   Editor 側：ServiceSimStageB.WriteLedgerRow が AssetDatabase と
    ///              EditorPrefs から詰める
    ///   player 側：HeadlessRun がシーンの参照と build_info.json から詰める
    ///
    /// 列の中身・status の決め方・2度書かない決まりは、移すときに変えていない。
    /// </summary>
    public static class LedgerRow
    {
        /// <summary>行を作るのに要る、外から渡す値</summary>
        public class Inputs
        {
            /// <summary>サイトの試算の値（版・店の形の版・条件ごとの site_result）</summary>
            public SimParams simParams;

            /// <summary>この回の条件（store・staff・robots・arrivals_per_h・mode・trays・fragile_by）</summary>
            public SimParams.Condition cond;

            /// <summary>run_info.json に無いときの控え（決まりの入切を読む）</summary>
            public RobotSpec robotSpec;
            public PeopleSpec peopleSpec;

            /// <summary>"ServiceSim/cs:12" の形</summary>
            public string unityCs;

            /// <summary>出力フォルダの名前（照合キー）と、その場所</summary>
            public string runFolder;
            public string runDir;

            /// <summary>種</summary>
            public int seed;

            /// <summary>実時間（秒）。run_info.json に wall_seconds があればそちらを使う</summary>
            public float wallS;

            /// <summary>台帳の実体（unity_ledger_v1.csv）</summary>
            public string ledgerPath;
        }


        /// <summary>
        /// その回の summary.json / run_info.json から台帳の1行を作って足す。
        /// 戻り値はログ用の短い1行。status も返す
        /// </summary>
        /// <summary>台帳に足せたか（2026-10-26・codex レビュー B3・D5）</summary>
        public enum AppendState
        {
            /// <summary>1行足せた</summary>
            Appended,
            /// <summary>もう同じ行があるので足さなかった（これは正しい動き）</summary>
            AlreadyThere,
            /// <summary>足せなかった（ヘッダ違い・鍵が取れない・読めない など）</summary>
            Failed,
        }

        public static string BuildAndAppend(Inputs inp, out string status)
        {
            AppendState state;
            return BuildAndAppend(inp, out status, out state);
        }

        /// <summary>
        /// 台帳に1行足す。足せたかどうかを state で返す。
        ///
        /// 2026-10-26（codex レビュー B3・D5）：前は足せなくても警告を出すだけで、
        /// 回は「完走・終了コード0」で終わっていた。
        /// つまり **台帳に行が無いのに成功**という道があった。
        /// 呼ぶ側（HeadlessRun）が state を見て、失敗なら終了コード 1 にする
        /// </summary>
        public static string BuildAndAppend(Inputs inp, out string status, out AppendState state)
        {
            if (inp == null) throw new ArgumentNullException("inp");

            SimParams sp = inp.simParams;
            SimParams.Condition cond = inp.cond;
            int seed = inp.seed;
            string folder = inp.runFolder;
            string runDir = inp.runDir;
            float wallS = inp.wallS;

            JsonValue sum = ServiceSimLedger.ReadSummary(runDir);
            JsonValue info = ReadJson(Path.Combine(runDir, "run_info.json"));

            // status の決め方（2026-10-26・codex レビュー D5 で厳しくした）
            //   前は summary.json があれば completed にしていたので、
            //   run_info.json が欠けている・壊れている回も completed になった。
            //   run_info.json には「途中で止めたか」が入っているので、
            //   それが読めない回を完走と呼ぶことはできない。
            //   **両方そろって初めて completed**にする（数の定義の変更）
            bool stoppedEarly = info != null && info["stopped_early"].B(false);
            if (sum == null)
            {
                status = "failed";
                Debug.LogWarning("[ServiceSim] summary.json が読めないので failed にします: " + runDir);
            }
            else if (info == null)
            {
                status = "failed";
                Debug.LogWarning("[ServiceSim] run_info.json が読めないので failed にします" +
                                 "（summary.json はあります）: " + runDir +
                                 "\n  途中で止めたかが分からない回を completed とは呼びません");
            }
            else
            {
                status = stoppedEarly ? "stopped" : "completed";
            }

            RobotSpec rs = inp.robotSpec;
            PeopleSpec ps = inp.peopleSpec;

            JsonValue simLike = sum != null ? sum["sim_like"] : null;
            JsonValue siteM = sum != null ? sum["site_metrics"] : null;
            JsonValue siteRun = sum != null ? sum["site_run"] : null;
            JsonValue trips = sum != null ? sum["trips"] : null;
            JsonValue stops = sum != null ? sum["stops"] : null;

            Dictionary<string, string> row = new Dictionary<string, string>();
            row["timestamp"] = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss");
            row["run_folder"] = folder;
            row["status"] = status;
            row["unity_cs"] = inp.unityCs != null ? inp.unityCs : "";
            row["layout_generation"] = sp != null ? sp.LayoutGeneration : "";
            row["params_model"] = sp != null ? sp.Model : "";

            if (cond != null)
            {
                row["store"] = cond.store;
                row["staff"] = ServiceSimLedger.Num(cond.staff);
                row["robots"] = ServiceSimLedger.Num(cond.robots);
                row["arrivals_per_h"] = ServiceSimLedger.Num(cond.arrivalsPerHour);
                row["mode"] = cond.mode;
                row["trays"] = ServiceSimLedger.Num(cond.trays);
                row["fragile_by"] = cond.fragileBy;
            }
            row["seed"] = ServiceSimLedger.Num(seed);

            // 決まり（回したときの設定）。run_info にあればそれを、無ければ .asset から
            row["staff_first_yield"] = info != null && !info["staff_first_yield"].IsNull
                ? ServiceSimLedger.Flag(info["staff_first_yield"].B(false))
                : ServiceSimLedger.Flag(ps != null && ps.staffFirstYield);
            row["yield_order_enabled"] = info != null && !info["yield_order_enabled"].IsNull
                ? ServiceSimLedger.Flag(info["yield_order_enabled"].B(false))
                : ServiceSimLedger.Flag(rs != null && rs.yieldOrderEnabled);
            row["one_robot_per_aisle"] = info != null && !info["one_robot_per_aisle"].IsNull
                ? ServiceSimLedger.Flag(info["one_robot_per_aisle"].B(false))
                : ServiceSimLedger.Flag(rs != null && rs.oneRobotPerAisle);
            row["robot_first_wait_s"] = Val(simLike, "robot_first_wait_s");

            row["everyone_left"] = siteRun != null && !siteRun["everyone_left"].IsNull
                ? ServiceSimLedger.Flag(siteRun["everyone_left"].B(false)) : "";
            row["total_minutes"] = Val(siteRun, "total_minutes");
            row["wall_seconds"] = info != null && !info["wall_seconds"].IsNull
                ? Val(info, "wall_seconds") : ServiceSimLedger.Num(wallS);

            row["entry_wait_mean"] = Val(siteM, "entry_wait_mean");
            row["staff_wait_mean"] = Val(siteM, "staff_wait_mean");
            row["dish_wait_mean"] = Val(siteM, "dish_wait_mean");
            row["cold_share"] = Val(siteM, "cold_share");
            row["robot_share"] = Val(siteM, "robot_share");
            row["staff_util"] = Val(siteM, "staff_util");
            row["robot_util"] = Val(siteM, "robot_util");
            row["walk_km_per_staff"] = Val(siteM, "walk_km_per_staff");

            // サイトの試算（同じ条件の site_result）
            if (cond != null)
            {
                row["site_entry_wait_mean"] = Site(cond, "entry_wait_mean");
                row["site_staff_wait_mean"] = Site(cond, "staff_wait_mean");
                row["site_dish_wait_mean"] = Site(cond, "dish_wait_mean");
                row["site_robot_share"] = Site(cond, "robot_share");
            }

            // サイトの unity_measures と同じ意味で Unity が測った値。
            // pause_rate・stuck_rate は hop_m（role=site_only）が要るので空欄にする
            row["robot_speed_mps"] = Val(trips, "robot_speed_mps");
            row["pause_rate"] = "";
            row["pause_s_mean"] = Val(trips, "pause_s_mean");
            row["stuck_rate"] = "";

            row["trips"] = Val(trips, "trips");
            row["timeouts"] = Val(trips, "timeouts");
            row["stuck_count"] = Val(stops, "stuck_count");
            row["stop_s_total"] = Val(stops, "stop_s_total");
            row["corner_stop_time_share"] = Val(simLike, "corner_stop_time_share");

            row["gave_up"] = Val(simLike, "gave_up");
            row["warped"] = Val(simLike, "warped");
            row["table_freed"] = Val(simLike, "table_freed");
            row["rescues"] = Val(siteM, "rescues");

            row["late_staff_dishes"] = Val(simLike, "late_staff_dishes");
            row["staff_yield_share_of_staff_time"] = Val(simLike, "staff_yield_share_of_staff_time");

            row["ledger_schema_version"] = ServiceSimLedger.Num(ServiceSimLedger.SchemaVersion);

            // ---- 依頼 v10b の 1：会計待ち・片付け待ちを試算と並べる（schema 2）----
            // 詰まる場所が試算（レジ）と Unity（片付け）で違うので、
            // 会計待ち・片付け待ちの卓・卓が使えるまでを、Unity とサイトの両方で並べる
            row["pay_wait_mean"] = Val(siteM, "pay_wait_mean");
            row["blocked_wait_mean"] = Val(siteM, "blocked_wait_mean");
            row["turn_mean"] = Val(siteM, "turn_mean");
            if (cond != null)
            {
                row["site_pay_wait_mean"] = Site(cond, "pay_wait_mean");
                row["site_blocked_wait_mean"] = Site(cond, "blocked_wait_mean");
                row["site_turn_mean"] = Site(cond, "turn_mean");
            }

            // 仕事が板に載ってから取られるまで（依頼 v10 の 2 で summary.json に入れた欄）。
            // 仕事の名前は StaffAgent.JobKindName と同じものを使う（字を手で書かない）
            JsonValue jobMean = sum != null ? sum["job_wait_mean_s"] : null;
            JsonValue jobMax = sum != null ? sum["job_wait_max_s"] : null;
            JsonValue jobOver = sum != null ? sum["job_wait_over_count"] : null;
            string clearName = StaffAgent.JobKindName(StaffJobKind.Clear);
            string payName = StaffAgent.JobKindName(StaffJobKind.Pay);
            row["clear_wait_mean_s"] = Val(jobMean, clearName);
            row["clear_wait_max_s"] = Val(jobMax, clearName);
            // 600 秒を超えた片付けが1件も無いときは、summary.json にその名前が出ないので 0 を書く
            row["clear_wait_over_count"] = ValOrZero(jobOver, clearName);
            row["pay_job_wait_mean_s"] = Val(jobMean, payName);

            string ledger = inp.ledgerPath;
            string why;
            if (ServiceSimLedger.Append(ledger, row, out why))
            {
                state = AppendState.Appended;
            }
            else if (why != null && why.IndexOf("2度書かない", StringComparison.Ordinal) >= 0)
            {
                // もう同じ行がある。これは決まりどおりの動きなので失敗にしない
                state = AppendState.AlreadyThere;
                Debug.Log("[ServiceSim] " + why);
            }
            else
            {
                state = AppendState.Failed;
                Debug.LogError("[ServiceSim] 台帳に足せませんでした: " + why);
            }

            return folder + " / " + status +
                   " / 実時間 " + (wallS / 60f).ToString("0.0") + " 分" +
                   " / 入店待ち " + Or(row, "entry_wait_mean") + " 分" +
                   " / ロボットが運んだ割合 " + Or(row, "robot_share");
        }

        static string Val(JsonValue v, string key)
        {
            if (v == null || v.IsNull) return "";
            JsonValue x = v[key];
            if (x.IsNull) return "";
            return ServiceSimLedger.Num(x.F(0f));
        }

        /// <summary>
        /// 数を返すが、入れ物はあって名前が無いときは 0 を返す（空欄にしない）。
        /// job_wait_over_count は「1件も無い」と「読めなかった」を分けたいので使う
        /// </summary>
        static string ValOrZero(JsonValue v, string key)
        {
            if (v == null || v.IsNull) return "";
            JsonValue x = v[key];
            return ServiceSimLedger.Num(x.IsNull ? 0f : x.F(0f));
        }

        static string Site(SimParams.Condition c, string key)
        {
            float f;
            return c != null && c.siteResult.TryGetValue(key, out f) ? ServiceSimLedger.Num(f) : "";
        }

        static string Or(Dictionary<string, string> row, string key)
        {
            string v;
            return row.TryGetValue(key, out v) && v.Length > 0 ? v : "-";
        }

        static JsonValue ReadJson(string path)
        {
            if (!File.Exists(path)) return null;
            try
            {
                string err;
                JsonValue v = JsonValue.Parse(File.ReadAllText(path), out err);
                return v != null && v.IsObject ? v : null;
            }
            catch (Exception) { return null; }
        }
    }
}
```

### Assets/ServiceSim/Experiment/HeadlessRun.cs

```csharp
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>
    /// 画面なしの実行ファイルで、1条件 × 1種を1回だけ回して終わる
    /// （2026-10-26・依頼 v12 の 2 章）。
    ///
    /// 1本の実行ファイル = 1回。終わったら台帳に1行書いて、終了コードを付けて終わる。
    /// 夜の実行係（tools/night_runner.py）が、これを N 本まで同時に起動する。
    ///
    /// 引数（night_runner.py が渡す形。2 章）
    ///   -ssCondition fam_p1_s4_r1_a42   … 条件の名前（必須。これが無ければ何もしない）
    ///   -ssSeed 3                        … 種（必須）
    ///   -ssLabel night                   … 出力フォルダに入る名札（既定 night）
    ///   -ssNight 20261026                … その夜の名前。フォルダ名の先頭にもなる
    ///   -ssParallel 3                    … 実行係に決めた同時本数 N（記録だけ）
    ///   -ssRunning 4                     … この回を起動したとき実際に同時に走る本数
    ///                                      （この回を含む。記録だけ。無ければ 0＝分からない）
    ///   -ssPassBy on|off                 … すれ違いの走り方（既定 on。2026-10-27 エイイチの決定）
    ///   -ssTry 2                          … やり直しの回。2 以上ならフォルダ名に _try2
    ///
    /// 出力フォルダ（tools/night_runner.py の folder_name と同じ字にする）
    ///   {夜}_{名札}_cs{N}_{店}_r{台数}_s{種}[_passon][_try{n}]
    ///   例 20261026_night_cs12_fam_p1_r1_s3
    ///   cs{N} は、実行ファイルのそばの build_info.json の unity_cs（ServiceSim/cs:12）から取る。
    ///   **実行ファイル側が名前を決める**（実行係は同じ式で先に名前を作って、
    ///   もう回した回を飛ばすのに使う）。
    ///
    /// 終わり方（2 章／依頼 v14 の 3）
    ///   0 … 最後まで回った
    ///   2 … 途中で止まった（実時間の上限など。記録は残る）
    ///   1 … 失敗（引数が読めない・条件が無い・KIOXIA が無い・記録が出ない）
    ///   3 … **台帳に書けなかった**（回そのものは終わっているかもしれない）。
    ///        重複で正しく断られた場合は 3 にしない
    ///
    /// KIOXIA（2 章）
    ///   **回し始める前に** つながっているかを確かめる。無ければ何も書かずに 1 で終わる。
    ///   Mac の中には書かない。
    ///
    /// 回し方は段階Bの Run All と同じ道を通す（ExperimentRunner.Job を1つ作って
    ///   SetSingleJob する）。台帳の行を作るのも同じ LedgerRow 1つだけ。
    /// </summary>
    public class HeadlessRun : MonoBehaviour
    {
        /// <summary>Setup が作るときの名前</summary>
        public const string RootName = "HeadlessRun";

        /// <summary>終了コード（依頼 v12 の 2 章・v14 の 3）</summary>
        public const int ExitOk = 0;
        public const int ExitFailed = 1;
        public const int ExitStopped = 2;

        /// <summary>
        /// 台帳に書けなかった（依頼 v14 の 3）。
        /// 回そのものの終わり方（completed / stopped）とは別に、
        /// 「行が残らなかった」ことだけを指す。
        /// 実行係はこれを「台帳に書けなかった回」として記録に出す
        /// </summary>
        public const int ExitLedgerFailed = 3;

        /// <summary>build_info.json を上に何階層まで探すか（.app の中から外に出るため）</summary>
        const int BuildInfoUpLevels = 6;

        /// <summary>回し始めるまでにこれだけかかったら、何か起きていると見て失敗にする（秒）</summary>
        const float StartTimeoutS = 180f;

        [Header("参照（Setup が入れる）")]
        public ExperimentRunner runner;
        public ExperimentPlan plan;
        public SimParams simParams;
        public StoreBuilder store;
        public NavMeshBaker baker;
        public RobotSpec robotSpec;
        public PeopleSpawner people;
        public KitchenMonitor monitor;

        [Tooltip("条件の店（fam_p1 / fam_p5）に合わせて選ぶ店の形。Setup が入れる")]
        public JsonStoreLayout[] layouts;

        // ---- 引数から読んだもの ----
        string _condition = "", _label = "night", _night = "", _passBy = "on";
        int _seed, _parallel, _parallelActual, _try = 1;

        string _unityCs = "", _buildId = "", _buildInfoPath = "";
        string _folder = "", _runDir = "";
        SimParams.Condition _cond;

        bool _active;          // 引数があり、回す気がある
        bool _sawRunning;      // runner が回り始めたのを見た
        bool _finished;        // もう後片付けをした
        float _awakeAt;

        void Awake()
        {
            // 引数に -ssCondition が無ければ、この部品は何もしない。
            // エディタで手で Play したときや、段階Bの Run All のときは必ずこちら
            string[] argv = SafeArgs();
            if (Find(argv, "-ssCondition") == null) return;

            _awakeAt = Time.realtimeSinceStartup;

            int code;
            if (!Setup(argv, out code))
            {
                Quit(code, "回し始める前に止めました");
                return;
            }
            _active = true;
        }

        /// <summary>
        /// 引数を読み、確かめ、1回ぶんの仕事を仕込む。
        /// 駄目なときは false と終了コードを返す（何も書かない）
        /// </summary>
        bool Setup(string[] argv, out int code)
        {
            code = ExitFailed;

            // ---- 引数 ----
            _condition = Find(argv, "-ssCondition") ?? "";
            string seedText = Find(argv, "-ssSeed");
            if (string.IsNullOrEmpty(_condition) || seedText == null ||
                !int.TryParse(seedText, NumberStyles.Integer, CultureInfo.InvariantCulture, out _seed))
            {
                Log("-ssCondition と -ssSeed が要ります（例 -ssCondition fam_p1_s4_r1_a42 -ssSeed 3）");
                return false;
            }
            _label = Find(argv, "-ssLabel") ?? "night";
            _night = Find(argv, "-ssNight") ?? DateTime.Now.ToString("yyyyMMdd", CultureInfo.InvariantCulture);
            _passBy = (Find(argv, "-ssPassBy") ?? "on").ToLowerInvariant() == "off" ? "off" : "on";
            int.TryParse(Find(argv, "-ssParallel") ?? "0", NumberStyles.Integer,
                         CultureInfo.InvariantCulture, out _parallel);
            // 起動したときに実際に同時に走る本数（この回を含む）。
            // 無ければ 0＝分からない（実行係を通していない起動）
            int.TryParse(Find(argv, "-ssRunning") ?? "0", NumberStyles.Integer,
                         CultureInfo.InvariantCulture, out _parallelActual);
            if (!int.TryParse(Find(argv, "-ssTry") ?? "1", NumberStyles.Integer,
                              CultureInfo.InvariantCulture, out _try) || _try < 1) _try = 1;

            // ---- シーンの参照 ----
            if (runner == null) runner = FindObjectOfType<ExperimentRunner>();
            if (plan == null && runner != null) plan = runner.plan;
            if (simParams == null && runner != null) simParams = runner.simParams;
            if (store == null && runner != null) store = runner.store;
            if (baker == null && runner != null) baker = runner.baker;
            if (robotSpec == null && runner != null) robotSpec = runner.robotSpec;
            if (people == null && runner != null) people = runner.people;
            if (monitor == null && runner != null) monitor = runner.monitor;

            if (runner == null || plan == null || simParams == null || store == null || baker == null)
            {
                Log("シーンの仕掛けが足りません（ExperimentRunner / ExperimentPlan / SimParams / " +
                    "StoreBuilder / NavMeshBaker）。ServiceSim/Setup を実行したシーンで作り直してください");
                return false;
            }

            // ---- 条件 ----
            // ★ player では誰も SimParams.Load を呼んでいないので、
            //   自分で読む（段階Bの LoadParams と同じ sp.Load("")）。
            //   読んだ中身（_conditions）は .asset に残らない作りなので、
            //   Play のたび・起動のたびに読み直す必要がある。
            //   このあと ExperimentRunner が店の形の版を渡して読み直すが、
            //   同じ JSON を読むので値は変わらない
            if (!simParams.Load("") || simParams.Conditions.Count == 0)
            {
                Log("serving-robot-params.json の unity_conditions を読めません" +
                    (simParams.Problems.Count > 0
                        ? "\n  " + string.Join("\n  ", ToArray(simParams.Problems)) : ""));
                return false;
            }

            _cond = ConditionById(_condition);
            if (_cond == null)
            {
                Log("知らない条件です: " + _condition + "\n  使える条件: " + KnownConditions());
                return false;
            }

            // ---- 店の形 ----
            JsonStoreLayout layout = LayoutFor(_cond.store);
            if (layout == null)
            {
                Log("店の形が見つかりません: " + _cond.store +
                    "（HeadlessRun の layouts に入っていません。ServiceSim/Setup を実行してください）");
                return false;
            }

            // ---- 版（出力フォルダの字と、台帳の unity_cs）----
            ReadBuildInfo();
            if (string.IsNullOrEmpty(_unityCs))
                Log("注意：build_info.json の unity_cs が読めませんでした。" +
                    "出力フォルダは csunknown になります（" + _buildInfoPath + "）");

            _folder = FolderName();
            _runDir = Path.Combine(plan.kioxiaOutputRoot, _folder);

            // ---- KIOXIA（書く前に確かめる。無ければ何も書かない）----
            // ★ Directory.Exists だけでなく、**本当にマウントされているか** を見る
            //   （依頼 v14 の 7）。抜いたあとに残った /Volumes/KIOXIA という
            //   フォルダ（Mac の中）に書いてしまわないため
            string mountWhy;
            if (!ServiceSimLedger.IsOnMountedVolume(plan.kioxiaOutputRoot, out mountWhy))
            {
                Log("KIOXIA がつながっていません（マウントを確かめました）\n  " +
                    mountWhy.Replace("\n", "\n  ") +
                    "\n  何も書かずに終わります（Mac の中には書きません）");
                return false;
            }
            if (!Directory.Exists(plan.kioxiaOutputRoot))
            {
                Log("置き場所がありません: " + plan.kioxiaOutputRoot +
                    "\n  MASTER の保存先です。新しく作らずに確かめてください");
                return false;
            }
            if (Directory.Exists(_runDir))
            {
                Log("その出力フォルダはもうあります: " + _runDir +
                    "\n  上書きしません（やり直しは -ssTry 2 で別の名前にしてください）");
                return false;
            }

            // ---- 台帳のヘッダ（書けない形なら、回す前に止める）----
            string ledgerPath = ServiceSimLedger.PathOf(plan);
            string headerWhy;
            if (File.Exists(ledgerPath) && !ServiceSimLedger.CanAppend(ledgerPath, out headerWhy))
            {
                Log("台帳に書けない形です。回す前に止めます\n  " + headerWhy.Replace("\n", "\n  "));
                return false;
            }

            // ---- 仕込む（段階Bの StartNext と同じ形）----
            store.layout = layout;
            baker.layout = layout;

            if (robotSpec != null) robotSpec.passByEnabled = _passBy == "on";

            ExperimentRunner.Job job = new ExperimentRunner.Job();
            job.siteRun = true;
            job.conditionIndex = ConditionIndexOf(_cond);
            job.storeKey = _cond.store;
            job.robots = _cond.robots;
            job.seed = _seed;
            job.aisleWidthIndex = layout.aisleWidthIndex;
            job.crowd = CrowdLevel.Normal;
            job.applyDeliveryOffset = false;
            job.folder = _runDir;

            runner.SetSingleJob(job);
            runner.timeMode = ExperimentRunner.TimeMode.FixedStep;
            runner.captureDeltaTimeOverride = 0f;
            // 1回の実時間の上限は段階Bと同じ（値は ExperimentRunner に1つだけ置いてある）
            runner.maxWallSecondsPerRun = ExperimentRunner.DefaultMaxWallSecondsPerRun;
            runner.unityCs = _unityCs;
            runner.pendingNote = "";
            runner.runLabel = _label;
            runner.viewMode = false;
            runner.runnerName = "headless";
            runner.buildId = _buildId;
            runner.night = _night;
            runner.parallel = _parallel;
            runner.parallelActual = _parallelActual;
            runner.runOnPlay = true;        // Awake なので、runner の Start より前に立つ

            if (monitor != null) monitor.show = false;   // 画面のものは切る（2 章）

            Log("回します: " + _condition + " 種 " + _seed +
                " / すれ違い " + (_passBy == "on" ? "入" : "切") +
                (_try > 1 ? " / やり直し " + _try + " 回め" : "") +
                "\n  版 " + (_unityCs.Length > 0 ? _unityCs : "（分からない）") +
                " / build " + (_buildId.Length > 0 ? _buildId : "（分からない）") +
                " / 夜 " + _night + " / 同時 " + _parallel +
                "（実際 " + (_parallelActual > 0 ? _parallelActual.ToString() : "分からない") + "）" +
                "\n  置き場所 " + _runDir);

            code = ExitOk;
            return true;
        }

        void Update()
        {
            if (!_active || _finished) return;

            if (!_sawRunning)
            {
                if (runner.Running) { _sawRunning = true; return; }

                // 合わない値で回さずに抜けた（ExperimentRunner.AbortRun）。
                // 待っても始まらないので、すぐ失敗にする
                if (runner.Aborted)
                {
                    Quit(ExitFailed, "合わない値があって回せませんでした" +
                                     "（ログの上の行に理由が出ています）");
                    return;
                }
                if (Time.realtimeSinceStartup - _awakeAt > StartTimeoutS)
                {
                    Quit(ExitFailed, "回し始められませんでした（" + StartTimeoutS + " 秒待ちました）");
                }
                return;
            }

            if (runner.Running) return;     // まだ回っている

            // 回り終わった
            Finish();
        }

        /// <summary>
        /// 回り終わったあと：台帳に1行書いて、終了コードを決めて終わる。
        /// 行を作るのは段階Bと同じ LedgerRow 1つだけ
        /// </summary>
        void Finish()
        {
            _finished = true;

            // ExperimentRunner が店の形の版で SimParams を読み直しているので、
            // 条件の入れ物は作り直されている。名前で引き直す
            // （引けなければ回し始めに取ったものを使う。値は同じ JSON から来る）
            SimParams.Condition cond = ConditionById(_condition);
            if (cond == null) cond = _cond;

            LedgerRow.Inputs inp = new LedgerRow.Inputs();
            inp.simParams = simParams;
            inp.cond = cond;
            inp.robotSpec = robotSpec;
            inp.peopleSpec = people != null ? people.spec : null;
            inp.unityCs = _unityCs;
            inp.runFolder = _folder;
            inp.runDir = _runDir;
            inp.seed = _seed;
            inp.wallS = Time.realtimeSinceStartup - _awakeAt;
            inp.ledgerPath = ServiceSimLedger.PathOf(plan);

            string status = "failed";
            string line;
            LedgerRow.AppendState state = LedgerRow.AppendState.Failed;
            try
            {
                line = LedgerRow.BuildAndAppend(inp, out status, out state);
            }
            catch (Exception e)
            {
                line = "台帳の行を作れませんでした: " + e.Message;
                status = "failed";
                state = LedgerRow.AppendState.Failed;
            }

            int code = status == "completed" ? ExitOk
                     : status == "stopped" ? ExitStopped
                     : ExitFailed;

            // ★ 台帳に足せなかったら、回が最後まで回っていても失敗にする
            //   （2026-10-26・codex レビュー B3・D5）。
            //   「完走・終了コード0・台帳に行なし」を作らないため。
            //   もう同じ行があるとき（AlreadyThere）は決まりどおりなので失敗にしない
            if (state == LedgerRow.AppendState.Failed)
            {
                line += "\n  ※ 回は " + status + " で終わりましたが、台帳に足せませんでした";
                code = ExitLedgerFailed;        // 依頼 v14 の 3：ほかの失敗と見分ける
            }

            Quit(code, "おわり: " + line);
        }

        // ---------------- 細かい道具 ----------------

        /// <summary>
        /// 出力フォルダの名前。
        /// tools/night_runner.py の folder_name と **同じ字** にする
        /// （実行係は同じ名前を先に作って、もう回した回を飛ばすのに使う）
        /// </summary>
        string FolderName()
        {
            return FolderName(_night, _label, CsToken(_unityCs),
                              _cond.store, _cond.robots, _seed, _passBy, _try);
        }

        /// <summary>
        /// 出力フォルダの名前（計算だけ。確かめのために外からも呼べるようにしてある）。
        ///
        /// ★ tools/night_runner.py の folder_name と **1字でも違ってはいけない**。
        ///   実行係はこの名前を先に作って「もう回した回」を飛ばすので、
        ///   食い違うと同じ回を2度回したり、回したのに飛ばしたりする
        /// </summary>
        public static string FolderName(string night, string label, string csToken,
                                        string store, int robots, int seed,
                                        string passBy, int tryNo)
        {
            string name = night + "_" + label + "_" + csToken + "_" +
                          store + "_r" + robots.ToString(CultureInfo.InvariantCulture) +
                          "_s" + seed.ToString(CultureInfo.InvariantCulture);
            if (passBy == "on") name += "_passon";
            if (tryNo > 1) name += "_try" + tryNo.ToString(CultureInfo.InvariantCulture);
            return name;
        }

        /// <summary>
        /// "ServiceSim/cs:12" → "cs12"。読めなければ "csunknown"。
        /// tools/night_runner.py の cs_token（既定 "cs{N}"）と同じ字にする
        /// </summary>
        public static string CsToken(string unityCs)
        {
            string n = "";
            int at = (unityCs ?? "").IndexOf("cs:", StringComparison.Ordinal);
            if (at >= 0)
            {
                for (int i = at + 3; i < unityCs.Length; i++)
                {
                    char c = unityCs[i];
                    if (c < '0' || c > '9') break;
                    n += c;
                }
            }
            return "cs" + (n.Length > 0 ? n : "unknown");
        }

        /// <summary>
        /// 実行ファイルのそばの build_info.json から、版（unity_cs）と build_id を読む。
        ///
        /// macOS の .app は Application.dataPath が
        ///   …/ServiceSim.app/Contents/Resources/Data
        /// なので、そこから上に何階層か探す（Builds/headless/ に置いてある形）
        /// </summary>
        void ReadBuildInfo()
        {
            string dir = Application.dataPath;
            for (int i = 0; i <= BuildInfoUpLevels && !string.IsNullOrEmpty(dir); i++)
            {
                string p = Path.Combine(dir, "build_info.json");
                if (File.Exists(p))
                {
                    _buildInfoPath = p;
                    try
                    {
                        string err;
                        JsonValue v = JsonValue.Parse(File.ReadAllText(p), out err);
                        if (v != null && v.IsObject)
                        {
                            _unityCs = v["unity_cs"].S("");
                            _buildId = v["build_id"].S("");
                            return;
                        }
                        Log("build_info.json が読めません: " + p + "\n  " + err);
                    }
                    catch (Exception e)
                    {
                        Log("build_info.json が読めません: " + p + "\n  " + e.Message);
                    }
                    return;
                }
                dir = Path.GetDirectoryName(dir);
            }
            _buildInfoPath = "（見つかりません。" + Application.dataPath + " から上に " +
                             BuildInfoUpLevels + " 階層まで探しました）";
        }

        SimParams.Condition ConditionById(string id)
        {
            if (simParams == null) return null;
            for (int i = 0; ; i++)
            {
                SimParams.Condition c = simParams.ConditionAt(i);
                if (c == null) return null;
                if (c.Id == id) return c;
            }
        }

        int ConditionIndexOf(SimParams.Condition cond)
        {
            if (simParams == null || cond == null) return -1;
            for (int i = 0; ; i++)
            {
                SimParams.Condition c = simParams.ConditionAt(i);
                if (c == null) return -1;
                if (c == cond) return i;
            }
        }

        string KnownConditions()
        {
            List<string> ids = new List<string>();
            for (int i = 0; simParams != null; i++)
            {
                SimParams.Condition c = simParams.ConditionAt(i);
                if (c == null) break;
                ids.Add(c.Id);
            }
            return ids.Count > 0 ? string.Join(", ", ids.ToArray()) : "（読めません）";
        }

        /// <summary>条件の店（fam_p1 / fam_p5）に合う店の形を選ぶ</summary>
        JsonStoreLayout LayoutFor(string storeKey)
        {
            if (layouts == null) return null;
            for (int i = 0; i < layouts.Length; i++)
            {
                if (layouts[i] == null) continue;
                if (layouts[i].ParamsStoreKey == storeKey) return layouts[i];
            }
            return null;
        }

        /// <summary>引数 name の次の字を返す。無ければ null</summary>
        static string Find(string[] argv, string name)
        {
            if (argv == null) return null;
            for (int i = 0; i < argv.Length - 1; i++)
                if (argv[i] == name) return argv[i + 1];
            return null;
        }

        static string[] ToArray(System.Collections.Generic.IList<string> list)
        {
            string[] a = new string[list != null ? list.Count : 0];
            for (int i = 0; i < a.Length; i++) a[i] = list[i];
            return a;
        }

        static string[] SafeArgs()
        {
            try { return Environment.GetCommandLineArgs(); }
            catch (Exception) { return new string[0]; }
        }

        static void Log(string msg)
        {
            Debug.Log("[ServiceSim/headless] " + msg);
        }

        void Quit(int code, string why)
        {
            _finished = true;
            if (code == ExitOk) Log(why + "\n  終了コード 0（最後まで回りました）");
            else if (code == ExitStopped) Debug.LogWarning("[ServiceSim/headless] " + why +
                                                           "\n  終了コード 2（途中で止まりました）");
            else if (code == ExitLedgerFailed) Debug.LogError("[ServiceSim/headless] " + why +
                                                              "\n  終了コード 3（台帳に書けませんでした）");
            else Debug.LogError("[ServiceSim/headless] " + why + "\n  終了コード 1（失敗）");

#if UNITY_EDITOR
            // エディタでは Play を抜けるだけ（終了コードは付かない）
            UnityEditor.EditorApplication.isPlaying = false;
#else
            Application.Quit(code);
#endif
        }
    }
}
```

### Assets/ServiceSim/Experiment/ExperimentRunner.cs

```csharp
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>
    /// 条件を回して記録する。1回の Play の中で、条件を順に通せる（段階3の Run All）。
    /// 数字と場所は ExperimentPlan の .asset に置く。回し方だけメニューが決める。
    ///
    /// この script は実験だけを扱う。店の運営は ServiceOps、人は GroupManager に任せる。
    /// </summary>
    public class ExperimentRunner : MonoBehaviour
    {
        /// <summary>時間の進め方</summary>
        public enum TimeMode { RealTime, FastTimeScale, FixedStep }

        /// <summary>回す条件1つ</summary>
        [System.Serializable]
        public class Job
        {
            public int aisleWidthIndex;
            public CrowdLevel crowd;
            public int robots;
            public int seed;
            public string folder;
            public string conditionId;

            /// <summary>
            /// 卓の停止点を通路の中心からずらす距離（m）。メニューが決める（C1）。
            /// applyDeliveryOffset が false のときは、.asset の値をそのまま使う。
            /// true のときも、使うのはこの回だけ。回し終わりに .asset の値へ戻すので、
            /// .asset は書き換わらない（run_info.json の delivery_offset_used_m に実際の値が出る）。
            /// </summary>
            public bool applyDeliveryOffset;
            public float deliveryOffset;

            // ---- サイトの条件で回す（v5 3章）----
            /// <summary>
            /// サイトの条件（unity_conditions）で回すか。
            /// true なら、店の回し方の数字は serving-robot-params.json から読み、
            /// 始め方もサイトと同じ「空の店から peak_min 分だけ来客し、全員が帰るまで」にする。
            /// </summary>
            public bool siteRun;

            /// <summary>unity_conditions の何番めか（siteRun のとき）</summary>
            public int conditionIndex = -1;

            /// <summary>試算の stores の名前（fam_p1 / fam_p5）。報告とフォルダ名に使う</summary>
            public string storeKey = "";
        }

        [Header("参照（Setup が入れる）")]
        public ExperimentPlan plan;
        public ServiceSpec service;

        [Tooltip("サイトの試算の値（v5 3章）。サイトの条件で回すときに読む")]
        public SimParams simParams;

        public StoreBuilder store;
        public NavMeshBaker baker;
        public PeopleSpawner people;
        public ServiceOps ops;
        public GroupManager groups;
        public JamWatcher jams;

        [Tooltip("キッチンの屋根のモニター（2026-10-23）。測る回でも出す")]
        public KitchenMonitor monitor;
        public RobotSpec robotSpec;
        public GameObject robotPrefab;
        public Transform robotsRoot;

        [Header("回し方（メニューが決める）")]
        public bool runOnPlay;

        /// <summary>
        /// 見る用の回か。字幕と描画をこの回だけ出し、時間は1倍速で進める。
        /// 設定ファイル（ExperimentPlan）は書き換えない。計測の比べには使わない。
        /// </summary>
        public bool viewMode;
        public TimeMode timeMode = TimeMode.FixedStep;
        public string runLabel = "v2";
        public float captureDeltaTimeOverride;

        [Tooltip("1回の実時間の上限（秒）。0 なら上限なし。" +
                 "超えたらその回を書き出して止める（段階B。台帳の status は stopped）。" +
                 "シミュレーションの動きは変えない")]
        public float maxWallSecondsPerRun;

        /// <summary>
        /// 1回の実時間の上限（秒）の既定。段階Bの Run All（40 分）と、
        /// 画面なしの実行ファイル（依頼 v12 の 2 章）で **同じ値** を使う。
        /// 2か所に書くと片方だけ変わるので、置き場所はここ1つにする
        /// </summary>
        public const float DefaultMaxWallSecondsPerRun = 40f * 60f;

        [Tooltip("回し始めの Unity Version Control の changeset（段階Bのメニューが入れる）。run_info に残す")]
        public string unityCs = "";

        [Tooltip("回し始めに保留中の変更があったか（段階Bのメニューが入れる）。run_info に残す")]
        public string pendingNote = "";

        [Header("いまの様子（読むだけ）")]
        [SerializeField] int _jobIndex;
        [SerializeField] int _jobCount;
        [SerializeField] float _simMinutes;
        [SerializeField] float _wallSeconds;
        [SerializeField] int _runsDone;
        [SerializeField] bool _running;

        // Play に入るとき、Unity はいちど script を読み直す（Domain Reload）。
        // そのときに消えないように、回す条件は .unity に書き出して持たせる。
        // メニューが入れた条件が Start() まで残らないと「回す条件がありません」で止まる。
        [SerializeField] List<Job> _queue = new List<Job>();

        readonly TripLogger _logger = new TripLogger();
        readonly List<ServingRobotController> _robots = new List<ServingRobotController>();
        readonly TripLogger.RunInfo _info = new TripLogger.RunInfo();
        readonly List<int> _walkerSamples = new List<int>();

        /// <summary>時間帯ごとの記録（依頼 v10b の 2。timeline.csv）</summary>
        readonly TimelineLogger _timeline = new TimelineLogger();

        /// <summary>
        /// ロボット1台ずつの位置と状態（2026-10-27）。robot_track.csv。
        /// **夜の回でも常に書く**（切り替えは作らない）。
        /// 刻みは店の中の時間なので、Mac の忙しさで行が変わらない
        /// </summary>
        readonly RobotTrackLogger _robotTrack = new RobotTrackLogger();

        /// <summary>
        /// ロボットどうしのすれ違いを数える（依頼 v13 の 依頼1。passes.csv）。
        /// 見るだけなので、passByEnabled が切の回でも同じように数える
        /// </summary>
        readonly PassByWatch _passBy = new PassByWatch();

        /// <summary>次に timeline の1行を取る時刻（店の中, 秒）</summary>
        float _nextTimelineS;
        float _nextRobotTrackS;

        /// <summary>ロボットの位置を取る刻み（店の中の秒）。1コマごとには取らない</summary>
        const float RobotTrackStepS = 5f;

        /// <summary>timeline の間（秒）。店の中の時間で 60 秒ごと</summary>
        const float TimelineStepS = 60f;

        /// <summary>計測が始まってからこの回数走っても記録が0なら、取りこぼしとみて止める</summary>
        const int TripRecordAlarmRuns = 3;

        Job _job;

        // 停止点は、メニューが決めた値をこの回だけ使う（C1）。
        // .asset を書き換えたままにしないよう、もとの値を覚えて回し終わりに戻す。
        float _savedDeliveryOffset;
        bool _deliveryOffsetSaved;

        // サイトの試算の値も、この回だけ入れて回し終わりに戻す（v5 3章）
        readonly SiteValues _siteValues = new SiteValues();
        SimParams.Condition _condition;

        /// <summary>来客を受け付け終わる時刻（店の中, 秒）。siteRun のときだけ使う</summary>
        float _arrivalsEndS;

        /// <summary>全員が帰るのを待つ上限（店の中, 秒）。これを超えたら打ち切る</summary>
        float _hardEndS;

        /// <summary>全員が帰って終わったか（打ち切りでないか）</summary>
        bool _everyoneLeft;

        float _endS, _measureStartS, _wallStart, _lastLog, _timeOffset;
        int _tripSeq;
        int _runsFinished;        // ロボットが走り終わった回数（慣らしも含む）
        int _runsAfterMeasure;    // 計測が始まってから走り終わった回数
        bool _stoppedEarly;
        bool _jobRunning;

        /// <summary>
        /// 1倍で回している（見る専用）か。2026-10-26・依頼 v13 の 依頼2。
        /// true のときは記録を書き出さない
        /// </summary>
        bool _viewOnly;

        // ---- 誰が回したか（依頼 v12 の 2 章・2026-10-26。run_info.json に残す）----
        /// <summary>"editor"（段階B・Site Run）/ "headless"（画面なしの実行ファイル）</summary>
        [HideInInspector] public string runnerName = "editor";

        /// <summary>画面なしの実行ファイルの版（build_info.json の build_id）</summary>
        [HideInInspector] public string buildId = "";

        /// <summary>その夜の名前（例 20261026）</summary>
        [HideInInspector] public string night = "";

        /// <summary>夜の実行係に決めた同時本数（N）</summary>
        [HideInInspector] public int parallel;

        /// <summary>
        /// この回を起動したときに実際に同時に走ることになった本数（この回を含む）。
        /// 0 は分からない（2026-10-26・依頼のあとの追加）
        /// </summary>
        [HideInInspector] public int parallelActual;

        /// <summary>この条件ぶんを書き出したか（途中で止めたときに二重に書かないため。v5 3章）</summary>
        bool _outputsWritten;

        public bool Running { get { return _running; } }

        /// <summary>
        /// 合わない値があって回さずに抜けたか（AbortRun を通ったか）。
        /// 画面なしの実行（HeadlessRun）が、待たずに失敗と分かるために読む
        /// （2026-10-26・依頼 v12 の 2 章）
        /// </summary>
        public bool Aborted { get { return _aborted; } }
        bool _aborted;

        /// <summary>回す条件を1つだけ入れる（段階2のやり方）</summary>
        public void SetSingleJob(Job job)
        {
            _queue.Clear();
            _queue.Add(job);
        }

        /// <summary>回す条件をまとめて入れる（段階3の Run All）</summary>
        public void SetQueue(IList<Job> jobs)
        {
            _queue.Clear();
            if (jobs != null) for (int i = 0; i < jobs.Count; i++) _queue.Add(jobs[i]);
        }

        void Start()
        {
            if (!runOnPlay) return;
            if (!Resolve()) return;
            if (_queue.Count == 0) { Debug.LogError("[ServiceSim] 回す条件がありません"); return; }

            Application.runInBackground = true;
            ApplyTimeMode();
            // 見る用の回は、描画を止めない（字幕も ServiceSimLabels が出す）
            // 画面なしの実行では、描くものが無いのでカメラも切る（依頼 v12 の 2 章）。
            // -nographics では元から何も描かれないが、はっきり切っておく
            if ((plan.disableRenderingWhileRunning || Application.isBatchMode) && !viewMode) DisableCameras();

            _jobIndex = 0;
            _jobCount = _queue.Count;
            _runsDone = 0;
            _wallStart = Time.realtimeSinceStartup;
            _running = true;
            StartJob(_queue[0]);
        }

        bool Resolve()
        {
            if (store == null) store = FindObjectOfType<StoreBuilder>();
            if (baker == null) baker = FindObjectOfType<NavMeshBaker>();
            if (people == null) people = FindObjectOfType<PeopleSpawner>();
            if (ops == null) ops = FindObjectOfType<ServiceOps>();
            if (groups == null) groups = FindObjectOfType<GroupManager>();
            if (jams == null) jams = FindObjectOfType<JamWatcher>();
            if (monitor == null) monitor = FindObjectOfType<KitchenMonitor>();
            if (robotsRoot == null && store != null && store.transform.parent != null)
                robotsRoot = store.transform.parent.Find("Robots");

            if (plan == null || service == null || store == null || baker == null || people == null ||
                ops == null || groups == null || jams == null || robotPrefab == null ||
                robotsRoot == null || store.layout == null)
            {
                Debug.LogError("[ServiceSim] ExperimentRunner の参照が足りません。ServiceSim/Setup を実行してください");
                return false;
            }
            return true;
        }

        void ApplyTimeMode()
        {
            Time.captureDeltaTime = 0f;
            switch (timeMode)
            {
                case TimeMode.RealTime: Time.timeScale = 1f; break;
                case TimeMode.FastTimeScale: Time.timeScale = plan.trialTimeScaleFast; break;
                default:
                    Time.timeScale = 1f;
                    Time.captureDeltaTime = captureDeltaTimeOverride > 0f
                        ? captureDeltaTimeOverride : plan.measureCaptureDeltaTime;
                    break;
            }
        }

        void DisableCameras()
        {
            Camera[] cams = FindObjectsOfType<Camera>();
            for (int i = 0; i < cams.Length; i++) cams[i].enabled = false;
        }

        // ---------------- 1条件ぶん ----------------

        void StartJob(Job job)
        {
            _job = job;
            _logger.Clear();
            _info.frameDeltas.Clear();
            _info.robotSteps.Clear();
            _info.peopleSteps.Clear();
            _info.chairPullsByReason.Clear();
            _info.chairPulls = 0;
            // 依頼 v9：道探しの時間と、寄せた回数・引けなかった回数は回ごとに数え直す
            _info.planMs.Clear();
            _info.mapSnapStarts = 0;
            _info.planFails = 0;
            _walkerSamples.Clear();
            _timeline.Clear();              // 時間帯ごとの記録（依頼 v10b の 2）
            _robotTrack.Clear();            // ロボット1台ずつの位置（2026-10-27）
            _tripSeq = 0;
            _runsFinished = 0;
            _runsAfterMeasure = 0;
            _stoppedEarly = false;
            _everyoneLeft = false;
            _outputsWritten = false;
            _condition = null;

            StoreLayout layout = store.layout;
            layout.aisleWidthIndex = job.aisleWidthIndex;

            // 停止点は条件の1つ（C1）。メニューが決めた値を、組み立てる前に入れる。
            // .asset のもとの値は覚えておき、ぜんぶ終わったら戻す（.asset は書き換えない）。
            if (!_deliveryOffsetSaved)
            {
                _savedDeliveryOffset = layout.deliveryOffsetFromAisleCenter;
                _deliveryOffsetSaved = true;
            }
            layout.deliveryOffsetFromAisleCenter = job.applyDeliveryOffset
                ? job.deliveryOffset : _savedDeliveryOffset;

            job.conditionId = ExperimentPlan.ConditionId(layout.AisleWidth, job.crowd, job.robots);

            // サイトの条件で回すとき（v5 3章）。
            // 建てる前に値を入れる（卓の数・来客のペース・時間の決まりが組み立てと来客に効く）。
            // 1つでも読めない値があれば、回さずに止める（Unity の .asset で埋めない）
            if (job.siteRun && !ApplySiteValues(job))
            {
                AbortRun();
                return;
            }

            store.Build();

            // 建てた卓の数が、試算の stores.tables と違ったら回さない（v5 3章）。
            // 店の形（JSON）が合わないときは Unity で埋めずに止める
            if (job.siteRun && !TablesMatchSite(job))
            {
                AbortRun();
                return;
            }

            baker.Bake();

            // 見る用の回は、床の塗り分けを Build Store と同じに作り直す。
            // 計測の回では作らない（見た目だけのものなので、速さを落とさない）
            if (viewMode) store.PaintFloor(baker, people.spec != null ? people.spec.personRadius : 0.2f);

            ops.spec = service;
            ops.robotSpec = robotSpec;
            ops.store = store;
            ops.Setup();

            people.service = service;
            people.ops = ops;
            people.Spawn(job.seed);

            groups.spec = service;
            groups.people = people.spec;
            groups.ops = ops;
            groups.store = store;
            groups.spawner = people;     // 人の動きに合わせて椅子を引く（B6）
            groups.bodyRoot = transform;

            jams.spec = people.spec;
            jams.ops = ops;
            jams.store = store;
            jams.Setup();

            // キッチンのモニター（測る回でも出す。切り替えは ExperimentPlan）
            if (monitor != null)
            {
                monitor.store = store;
                monitor.ops = ops;
                monitor.groups = groups;
                monitor.people = people;
                // 画面なしの実行（夜の実行ファイル）では出さない（依頼 v12 の 2 章）。
                // 吹き出しを描くのは Editor だけの ServiceSimLabels なので、
                // player では文を作っても誰も見ない。作るだけ無駄になる。
                // 文を作るか作らないかは、店の回し方・数字には1つも効かない
                monitor.show = plan.showKitchenMonitor && !Application.isBatchMode;
            }

            SpawnRobots(job.robots);

            // 始め方（v5 3章）。
            //   サイトの条件 … 空の店から、peak_min 分だけ来客し、全員が帰るまで回す
            //                  （max_extra_min で打ち切り）。慣らしは無し
            //   それ以外     … これまでどおり、8割埋まった店＋慣らし 15 分
            if (job.siteRun)
            {
                groups.seedWarmupGroups = false;
                _arrivalsEndS = service.peakMinutes * 60f;
                groups.arrivalsEndS = _arrivalsEndS;
                _hardEndS = _arrivalsEndS + service.maxExtraMin * 60f;
                _measureStartS = 0f;
                _endS = _hardEndS;
            }
            else
            {
                groups.seedWarmupGroups = true;
                groups.arrivalsEndS = 0f;
                _arrivalsEndS = 0f;
                _hardEndS = 0f;
                _measureStartS = plan.warmupMinutes * 60f;
                _endS = (plan.warmupMinutes + plan.measureMinutes) * 60f;
            }

            // 時間帯ごとの記録は、来客が始まるところから取る（依頼 v10b の 2）。
            // サイトの条件では _measureStartS が 0（空の店から）なので t_s = 0 が1行め
            _nextTimelineS = _measureStartS;
            _nextRobotTrackS = _measureStartS;

            groups.Setup(job.crowd, job.seed, people.Targets);
            ops.SetStaffCount(people.StaffSpawned);

            _timeOffset = Time.time;
            _lastLog = 0f;
            _jobRunning = true;

            ApplyDetCheck();

            // 2026-10-26（依頼 v13 の 依頼2）：1倍で回した回は **見る専用**。
            // 記録を書き出さない（出力フォルダも作らない）。
            // 1倍の切り替えは SimHud にあり、初期値は切なので測る回には効かない
            SimHud hud = FindObjectOfType<SimHud>();
            _viewOnly = hud != null && hud.RealTimeNow;
            if (_viewOnly)
                Debug.Log("[ServiceSim] 1倍で回します（SimHud）。" +
                          "この回は見る専用で、記録を書き出しません（出力フォルダも作りません）");

            // 2026-10-26（依頼 v13 の直し③）：向かい合いを場所で分けて数えるため、
            // 受け渡し口とロボットの待機場所を渡す
            if (store != null) _passBy.SetPlaces(store.PassPoint, store.RobotStandbyPoints);

            // 回し始めの行に、Unity だけの決まりの切り替えを必ず出す
            string how = "\n  人のよけ方: " +
                         (people.spec != null && people.spec.staffFirstYield ? "店員優先の版" : "今のよけ方") +
                         " / 同じ通路に2台を集めない: " +
                         (robotSpec != null && robotSpec.oneRobotPerAisle ? "入れた" : "入れない") +
                         " / 戻りの線: " +
                         (robotSpec != null && robotSpec.useReturnLine ? "使う" : "使わない") +
                         " / キッチンのモニター: " + (plan.showKitchenMonitor ? "出す" : "出さない") +
                         // 2026-10-26：道探しの枠を回し始めの行に必ず出す（隠れた設定を残さない）
                         " / 道探しの枠: " + _pathIterationsUsed + " 回/コマ" +
                         // 依頼 v13 の 依頼1：すれ違いの入切を、回し始めの行に必ず出す
                         " / すれ違い: " +
                         (robotSpec != null && robotSpec.passByEnabled
                            ? "入（速さ " + robotSpec.passBySpeedMps.ToString("0.##") + " m/秒・間 " +
                              robotSpec.passBySeparationM.ToString("0.##") + " m）"
                            : "切（cs:3 と同じ動き）");

            Debug.Log("[ServiceSim] 回し始め " + (_jobIndex + 1) + "/" + _jobCount +
                      "：" + job.conditionId + " 種" + job.seed + how +
                      (job.siteRun
                        ? "（サイトの条件。空の店から来客 " + service.peakMinutes +
                          " 分 ＋ 全員が帰るまで（上限 " + service.maxExtraMin + " 分）" +
                          "・スタッフ " + people.StaffSpawned + " 人・ロボット " + job.robots + " 台）"
                        : "（慣らし " + plan.warmupMinutes + " 分 ＋ 計測 " + plan.measureMinutes + " 分）"));
        }

        /// <summary>
        /// 合わない値があって回せないとき。書き出さずに Play を抜ける（v5 3章）。
        /// .asset に入れた値は、OnDisable と同じようにもとへ戻す
        /// </summary>
        void AbortRun()
        {
            _aborted = true;
            _running = false;
            _jobRunning = false;
            RestoreDeliveryOffset();
            RestoreSiteValues();
            Time.captureDeltaTime = 0f;
            Time.timeScale = 1f;
            Debug.LogError("[ServiceSim] 回しませんでした。Console の上の行を見てください");
#if UNITY_EDITOR
            UnityEditor.EditorApplication.isPlaying = false;
#endif
        }

        /// <summary>
        /// 建てた卓の数が、試算の stores.{店}.tables と合っているか（v5 3章）。
        /// 合わなければ、どちらかが古い。Unity で埋めずに止める
        /// </summary>
        bool TablesMatchSite(Job job)
        {
            JsonStoreLayout js = store.layout as JsonStoreLayout;
            if (js == null || simParams == null) return true;

            // ops.Setup() はこのあとなので、建てた数は店の形（JSON）から数えたものを見る
            int want = Mathf.RoundToInt(simParams.StoreValue(job.storeKey, "tables", -1f));
            int got = js.TableCount;
            if (want < 0 || want == got) return true;

            Debug.LogError("[ServiceSim] 卓の数が合いません（回しません）：試算 stores." + job.storeKey +
                           ".tables = " + want + " / 建てた店 = " + got +
                           "\n  どちらかが古いので、サイトから写し直してください");
            return false;
        }

        /// <summary>
        /// サイトの試算の値を、この回だけ ServiceSpec / PeopleSpec / RobotSpec に入れる（v5 3章）。
        /// 版の番号（店の形の meta.generation ↔ 試算の meta.layout_generation）が合わなければ回さない。
        /// 読めない値が1つでもあれば回さない（Unity の .asset で埋めない）。
        /// </summary>
        bool ApplySiteValues(Job job)
        {
            if (simParams == null)
            {
                Debug.LogError("[ServiceSim] SimParams が入っていません（ServiceSim/Setup を実行してください）");
                return false;
            }

            JsonStoreLayout js = store.layout as JsonStoreLayout;
            if (js == null)
            {
                Debug.LogError("[ServiceSim] サイトの条件で回すには、JSON から建てた店（JsonStoreLayout）が要ります。" +
                               "いまの店: " + store.layout);
                return false;
            }

            string storeKey = string.IsNullOrEmpty(job.storeKey) ? js.ParamsStoreKey : job.storeKey;
            job.storeKey = storeKey;

            if (!simParams.Load(js.ReadGeneration()))
            {
                string why = "";
                for (int i = 0; i < simParams.Problems.Count; i++) why += "\n  - " + simParams.Problems[i];
                Debug.LogError("[ServiceSim] 試算の値を読めません（回しません）" + why);
                return false;
            }

            _condition = job.conditionIndex >= 0 ? simParams.ConditionAt(job.conditionIndex) : null;
            if (_condition == null)
            {
                Debug.LogError("[ServiceSim] 条件 " + job.conditionIndex + " が unity_conditions にありません");
                return false;
            }
            if (_condition.store != storeKey)
            {
                Debug.LogError("[ServiceSim] 条件の店（" + _condition.store + "）と、建てる店（" +
                               storeKey + "）が違います");
                return false;
            }

            if (!_siteValues.Apply(simParams, storeKey, _condition, service, people.spec, robotSpec))
            {
                string why = "";
                for (int i = 0; i < _siteValues.Problems.Count; i++) why += "\n  - " + _siteValues.Problems[i];
                Debug.LogError("[ServiceSim] 試算の値が足りません（回しません）" + why);
                return false;
            }

            job.robots = _condition.robots;
            job.conditionId = _condition.Id;
            Debug.Log("[ServiceSim] " + _siteValues.Report());
            return true;
        }

        void SpawnRobots(int count)
        {
            // 往復の記録は、ServiceOps が片づけを終えてから書く（依頼 v8 の 2）。
            // ロボットの TripCompleted を直に受けると、呼ばれる順番によって
            // LastBatchOf が1つ前の往復の皿を返していた
            ops.RunRecorded -= OnRunDone;
            ops.RunRecorded += OnRunDone;

            ops.ClearRobots();
            for (int i = robotsRoot.childCount - 1; i >= 0; i--) Destroy(robotsRoot.GetChild(i).gameObject);
            _robots.Clear();

            // 置き始めは、1台に1つの待機場所へ（v5）。
            // 受け渡し口の前に重ねて置くと、はじめからふさぎ合う
            Vector3 pass = store.PassPoint;
            IList<Vector3> standby = store.RobotStandbyPoints;
            for (int i = 0; i < count; i++)
            {
                GameObject go = Instantiate(robotPrefab, robotsRoot);
                go.name = "ServingRobot" + (i + 1).ToString("00");
                ServingRobotController c = go.GetComponent<ServingRobotController>();
                if (c == null) { Destroy(go); continue; }
                c.robotId = i + 1;
                Vector3 start = standby.Count > 0
                    ? standby[i % standby.Count]
                    : pass + new Vector3(0f, 0f, -0.8f * i);
                c.PlaceAt(start, Quaternion.LookRotation(Vector3.back, Vector3.up));
                _robots.Add(c);
                ops.RegisterRobot(c);
            }
        }

        void Update()
        {
            if (!_running || !_jobRunning) return;

            float dt = Time.deltaTime;
            _wallSeconds = Time.realtimeSinceStartup - _wallStart;
            _info.frameDeltas.Add(dt);

            // 実時間の上限（段階B）。超えたら書き出して止める。
            // シミュレーションの動きは変えない。回しっぱなしで1回に居座らないための保険
            if (maxWallSecondsPerRun > 0f && _wallSeconds >= maxWallSecondsPerRun)
            {
                Debug.LogWarning("[ServiceSim] 実時間の上限を超えたので、この回を止めます: " +
                                 (_wallSeconds / 60f).ToString("0.0") + " 分 ≥ " +
                                 (maxWallSecondsPerRun / 60f).ToString("0.0") + " 分" +
                                 "\n  店の中 " + (ops != null ? (ops.NowS / 60f).ToString("0.0") : "?") + " 分" +
                                 "\n  書き出してから次へ進みます（台帳の status は stopped）");
                StopEarly();
                return;
            }

            float step = 0f;
            for (int i = 0; i < _robots.Count; i++)
                if (_robots[i] != null) step = Mathf.Max(step, _robots[i].LastStepM);
            _info.robotSteps.Add(step);

            // 人の1コマの移動量も記録する（B10）。通路を歩く人のうちいちばん大きいもの
            float peopleStep = 0f;
            IReadOnlyList<PersonBody> bodies = PersonBody.All;
            for (int i = 0; i < bodies.Count; i++)
            {
                PersonBody b = bodies[i];
                if (b == null || !b.walksInAisle) continue;
                if (b.LastStepM > peopleStep) peopleStep = b.LastStepM;
            }
            _info.peopleSteps.Add(peopleStep);

            ops.Advance(dt);
            groups.Tick(dt);
            jams.Tick(dt);
            // ロボットどうしのすれ違いを数える（依頼 v13 の 依頼1）。見るだけ
            _passBy.Tick(dt, ops.NowS, _robots, robotSpec);
            _simMinutes = ops.NowS / 60f;

            if (ops.NowS >= _measureStartS) _walkerSamples.Add(groups.WalkingBodies + people.StaffSpawned);

            // 1コマごとの指紋（依頼 v11 の 5 章。試しだけ）
            WriteDetFrame(_job.folder);

            // 時間帯ごとの記録（依頼 v10b の 2）。店の中の時間で 60 秒ごとに1行。
            // 1コマは 0.02 秒なので飛ぶことは無いが、万一飛んだときに
            // 同じ様子を何行も書かないよう、時刻だけ先へ進める
            if (ops.NowS >= _nextTimelineS)
            {
                _timeline.Sample(_nextTimelineS, groups, ops);
                do { _nextTimelineS += TimelineStepS; } while (ops.NowS >= _nextTimelineS);
            }

            // ロボット1台ずつの位置と状態（2026-10-27）。timeline と同じ作りで、
            // 刻みだけ 5 秒。店の中の時間で決めるので実時間の速さに左右されない
            if (ops.NowS >= _nextRobotTrackS)
            {
                _robotTrack.Sample(_nextRobotTrackS, _robots);
                do { _nextRobotTrackS += RobotTrackStepS; } while (ops.NowS >= _nextRobotTrackS);
            }

            if (_wallSeconds - _lastLog >= 60f)
            {
                _lastLog = _wallSeconds;

                // 「走り」はロボットが走り終わった回数、「記録」は trips.csv に書いた行数。
                // 走りがあるのに記録が増えないときは、記録の取りこぼしなのでその場で止める
                Debug.Log("[ServiceSim] とちゅう " + (_jobIndex + 1) + "/" + _jobCount + "：" +
                          _simMinutes.ToString("0.0") + " / " +
                          (_job.siteRun
                            ? "来客 " + (_arrivalsEndS / 60f).ToString("0") + " 分・上限 " +
                              (_hardEndS / 60f).ToString("0") + " 分"
                            : (_endS / 60f).ToString("0") + " 分") + "、" +
                          "組 " + groups.GroupsArrived + "（待ち " + groups.WaitingGroups +
                          "・帰った " + groups.GroupsDone + "）、" +
                          "走り " + _runsFinished + " 回・記録 " + _logger.RowCount + " 行、" +
                          "椅子 " + ops.chairPulls + " 回、" +
                          "あきらめ " + (ops.gaveUpCount + ops.warpedCount + ops.tableFreedCount) + " 回、" +
                          "立ち往生 " + ops.rescueCalls + " 回、" +
                          "実時間 " + (_wallSeconds / 60f).ToString("0.0") + " 分");

                CheckTripsAreRecorded();
            }

            if (ShouldFinish()) FinishJob();
        }

        /// <summary>
        /// 1条件を終える時。
        ///   サイトの条件（v5 3章）… 来客を受け付け終わって、全員が帰ったら終わり。
        ///                            帰らないまま max_extra_min を過ぎたら打ち切る
        ///   それ以外               … これまでどおり、慣らし＋計測の長さで終わり
        /// </summary>
        bool ShouldFinish()
        {
            if (!_job.siteRun) return ops.NowS >= _endS;

            if (ops.NowS >= _arrivalsEndS && !groups.AnyGroupInStore)
            {
                _everyoneLeft = true;
                return true;
            }
            return ops.NowS >= _hardEndS;
        }

        /// <summary>
        /// 走りがあるのに trips.csv の行が増えないときは、記録の取りこぼし。
        /// 気づかないまま何時間も回さないように、その場で止めて知らせる。
        /// 慣らしの間に始まった走りは数えないので、計測が始まってから見る。
        /// </summary>
        void CheckTripsAreRecorded()
        {
            if (ops.NowS < _measureStartS) return;
            if (_runsAfterMeasure <= 0) return;
            if (_logger.RowCount > 0) return;

            // 計測が始まってから走り終わった回数がこれを超えても記録が0なら、おかしい
            if (_runsAfterMeasure < TripRecordAlarmRuns) return;

            Debug.LogError("[ServiceSim] 走りが " + _runsAfterMeasure +
                           " 回あるのに trips.csv の行が 0 です。記録を取りこぼしています。" +
                           "\n  止めました。ExperimentRunner.OnRunDone と " +
                           "ServingRobotController.RunStartS を確かめてください" +
                           "\n  店の中 " + (ops.NowS / 60f).ToString("0.0") + " 分 / 計測の始まり " +
                           (_measureStartS / 60f).ToString("0.0") + " 分");
            StopEarly();
        }

        /// <summary>途中で止める（書き出しはする）</summary>
        void StopEarly()
        {
            _stoppedEarly = true;
            FinishJob();
            AllDone();
        }

        void OnRunDone(ServingRobotController r)
        {
            _runsFinished++;
            if (ops.NowS >= _measureStartS) _runsAfterMeasure++;

            // 走り始めた時刻はロボットが持っている（ServingRobotController.RunStartS）。
            // ここで覚えておくと、走り終わってすぐ次の仕事が入ったとき、
            // 「空いている」状態を一度も見ないまま時刻が古いままになり、全部の走りを取りこぼす。
            float startS = r.RunStartS >= 0f ? r.RunStartS : ops.NowS - r.TotalS;
            if (startS < _measureStartS) return;

            IList<DishOrder> batch = ops.LastBatchOf(r);
            _tripSeq++;

            TripLogger.Row row = new TripLogger.Row();
            row.conditionId = _job.conditionId;
            row.aisleWidthM = store.layout.AisleWidth;
            row.crowd = ExperimentPlan.CrowdName(_job.crowd);
            row.robots = _job.robots;
            row.seed = _job.seed;
            row.robotId = r.robotId;
            row.tripId = _tripSeq;
            row.tableId = r.TableIds.Count > 0 ? r.TableIds[0] : 0;
            row.distanceM = r.DistanceM;
            row.totalS = r.TotalS;
            row.stoppedS = r.StoppedS;
            row.nStops = r.StopCount;
            row.stopDurationsS = ToArray(r.StopDurationsS);
            row.stuck = r.Stuck;
            row.timeout = r.TimedOut;
            row.waitAisleS = r.WaitAisleS;
            row.maxDecelMps2 = r.MaxDecelMps2;
            row.minClearanceM = r.MinClearanceM;
            row.metRobot = r.MetRobot;

            int[] ids = new int[r.TableIds.Count];
            for (int i = 0; i < ids.Length; i++) ids[i] = r.TableIds[i];
            row.tableIds = ids;
            row.dishes = batch.Count;

            float[] ready = new float[batch.Count], arrived = new float[batch.Count];
            int undelivered = 0;
            for (int i = 0; i < batch.Count; i++)
            {
                ready[i] = batch[i].readyS;
                arrived[i] = batch[i].arrivedS;
                if (batch[i].arrivedS < 0f) undelivered++;
            }
            row.dishReadyS = ready;
            row.dishArrivedS = arrived;
            row.undelivered = undelivered;

            // ---- 後ろに足した列（依頼 v8 の 2。今ある列の並びは変えない）----
            row.runStartS = startS;

            int[] dishIds = new int[batch.Count];
            for (int i = 0; i < batch.Count; i++) dishIds[i] = batch[i].id;
            row.dishIds = dishIds;

            float[] stopArrived = new float[r.StopArrivedS.Count];
            for (int i = 0; i < stopArrived.Length; i++) stopArrived[i] = r.StopArrivedS[i];
            row.stopArrivedS = stopArrived;
            row.resetTo = r.ResetTo;        // 打ち切りで置き直した先（依頼 v8b の A）

            // ---- 往復ごとの測り（v5 3章・設計書2章の4つ）----
            // 実際に走れた速さは、止まっていた分を除いた時間で出す（止まり込みの平均は別物）
            float movingS = r.TotalS - r.StoppedS;
            row.speedMps = movingS > 0.01f ? r.DistanceM / movingS : 0f;
            row.stuckCount = r.StuckCount;
            row.stuckWaitS = r.StuckWaitS;
            row.viaNarrow = r.LegWasNarrow;
            SplitStopsByAisle(r, ref row);

            // ---- 依頼 v9 の 5 章：地図の上で自分で道を引く ----
            row.tripEndS = r.TripEndS >= 0f ? r.TripEndS : ops.NowS;
            row.pathMode = r.ActivePathMode.ToString();
            row.replans = r.Replans;
            row.detours = r.Detours;
            row.robotYields = r.RobotYields;
            row.siteRouteM = r.SiteRouteM;
            row.detourExtraM = r.DistanceM - r.SiteRouteM;
            row.narrowUsed = r.LegWasNarrow;
            row.planMsMax = r.PlanMsMax;

            _info.planMs.Add(r.PlanMsMax);
            _info.mapSnapStarts += r.MapSnapStarts;
            _info.planFails += r.PlanFails;

            _logger.Add(row);

            AddStopRows(r, _tripSeq);
        }

        void AddStopRows(ServingRobotController robot, int tripId)
        {
            float normal = NormalWidthM();
            IReadOnlyList<ServingRobotController.StopEvent> events = robot.StopEvents;
            for (int i = 0; i < events.Count; i++)
            {
                ServingRobotController.StopEvent e = events[i];
                TripLogger.StopRow s = new TripLogger.StopRow();
                s.conditionId = _job.conditionId;
                s.seed = _job.seed;
                s.robotId = robot.robotId;
                s.tripId = tripId;
                s.tS = e.startTimeS - _timeOffset;
                s.x = e.position.x;
                s.z = e.position.z;
                s.aisle = AisleNameAt(e.position);
                s.inNarrow = InNarrowAisle(e.position, normal);
                s.cause = TripLogger.CauseLabel(e.cause);
                s.causeDetail = e.causeDetail;
                s.pathBlocked = e.pathBlocked;
                s.durationS = e.durationS;
                s.resolved = TripLogger.ResolveLabel(e.resolve);
                s.clearanceLeftCm = e.clearanceLeftM * 100f;
                s.clearanceRightCm = e.clearanceRightM * 100f;
                s.minGapCm = Mathf.Min(s.clearanceLeftCm, s.clearanceRightCm);

                // 何で動けなかったかの内訳（B7）
                s.blockedS = e.blockedS;
                s.noPathS = e.noPathS;
                s.yieldS = e.yieldS;
                s.clearWaitS = e.clearWaitS;
                s.replans = e.replans;

                // 道を引いたときの中身（調べるためだけ）
                s.solveFrom = Xz(e.solveFrom);
                s.solveTo = Xz(e.solveTo);
                s.solveStatus = string.IsNullOrEmpty(e.solveStatus) ? "なし" : e.solveStatus;
                s.solveCorners = e.solveCorners;
                s.offMeshM = e.offMeshM;

                _logger.AddStop(s);
            }
        }

        /// <summary>
        /// 止まりを、狭い通路の中と外に分けて数える（v5 3章。⑦の読み分け）。
        /// 「狭い」はサイトの決まりのまま、幅が meta.settings.normal_width（1.0 m）未満の通路。
        /// ③の通路は 1.2 m なので、ぜんぶ「外」になる。
        /// </summary>
        void SplitStopsByAisle(ServingRobotController robot, ref TripLogger.Row row)
        {
            float normal = NormalWidthM();
            IReadOnlyList<ServingRobotController.StopEvent> events = robot.StopEvents;
            for (int i = 0; i < events.Count; i++)
            {
                if (InNarrowAisle(events[i].position, normal))
                {
                    row.stopsInNarrow++;
                    row.stoppedInNarrowS += events[i].durationS;
                }
                else
                {
                    row.stopsOutNarrow++;
                    row.stoppedOutNarrowS += events[i].durationS;
                }
            }
        }

        /// <summary>サイトの「ふだん走らせる道の幅」（m）。JSON に無ければ 0（＝ぜんぶ「外」）</summary>
        float NormalWidthM()
        {
            JsonStoreLayout js = store != null ? store.layout as JsonStoreLayout : null;
            return js != null ? js.NormalWidth : 0f;
        }

        bool InNarrowAisle(Vector3 p, float normalWidth)
        {
            if (normalWidth <= 0f) return false;
            IList<StoreBuilder.AisleInfo> aisles = store.Aisles;
            for (int i = 0; i < aisles.Count; i++)
            {
                if (aisles[i].width >= normalWidth) continue;
                Bounds b = aisles[i].zone;
                if (p.x < b.min.x || p.x > b.max.x || p.z < b.min.z || p.z > b.max.z) continue;
                return true;
            }
            return false;
        }

        /// <summary>点を "x;z" の1つの欄にする（stops.csv の solve_from / solve_to）</summary>
        static string Xz(Vector3 p)
        {
            return p.x.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture) + ";" +
                   p.z.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture);
        }

        string AisleNameAt(Vector3 p)
        {
            string wide = "";
            IList<StoreBuilder.AisleInfo> aisles = store.Aisles;
            for (int i = 0; i < aisles.Count; i++)
            {
                Bounds b = aisles[i].zone;
                if (p.x < b.min.x || p.x > b.max.x || p.z < b.min.z || p.z > b.max.z) continue;
                if (aisles[i].narrow) return aisles[i].name;
                if (wide.Length == 0) wide = aisles[i].name;
            }
            return wide;
        }

        static float[] ToArray(IReadOnlyList<float> list)
        {
            if (list == null || list.Count == 0) return new float[0];
            float[] a = new float[list.Count];
            for (int i = 0; i < a.Length; i++) a[i] = list[i];
            return a;
        }

        // ---------------- 書き出して次へ ----------------

        void FinishJob()
        {
            _jobRunning = false;
            CloseDetWriter();        // 依頼 v11 の 5 章（試しだけ）
            AddGroupRows();
            WriteOutputs(_job.folder, _stoppedEarly);

            _jobIndex++;
            if (_jobIndex < _queue.Count) { StartJob(_queue[_jobIndex]); return; }
            AllDone();
        }

        /// <summary>
        /// 1条件ぶんの記録を書き出す（v5 3章で切り出した）。
        /// 途中で Play を止めたときも、ここまでの分を同じ形で書き出せるようにするため、
        /// 置き場所と「途中で止めたか」を渡す形にした。
        /// 書き出しの中で、オブジェクトを探したり消したりしないこと
        /// （Play を抜ける最中にも呼ばれるため）。
        /// </summary>
        void WriteOutputs(string dir, bool stoppedEarly)
        {
            _outputsWritten = true;

            // 1倍の回は見る専用（依頼 v13 の 依頼2）。フォルダも作らない
            if (_viewOnly)
            {
                Debug.Log("[ServiceSim] 見る専用の回なので、記録は書き出しません: " + dir);
                return;
            }
            try
            {
                Directory.CreateDirectory(dir);

                TripLogger.SimLike sim = new TripLogger.SimLike();
                sim.dishWaitMin.AddRange(ops.dishWaitMin);
                sim.seatWaitMin.AddRange(ops.seatWaitMin);
                sim.turnMin.AddRange(ops.turnMin);
                CollectPayWait(sim);                // pay_wait_mean（依頼 v10 の 2）
                sim.coldMin = service.coldMin;
                sim.robotDishes = ops.robotDishes;
                sim.staffDishes = ops.staffDishes;
                sim.requeuedDishes = ops.requeuedDishes;
                sim.dishesToStaffAfterTimeout = ops.dishesToStaffAfterTimeout;
                sim.groups = groups.GroupsDone;
                sim.jamCount = jams.JamCount;
                sim.jamTotalS = jams.TotalJamSeconds;
                sim.robotBlockedJams = jams.RobotBlockedJamCount;   // B9
                sim.chairPulls = ops.chairPulls;                     // B6
                sim.robotContacts = ops.Contacts.Count;              // 人がロボットをよける
                sim.robotYieldWaits = ops.robotYieldWaits;
                sim.robotYieldWaitSeconds = ops.robotYieldWaitSeconds;
                sim.robotYieldDetours = ops.robotYieldDetours;
                // ロボットどうしのすれ違い（依頼 v13 の 依頼1）。切の回でも数える
                sim.passCount = _passBy.PassCount;
                sim.passSlowS = _passBy.PassSlowS;
                sim.faceStopCount = _passBy.FaceStopCount;
                sim.faceStopMaxS = _passBy.FaceStopMaxS;
                // 2026-10-26（直し③）：場所で分けた数
                sim.faceStopAtPassCount = _passBy.FaceStopAtPassCount;
                sim.faceStopAtPassMaxS = _passBy.FaceStopAtPassMaxS;
                sim.faceStopAtStandbyCount = _passBy.FaceStopAtStandbyCount;
                sim.faceStopAtStandbyMaxS = _passBy.FaceStopAtStandbyMaxS;
                sim.faceStopInAisleCount = _passBy.FaceStopInAisleCount;
                sim.faceStopInAisleMaxS = _passBy.FaceStopInAisleMaxS;
                sim.faceStopOverlapCount = _passBy.FaceStopOverlapCount;
                sim.registerCrowdMax = ops.registerCrowdMax;         // レジの前の人数
                sim.registerCrowdMean = ops.RegisterCrowdMean;
                sim.registerPayCount = ops.registerPayCount;
                sim.registerPayNotAtBackCount = ops.registerPayNotAtBackCount;
                sim.registerPayOffSpotCount = ops.registerPayOffSpotCount;
                sim.registerPayMeanOffsetM = ops.RegisterPayMeanOffsetM;
                sim.registerPayMaxOffsetM = ops.registerPayMaxOffsetM;
                sim.registerBackIntruderCount = ops.registerBackIntruderCount;
                sim.registerBackIntruderSeconds = ops.registerBackIntruderSeconds;
                sim.staffTime.Clear();
                foreach (KeyValuePair<string, float> kv in ops.StaffTime) sim.staffTime.Add(kv);
                sim.dishParts.Clear();
                sim.dishParts.AddRange(ops.DishPartsList);
                // サイトの試算 v2.7：2分を過ぎて人が拾った皿と、その回の robot_first_wait_s
                sim.lateStaffDishes = ops.lateStaffDishes;
                sim.robotFirstWaitS = service.robotFirstWaitSec;
                // 譲る順番 v6：譲りの記録と、角の範囲（RobotSpec の1か所から写す）
                sim.yields.Clear();
                sim.yields.AddRange(ops.Yields);
                // 依頼 v9：角の箱（cornerMin/Max）は SiteRoutes のときだけ働く。
                // MapPlanner では範囲を入れないので、summary.json の corner_stops は 0 になる
                if (robotSpec != null && !robotSpec.UseMapPlanner)
                {
                    sim.cornerMinX = robotSpec.cornerMinX;
                    sim.cornerMaxX = robotSpec.cornerMaxX;
                    sim.cornerMinZ = robotSpec.cornerMinZ;
                    sim.cornerMaxZ = robotSpec.cornerMaxZ;
                }
                sim.jobWaitMean.Clear();
                foreach (KeyValuePair<string, float> kv in ops.JobWaitSum)
                {
                    int n;
                    ops.JobWaitCount.TryGetValue(kv.Key, out n);
                    sim.jobWaitMean.Add(new KeyValuePair<string, float>(kv.Key, n > 0 ? kv.Value / n : 0f));
                }
                // 依頼 v10 の 2：板で待った最大と、長びいた数（片付け・会計を見る）
                sim.jobWaitMax.Clear();
                foreach (KeyValuePair<string, float> kv in ops.JobWaitMax) sim.jobWaitMax.Add(kv);
                sim.jobWaitOver.Clear();
                foreach (KeyValuePair<string, int> kv in ops.JobWaitOverCount) sim.jobWaitOver.Add(kv);
                sim.jobWaitOverSec = ops.JobWaitOverSec;
                sim.drinkBarQueued = ops.drinkBarQueued;             // B4
                sim.drinkBarGaveUp = ops.drinkBarGaveUp;
                sim.drinkBarWaitMin.Clear();
                sim.drinkBarWaitMin.AddRange(ops.drinkBarWaitMin);
                sim.gaveUp = ops.gaveUpCount;                        // B1
                sim.warped = ops.warpedCount;
                sim.tableFreed = ops.tableFreedCount;
                sim.walkersMean = MeanInt(_walkerSamples);
                sim.measuredMinutes = _job.siteRun ? ops.NowS / 60f : plan.measureMinutes;

                // ---- サイトと同じ名前で出す値（v5 3章・meta.metrics）----
                sim.entryWaitMin.AddRange(ops.entryWaitMin);
                sim.fullWaitMin.AddRange(ops.fullWaitMin);
                sim.blockedWaitMin.AddRange(ops.blockedWaitMin);
                sim.guideWaitMin.AddRange(ops.guideWaitMin);
                sim.staffWaitMin.AddRange(ops.staffWaitMin);
                sim.walkKmPerStaff = ops.WalkKmPerStaff;
                sim.staffUtil = StaffUtil();
                sim.robotUtil = ops.RobotUtil;
                sim.aisleDeferrals = ops.aisleDeferrals;
                sim.robotStuckDetours = ops.robotStuckDetours;
                sim.staffFirstYield = people.spec != null && people.spec.staffFirstYield;
                sim.rescues = ops.rescueCalls;
                sim.rescuesDone = ops.rescueDone;
                sim.rescueWaitS = ops.rescueWaitSeconds;
                sim.pauses = _logger.TotalStopCount();
                sim.siteRun = _job.siteRun;
                sim.siteConditionId = _job.conditionId;
                sim.siteResult.Clear();
                if (_condition != null)
                    foreach (KeyValuePair<string, float> kv in _condition.siteResult)
                        sim.siteResult.Add(kv);
                sim.arrivalsMinutes = _job.siteRun ? _arrivalsEndS / 60f : 0f;
                sim.totalMinutes = ops.NowS / 60f;
                sim.everyoneLeft = _everyoneLeft;
                FillTimelineCheck(sim);         // 時間帯ごとの記録の確かめ（依頼 v10b の 3 A）
                _logger.SetSimLike(sim);

                _logger.WriteTripsCsv(Path.Combine(dir, "trips.csv"));
                _logger.WriteStopsCsv(Path.Combine(dir, "stops.csv"));
                _logger.WriteGroupsCsv(Path.Combine(dir, "groups.csv"));
                TripLogger.WriteJamsCsv(Path.Combine(dir, "people_jams.csv"),
                                        _job.conditionId, _job.seed, jams.Rows);
                TripLogger.WriteGiveUpsCsv(Path.Combine(dir, "give_ups.csv"),
                                        _job.conditionId, _job.seed, ops.GiveUps);
                // dishes.csv は皿ごとの記録（誰が運んだか・受け渡し口で待った秒数・
                // 2分を過ぎて人が拾ったか）。戻した1回ずつは requeues.csv に分けた
                TripLogger.WriteDishesCsv(Path.Combine(dir, "dishes.csv"),
                                        _job.conditionId, _job.seed, ops.DishRows);
                TripLogger.WriteRequeuesCsv(Path.Combine(dir, "requeues.csv"),
                                        _job.conditionId, _job.seed, ops.Requeues);
                TripLogger.WriteContactsCsv(Path.Combine(dir, "contacts.csv"),
                                        _job.conditionId, _job.seed, ops.Contacts);
                // 誰が誰に譲ったか（譲る順番 v6）
                TripLogger.WriteYieldsCsv(Path.Combine(dir, "yields.csv"),
                                        _job.conditionId, _job.seed, ops.Yields);
                // ロボットどうしのすれ違い1回ずつ（依頼 v13 の 依頼1）
                TripLogger.WritePassesCsv(Path.Combine(dir, "passes.csv"),
                                        _job.conditionId, _job.seed, _passBy.Rows);
                // 時間帯ごとの記録（依頼 v10b の 2）。段階Bと Site Run で同じ作り
                _timeline.Write(Path.Combine(dir, "timeline.csv"),
                                        _job.conditionId, _job.seed);
                // ロボット1台ずつの位置と状態（2026-10-27）。常に書く
                _robotTrack.Write(Path.Combine(dir, "robot_track.csv"),
                                        _job.conditionId, _job.seed);
                _logger.WriteSummaryJson(Path.Combine(dir, "summary.json"), plan.runName);

                _info.runLabel = runLabel;
                _info.conditionId = _job.conditionId;
                _info.timeMode = timeMode.ToString();
                _info.tripsRequested = 0;
                _info.tripsDone = _logger.RowCount;
                _info.robots = _job.robots;
                _info.seed = _job.seed;
                _info.aisleWidthM = store.layout.AisleWidth;
                _info.deliveryOffsetM = store.layout.deliveryOffsetFromAisleCenter;
                _info.deliveryOffsetUsedM = store.layout.DeliveryOffset();
                _info.crowd = ExperimentPlan.CrowdName(_job.crowd);
                _info.simSeconds = ops.NowS;
                _info.wallSeconds = _wallSeconds;
                _info.timeScale = timeMode == TimeMode.FastTimeScale ? plan.trialTimeScaleFast : 1f;
                _info.captureDeltaTime = Time.captureDeltaTime;

                // 椅子を引いた回数（B6）
                _info.chairPulls = ops.chairPulls;
                _info.chairPullsByReason.Clear();
                foreach (KeyValuePair<string, int> kv in ops.ChairPullsByReason)
                    _info.chairPullsByReason.Add(kv);
                _info.stoppedEarly = stoppedEarly;
                _info.oneRobotPerAisle = robotSpec != null && robotSpec.oneRobotPerAisle;
                _info.yieldOrderEnabled = robotSpec != null && robotSpec.yieldOrderEnabled;
                _info.unityCs = unityCs;
                _info.pendingNote = pendingNote;
                // 依頼 v12 の 2 章：誰が回したか
                _info.runner = string.IsNullOrEmpty(runnerName) ? "editor" : runnerName;
                _info.buildId = buildId != null ? buildId : "";
                _info.night = night != null ? night : "";
                _info.parallel = parallel;
                _info.parallelActual = parallelActual;
                // 依頼 v11 の 5 章（試しだけ）
                _info.detCheck = DetCheckNote();
                _info.pathIterationsPerFrame = _pathIterationsUsed;
                _info.sensorMaxHits = FrontSensor.MaxSeen;
                _info.sensorOverflows = FrontSensor.Overflows;
                _info.staffFirstYield = people.spec != null && people.spec.staffFirstYield;
                _info.storeKind = store.layout.name;

                // 依頼 v9 の 5 章：道の決め方と、地図の版・道の引き方の数字
                if (robotSpec != null)
                {
                    _info.narrowCellCost = robotSpec.narrowCellCost;
                    _info.centerWeight = robotSpec.centerWeight;
                    _info.preferClearM = robotSpec.preferClearM;
                    _info.detourAfterSec = robotSpec.detourAfterSec;
                    _info.pathMode = robotSpec.pathMode.ToString();
                    // 依頼 v13 の 依頼1：すれ違いの入切と3つの値
                    _info.passByEnabled = robotSpec.passByEnabled;
                    _info.passByDistanceM = robotSpec.passByDistanceM;
                    _info.passBySpeedMps = robotSpec.passBySpeedMps;
                    _info.passBySeparationM = robotSpec.passBySeparationM;
                }
                // 実際に使った決め方（地図が無ければ SiteRoutes に落ちる）は台から読む
                IList<ServingRobotController> rs = ops != null ? ops.Robots : null;
                if (rs != null && rs.Count > 0 && rs[0] != null)
                    _info.pathMode = rs[0].ActivePathMode.ToString();
                JsonStoreLayout jsMap = store.layout as JsonStoreLayout;
                _info.mapGeneration = jsMap != null ? jsMap.MapGeneration : "";
                TripLogger.WriteRunInfoJson(Path.Combine(dir, "run_info.json"), _info);

                TripLogger.WriteConditionsJson(Path.Combine(dir, "conditions.json"),
                    plan.runName, store.layout, robotSpec, people.spec, plan,
                    plan.BuildConditions(store.layout));

                _runsDone++;
                Debug.Log("[ServiceSim] 書き出し " + (_jobIndex + 1) + "/" + _jobCount + "：" + _job.conditionId +
                          " 種" + _job.seed +
                          "\n  走り " + _runsFinished + " 回 / 記録 " + _logger.RowCount + " 行 / 止まり " +
                          _logger.StopCount + " 回 / 組 " + _logger.GroupRowCount +
                          "\n  椅子を引いた " + ops.chairPulls + " 回 / 人どうしの詰まり " + jams.JamCount +
                          " 回（うちロボットが近くで止まっていた " + jams.RobotBlockedJamCount + " 回）" +
                          "\n  会計 " + ops.registerPayCount + " 回（店員側にいなかった " +
                          ops.registerPayNotAtBackCount + " 回・立つ点から離れた " +
                          ops.registerPayOffSpotCount + " 回・いちばん離れた " +
                          ops.registerPayMaxOffsetM.ToString("0.00") + " m・会計でないのに店員側にいた " +
                          ops.registerBackIntruderCount + " 回）/ レジの前の人数 最大 " +
                          ops.registerCrowdMax + "・平均 " + ops.RegisterCrowdMean.ToString("0.0") +
                          "\n  人がロボットを待った " + ops.robotYieldWaits + " 回・" +
                          ops.robotYieldWaitSeconds.ToString("0") + " 秒 / 回り道 " +
                          ops.robotYieldDetours + " 回 / 近づきすぎ " + ops.Contacts.Count + " 回" +
                          "\n  人のよけ方: " +
                          (people.spec != null && people.spec.staffFirstYield ? "店員優先の版" : "いまの版") +
                          "（店員が止まったロボットを回り道した " + ops.robotStuckDetours + " 回）" +
                          "\n  同じ通路を避けて後に回した " + ops.aisleDeferrals + " 回" +
                          "（同じ通路に2台を集めない: " +
                          (robotSpec != null && robotSpec.oneRobotPerAisle ? "入れた" : "入れない") + "）" +
                          "\n  戻りの線を使えなかった " + ReturnLineFallbacks() + " 回" +
                          "（使えないときは受け渡し口の前へ戻る）" +
                          // 依頼 v13 の 依頼1：すれ違いの数（切の回でも出る）
                          "\n  すれ違い " + _passBy.PassCount + " 回・減速していた " +
                          _passBy.PassSlowS.ToString("0") + " 秒 / 向かい合って止まった " +
                          _passBy.FaceStopCount + " 回（最長 " +
                          _passBy.FaceStopMaxS.ToString("0.0") + " 秒）" +
                          "（すれ違いの走り方: " +
                          (robotSpec != null && robotSpec.passByEnabled ? "入" : "切") + "）" +
                          "\n  立ち往生で呼んだ " + ops.rescueCalls + " 回（動かした " + ops.rescueDone +
                          " 回・待った " + ops.rescueWaitSeconds.ToString("0") + " 秒）" +
                          "\n  入店待ち " + Mean(ops.entryWaitMin).ToString("0.00") + " 分（満席 " +
                          Mean(ops.fullWaitMin).ToString("0.00") + "・片付け待ち " +
                          Mean(ops.blockedWaitMin).ToString("0.00") + "・案内待ち " +
                          Mean(ops.guideWaitMin).ToString("0.00") + "）/ スタッフの歩き " +
                          ops.WalkKmPerStaff.ToString("0.00") + " km・手がふさがり " +
                          (StaffUtil() * 100f).ToString("0") + "%・ロボット " +
                          (ops.RobotUtil * 100f).ToString("0") + "%" +
                          "\n  あきらめた " + ops.gaveUpCount + " / 移した " + ops.warpedCount +
                          " / 卓を空けた " + ops.tableFreedCount +
                          " / 戻した皿 " + ops.requeuedDishes + "（届いた " + ops.robotDishes + "）" +
                          "\n  " + TripLogger.FrameStatsText(_info) +
                          "\n  " + dir);
            }
            catch (System.Exception e)
            {
                Debug.LogError("[ServiceSim] 書き出しに失敗: " + dir + "\n" + e);
            }
        }

        // ---------------- ずれの切り分け（依頼 v11 の 5 章。試しだけ）----------------

        /// <summary>
        /// この回で実際に入った道探しの枠（run_info.json に出す。2026-10-26）。
        /// 入れたあとに Unity から読み直した値なので、「入れたつもり」ではなく本当の値
        /// </summary>
        int _pathIterationsUsed;
        System.IO.StreamWriter _detWriter;
        int _detFrame;

        /// <summary>
        /// 試しの切り替えを入れる。ぜんぶ切なら cs:2 と同じ動きになる。
        /// ★ 調べるためだけの仕掛け。check-in しない
        /// </summary>
        void ApplyDetCheck()
        {
            FrontSensor.MaxSeen = 0;
            FrontSensor.Overflows = 0;
            _detFrame = 0;
            // すれ違いの数も回ごとに数え直す（依頼 v13。前の回の分を持ち越さない）
            _passBy.Reset();
            if (plan == null) return;

            // 毎コマの道探しの枠を、**回すたびに必ず** ExperimentPlan の値で入れ直す
            // （2026-10-26）。前は「試しのときだけ入れて、終わりに戻す」形だったが、
            // 戻し損ねるとエディタを閉じるまで残り、次の回の人の動きが変わってしまう
            // （⑦の18回で待ちが短く出た疑い）。入れたあとに読み直して記録する
            int want = plan.pathIterationsPerFrame > 0 ? plan.pathIterationsPerFrame : 100;
            UnityEngine.AI.NavMesh.pathfindingIterationsPerFrame = want;
            _pathIterationsUsed = UnityEngine.AI.NavMesh.pathfindingIterationsPerFrame;
            if (_pathIterationsUsed != want)
                Debug.LogWarning("[ServiceSim] 道探しの枠を入れられませんでした: 入れた " + want +
                                 " / 実際 " + _pathIterationsUsed);

            // 試し2：人の道を同期で引く
            PeopleNav.SyncPaths = plan.detCheckSyncPeoplePaths;

            // 試し4：人の局所回避（RVO）を切る。要るときだけ
            if (plan.detCheckNoAvoidance)
            {
                IReadOnlyList<StaffAgent> st = StaffAgent.All;
                for (int i = 0; i < st.Count; i++)
                    if (st[i] != null) SetNoAvoidance(st[i].gameObject);
            }

            if (plan.detCheckSyncPeoplePaths ||
                plan.detCheckNoAvoidance || plan.detCheckFrameFingerprint)
                Debug.Log("[ServiceSim] ずれの切り分け（試しの切り替え）が入っています: " + DetCheckNote());
        }

        static void SetNoAvoidance(GameObject go)
        {
            UnityEngine.AI.NavMeshAgent a =
                go != null ? go.GetComponent<UnityEngine.AI.NavMeshAgent>() : null;
            if (a != null)
                a.obstacleAvoidanceType = UnityEngine.AI.ObstacleAvoidanceType.NoObstacleAvoidance;
        }

        /// <summary>入れた切り替えを、人が読める1行にする（run_info とログに出す）</summary>
        string DetCheckNote()
        {
            if (plan == null) return "";
            if (!plan.detCheckSyncPeoplePaths &&
                !plan.detCheckNoAvoidance && !plan.detCheckFrameFingerprint) return "";
            return "sync_people_paths=" + (plan.detCheckSyncPeoplePaths ? "入" : "切") +
                   " / no_avoidance=" + (plan.detCheckNoAvoidance ? "入" : "切") +
                   " / frame_fingerprint=" + (plan.detCheckFrameFingerprint ? "入" : "切");
        }

        /// <summary>試しの切り替えを元に戻す（.asset と Unity の設定を残さない）</summary>
        void RestoreDetCheck()
        {
            // 道探しの枠は戻さない。回し始めに毎回入れ直すので、
            // 「戻し損ねて次の回に残る」という形そのものを無くした（2026-10-26）
            PeopleNav.SyncPaths = false;
            CloseDetWriter();
        }

        /// <summary>
        /// 1コマごとの指紋を det_frames.csv に1行書く。
        /// 人とロボットの位置をまとめた数なので、2回ぶんを並べれば
        /// **どのコマで初めてずれたか** が1コマ単位で分かる。
        /// 位置は 0.1 mm まで整数にして、小数の書き方で差が出ないようにする
        /// </summary>
        void WriteDetFrame(string dir)
        {
            if (plan == null || !plan.detCheckFrameFingerprint) return;
            if (_viewOnly) return;      // 見る専用の回はフォルダも作らない（依頼 v13 の 依頼2）
            if (_detWriter == null)
            {
                try
                {
                    Directory.CreateDirectory(dir);
                    _detWriter = new System.IO.StreamWriter(
                        Path.Combine(dir, "det_frames.csv"), false,
                        new System.Text.UTF8Encoding(false));
                    _detWriter.Write("frame,t_s,people,robots,people_sum,robot_sum,path_pending\n");
                }
                catch (System.Exception e)
                {
                    Debug.LogWarning("[ServiceSim] det_frames.csv を開けません: " + e.Message);
                    plan.detCheckFrameFingerprint = false;
                    return;
                }
            }

            long pSum = 0; int pCount = 0, pending = 0;
            IReadOnlyList<PersonBody> bodies = PersonBody.All;
            for (int i = 0; i < bodies.Count; i++)
            {
                PersonBody b = bodies[i];
                if (b == null) continue;
                Vector3 p = b.transform.position;
                pSum += (long)Mathf.RoundToInt(p.x * 10000f) * 31 +
                        (long)Mathf.RoundToInt(p.z * 10000f) * 17;
                pCount++;
                UnityEngine.AI.NavMeshAgent a = b.GetComponent<UnityEngine.AI.NavMeshAgent>();
                if (a != null && a.isOnNavMesh && a.pathPending) pending++;
            }

            long rSum = 0; int rCount = 0;
            for (int i = 0; i < _robots.Count; i++)
            {
                ServingRobotController r = _robots[i];
                if (r == null) continue;
                Vector3 p = r.transform.position;
                rSum += (long)Mathf.RoundToInt(p.x * 10000f) * 31 +
                        (long)Mathf.RoundToInt(p.z * 10000f) * 17;
                rCount++;
            }

            _detWriter.Write(_detFrame.ToString(System.Globalization.CultureInfo.InvariantCulture)); _detWriter.Write(',');
            _detWriter.Write(ops.NowS.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture)); _detWriter.Write(',');
            _detWriter.Write(pCount.ToString(System.Globalization.CultureInfo.InvariantCulture)); _detWriter.Write(',');
            _detWriter.Write(rCount.ToString(System.Globalization.CultureInfo.InvariantCulture)); _detWriter.Write(',');
            _detWriter.Write(pSum.ToString(System.Globalization.CultureInfo.InvariantCulture)); _detWriter.Write(',');
            _detWriter.Write(rSum.ToString(System.Globalization.CultureInfo.InvariantCulture)); _detWriter.Write(',');
            _detWriter.Write(pending.ToString(System.Globalization.CultureInfo.InvariantCulture)); _detWriter.Write('\n');
            _detFrame++;
        }

        void CloseDetWriter()
        {
            if (_detWriter == null) return;
            try { _detWriter.Flush(); _detWriter.Dispose(); }
            catch (System.Exception) { }
            _detWriter = null;
        }

        /// <summary>
        /// 時間帯ごとの記録の確かめ（依頼 v10b の 3 A）。
        ///
        /// どの行でも
        ///   seated_groups ＋ tables_free ＋ tables_dirty_untaken ＋ tables_clearing ＝ 卓の数
        ///   staff_idle ＋ …（仕事の種類 6 つ）＝ スタッフの人数
        /// になっているかを数えて、summary.json の timeline_check に出す。
        /// 合わない行があれば Console にも出す（夜にまとめて回したあとでも分かるように）
        /// </summary>
        void FillTimelineCheck(TripLogger.SimLike sim)
        {
            int tables = ops != null ? ops.Tables.Count : 0;
            int staff = people != null ? people.StaffSpawned : 0;

            int badTables, badStaff;
            string why;
            bool ok = _timeline.CheckSums(tables, staff, out badTables, out badStaff, out why);

            sim.timelineRows = _timeline.RowCount;
            sim.timelineTables = tables;
            sim.timelineStaff = staff;
            sim.timelineBadTableRows = badTables;
            sim.timelineBadStaffRows = badStaff;
            sim.timelineFirstBad = why;

            if (!ok)
                Debug.LogWarning("[ServiceSim] timeline.csv の合計が合いません（" +
                                 "卓 " + badTables + " 行・スタッフ " + badStaff + " 行 / " +
                                 _timeline.RowCount + " 行）\n  いちばん早い行: " + why);
        }

        /// <summary>
        /// pay_wait_mean：食べ終えてから会計が始まるまで（分。依頼 v10 の 2）。
        ///
        /// 組ごとに（会計が終わった時刻 − 食べ終えた時刻 − pay_s）。
        /// groups.csv の pay_s 列（会計が終わった時刻）と eat_end_s から出せる値と同じで、
        /// 引く pay_s は ServiceSpec.paySec（会計そのものにかかる 90 秒）。
        ///
        /// 入れない組:
        ///   ・逃がし弁で卓を空けられた組（会計が途中で切れている）
        ///   ・会計が終わっていない組（payS が -1。打ち切りで店に残った組）
        ///   ・慣らしで最初から座っていた組（ほかの site_metrics と同じ扱い）
        ///
        /// サイトの試算は「レジの順番とスタッフの手が空くのを待った時間」だけだが、
        /// Unity は会計に立つ人がレジまで歩く時間と、スタッフがレジへ歩く時間も入るので、
        /// 試算より少し長く出る
        /// </summary>
        void CollectPayWait(TripLogger.SimLike sim)
        {
            if (groups == null || service == null) return;
            IList<GroupManager.Group> gs = groups.Groups;
            for (int i = 0; i < gs.Count; i++)
            {
                GroupManager.Group g = gs[i];
                if (g.warmup || g.tableForceFreed) continue;
                if (g.payS < 0f || g.eatEndS < 0f) continue;
                sim.payWaitMin.Add(Mathf.Max(0f, g.payS - g.eatEndS - service.paySec) / 60f);

                // 照らし合わせ用：食べ終えた時刻ではなく、会計に入った時刻（pay_start_s）から数えた分。
                // Unity は eat_end_s（食べ始めに決めた食べ終わりの時刻）のあとも、
                // 遅れて届いた皿やドリンクバー・トイレの人を待ってから会計に入るので、
                // その待ちが pay_wait_mean に混ざる。混ざった分の大きさが見えるように両方出す
                if (g.payStartS >= 0f)
                    sim.payWaitFromStartMin.Add(
                        Mathf.Max(0f, g.payS - g.payStartS - service.paySec) / 60f);
            }
        }

        void AddGroupRows()
        {
            IList<GroupManager.Group> gs = groups.Groups;
            for (int i = 0; i < gs.Count; i++)
            {
                GroupManager.Group g = gs[i];
                TripLogger.GroupRow r = new TripLogger.GroupRow();
                r.conditionId = _job.conditionId;
                r.seed = _job.seed;
                r.groupId = g.id;
                r.size = g.size;
                r.tableId = g.table != null ? g.table.tableId : 0;
                r.tableSeats = g.table != null ? g.table.seatCount : 0;   // 2人席／4人席
                r.arriveS = g.arriveS;
                r.guideStartS = g.guideStartS;
                r.seatedS = g.seatedS;
                r.eatStartS = g.eatStartS;      // 食べ始めた時刻（依頼 v8 の 2）
                r.eatEndS = g.eatEndS;
                r.payStartS = g.payStartS;
                r.payS = g.payS;
                r.leftS = g.leftS;
                r.waitedMin = g.seatedS >= 0f && g.arriveS >= 0f ? (g.seatedS - g.arriveS) / 60f : -1f;
                r.warmup = g.warmup;
                r.state = g.state.ToString();
                _logger.AddGroup(r);
            }
        }

        /// <summary>
        /// staff_util：スタッフの手がふさがっていた割合（v5 3章）。
        /// ops.StaffTime が仕事ごとの秒を持っているので、「待機」以外の割合で出す。
        /// </summary>
        float StaffUtil()
        {
            float total = 0f, idle = 0f;
            foreach (KeyValuePair<string, float> kv in ops.StaffTime)
            {
                total += kv.Value;
                if (kv.Key == "待機") idle += kv.Value;
            }
            return total > 0.01f ? (total - idle) / total : 0f;
        }

        /// <summary>
        /// 戻りの線が使えず、今までどおり受け渡し口の前へ戻った回数（v5）。
        /// 0 でなければ、線が通れる床に乗っていないか、出ていく線・ドリンクバーの列に近すぎる
        /// </summary>
        int ReturnLineFallbacks()
        {
            int n = 0;
            for (int i = 0; i < _robots.Count; i++)
                if (_robots[i] != null) n += _robots[i].ReturnLineFallbacks;
            return n;
        }

        static float Mean(List<float> v)
        {
            if (v == null || v.Count == 0) return 0f;
            float s = 0f;
            for (int i = 0; i < v.Count; i++) s += v[i];
            return s / v.Count;
        }

        static float MeanInt(List<int> v)
        {
            if (v == null || v.Count == 0) return 0f;
            long s = 0;
            for (int i = 0; i < v.Count; i++) s += v[i];
            return (float)s / v.Count;
        }

        /// <summary>
        /// メニューが決めた停止点を、.asset のもとの値へ戻す（C1）。
        /// 書き出しが終わったあとに呼ぶ。.asset を書き換えたままにしないため。
        /// </summary>
        void RestoreDeliveryOffset()
        {
            if (!_deliveryOffsetSaved) return;
            _deliveryOffsetSaved = false;
            if (store != null && store.layout != null)
                store.layout.deliveryOffsetFromAisleCenter = _savedDeliveryOffset;
        }

        /// <summary>
        /// サイトの試算から入れた値を、.asset のもとの値へ戻す（v5 3章）。
        /// 停止点（C1）と同じ扱い。.asset を書き換えたままにしないため
        /// </summary>
        void RestoreSiteValues()
        {
            _siteValues.Restore();
        }

        void AllDone()
        {
            _running = false;
            RestoreDeliveryOffset();
            RestoreSiteValues();
            Time.captureDeltaTime = 0f;
            Time.timeScale = 1f;
            Debug.Log("[ServiceSim] ぜんぶ終わりました。" + _runsDone + " / " + _jobCount + " 条件、" +
                      "実時間 " + (_wallSeconds / 60f).ToString("0.0") + " 分");
#if UNITY_EDITOR
            UnityEditor.EditorApplication.isPlaying = false;
#endif
        }

        /// <summary>
        /// 途中で Play を止めたとき、そこまでの記録を書き出す（v5 3章）。
        ///
        /// 回している最中に止めると、これまでは何も残らなかった。
        /// 詰まった回でも give_ups.csv と summary.json を見られるように、同じ形で書き出す。
        ///   ・置き場所は、ふだんのフォルダ名に _stopped を付けたもの
        ///   ・run_info.json の stopped_early が true、summary.json の
        ///     site_run.everyone_left は false のまま
        /// 書き出しに失敗しても Play を抜けられるよう、まるごと try/catch で包む。
        /// Play を抜ける最中なので、ここから新しくオブジェクトを探したり消したりしない。
        /// </summary>
        void WriteOnStop()
        {
            if (!_running || !_jobRunning || _outputsWritten || _job == null) return;

            try
            {
                _stoppedEarly = true;
                _jobRunning = false;
                AddGroupRows();
                WriteOutputs(_job.folder + "_stopped", true);
                Debug.LogWarning("[ServiceSim] 途中で止めました。ここまでの記録を書き出しました" +
                                 "（計測には使えません）\n  店の中 " +
                                 (ops != null ? (ops.NowS / 60f).ToString("0.0") : "?") + " 分\n  " +
                                 _job.folder + "_stopped");
            }
            catch (System.Exception e)
            {
                Debug.LogError("[ServiceSim] 途中で止めたときの書き出しに失敗しました\n" + e);
            }
        }

        void OnApplicationQuit()
        {
            WriteOnStop();
        }

        void OnDisable()
        {
            // 回している最中に止めたときは、ここまでの記録を書き出す（v5 3章）
            WriteOnStop();

            // 途中で Play を抜けたときも、停止点とサイトの値を .asset のもとの値へ戻す
            RestoreDeliveryOffset();
            RestoreSiteValues();
            RestoreDetCheck();       // 依頼 v11 の 5 章（試しだけ）
            Time.captureDeltaTime = 0f;
            if (_running) Time.timeScale = 1f;
        }
    }
}
```

### Assets/ServiceSim/Experiment/TripLogger.cs

```csharp
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>
    /// trips.csv・conditions.json・summary.json を書く。
    /// この script は実験だけを扱う。店・ロボット・人は動かさない。
    /// 1往復ぶんの値は ExperimentRunner が Row に詰めて渡す。
    /// </summary>
    public class TripLogger
    {
        /// <summary>trips.csv の1行</summary>
        public struct Row
        {
            public string conditionId;
            public float aisleWidthM;
            public string crowd;
            public int robots;
            public int seed;
            public int robotId;
            public int tripId;
            public int tableId;
            public float distanceM;
            public float totalS;
            public float stoppedS;
            public int nStops;
            public float[] stopDurationsS;
            public bool stuck;
            public bool timeout;
            public float waitAisleS;
            public float maxDecelMps2;
            public float minClearanceM;
            public bool metRobot;
            // v2：1行＝ロボットの1走り
            public int[] tableIds;          // 回った卓
            public int dishes;              // 載せた料理の数
            public float[] dishReadyS;      // 各料理ができた時刻
            public float[] dishArrivedS;    // 各料理が届いた時刻（-1 は届かなかった）
            public int undelivered;         // 届けられず受け渡し口に戻した数

            // ---- 往復ごとの測り（v5 3章・設計書2章の4つ）----
            /// <summary>その往復で実際に走れた速さ（m/秒）＝ distance_m /（total_s − stopped_s）</summary>
            public float speedMps;

            /// <summary>動けなくなってスタッフを呼んだ回数と、スタッフを待った秒数</summary>
            public int stuckCount;
            public float stuckWaitS;

            /// <summary>その往復でサイトの道順が 0.8 m の道を使ったか（routes の use が narrow）</summary>
            public bool viaNarrow;

            /// <summary>止まりの回数と秒数を、狭い通路の中と外に分けたもの（⑦の読み分け用）</summary>
            public int stopsInNarrow, stopsOutNarrow;
            public float stoppedInNarrowS, stoppedOutNarrowS;

            // ---- 後ろに足した列（依頼 v8 の 2。今ある列の並びは変えない）----
            /// <summary>その往復を始めた時刻（店の中, 秒。ほかの _s 列と同じ基準）</summary>
            public float runStartS;

            /// <summary>その往復で載せた皿の番号（dishes.csv の dish_id と同じ）</summary>
            public int[] dishIds;

            /// <summary>止まる所ごとに着いた時刻（着かなかった所は -1）</summary>
            public float[] stopArrivedS;

            /// <summary>
            /// 打ち切りで置き直した先（依頼 v8b の A）。pass / standby1 / standby2 …。
            /// 打ち切りでない往復は空
            /// </summary>
            public string resetTo;

            // ---- 後ろに足した列（依頼 v9 の 5 章。今ある列の並びは変えない）----

            /// <summary>往復が実際に終わった店の中の時刻（卓での25秒を含む, 秒）。total_s はそのまま</summary>
            public float tripEndS;

            /// <summary>道の決め方（MapPlanner / SiteRoutes）</summary>
            public string pathMode;

            /// <summary>道を引き直した回数（回り込み・よけを含む）</summary>
            public int replans;

            /// <summary>3 章の回り込みで道を変えた回数</summary>
            public int detours;

            /// <summary>
            /// 4 章でよけた回数。よける場所へ動いた回数 ＋
            /// 相手の道をよけた道に引き直した回数
            /// </summary>
            public int robotYields;

            /// <summary>その往復でサイトの道順をたどったときの長さ（routes の length の和, m）</summary>
            public float siteRouteM;

            /// <summary>走った距離 − site_route_m（m）</summary>
            public float detourExtraM;

            /// <summary>狭い道（free_class 1 のマス）を通ったか</summary>
            public bool narrowUsed;

            /// <summary>その往復で 1 回の道探しにかかった最大の時間（ミリ秒）</summary>
            public float planMsMax;
        }

        /// <summary>groups.csv の1行（1組ぶん）</summary>
        public struct GroupRow
        {
            public string conditionId;
            public int seed;
            public int groupId;
            public int size;
            public int tableId;

            /// <summary>座った卓の大きさ（2 なら2人席、4 なら4人席。0 は座れなかった）</summary>
            public int tableSeats;
            public float arriveS, guideStartS, seatedS, eatEndS, payS, leftS;

            /// <summary>食べ始めた時刻（秒）。食べ始めなかった組は -1（依頼 v8 の 2）</summary>
            public float eatStartS;

            /// <summary>会計を始めた時刻（食べ終わって会計待ちに入った時刻, 秒）</summary>
            public float payStartS;
            public float waitedMin;
            public bool warmup;
            public string state;
        }

        /// <summary>stops.csv の1行（止まり1回ぶん）</summary>
        public struct StopRow
        {
            public string conditionId;
            public int seed;
            public int robotId;
            public int tripId;
            public float tS;            // 店の中の時刻
            public float x, z;
            public string aisle;        // 通路の名前

            /// <summary>
            /// サイトの決まりで「狭い通路」（幅が meta.settings.normal_width 未満）の中で
            /// 止まったか（v5 3章。⑦の読み分け。③は 1.2 m なのでいつも 0）
            /// </summary>
            public bool inNarrow;
            public string cause;
            public string causeDetail;
            public bool pathBlocked;    // 行き先への道が引けていなかったか
            public float durationS;
            public string resolved;
            public float clearanceLeftCm;
            public float clearanceRightCm;
            public float minGapCm;

            // 止まっている間、何で動けなかったかの内訳（秒）。B7 を調べるため
            public float blockedS;
            public float noPathS;
            public float yieldS;
            public float clearWaitS;
            public int replans;

            // 道を引いたときの中身（調べるためだけ。動きは変えない）
            /// <summary>寄せたあとの出発点・行き先（"x;z"）</summary>
            public string solveFrom, solveTo;
            /// <summary>CalculatePath の結果と、引けた角の数</summary>
            public string solveStatus;
            public int solveCorners;
            /// <summary>ロボットの位置から、いちばん近い焼いた面までの距離（m）。-1 は見つからない</summary>
            public float offMeshM;
        }

        /// <summary>
        /// 止まった原因の名前。観察シート（serving-robot-observation.html）の
        /// 「止まった原因」の選択肢にそろえる。Unity にしかない原因は足した。
        /// </summary>
        public static string CauseLabel(ServingRobotController.StopCause cause)
        {
            switch (cause)
            {
                case ServingRobotController.StopCause.Guest: return "お客さんとすれ違った";
                case ServingRobotController.StopCause.Staff: return "スタッフとすれ違った";
                case ServingRobotController.StopCause.PersonStanding: return "人が立ち止まっていた";
                case ServingRobotController.StopCause.Chair: return "椅子・荷物・ベビーカー";
                case ServingRobotController.StopCause.NoPath: return "通路の幅が足りない";
                case ServingRobotController.StopCause.OtherRobot: return "もう1台のロボット";
                case ServingRobotController.StopCause.AisleQueue: return "通路の順番待ち";
                default: return "分からない";
            }
        }

        /// <summary>どう動き出したかの名前</summary>
        public static string ResolveLabel(ServingRobotController.StopResolve r)
        {
            switch (r)
            {
                case ServingRobotController.StopResolve.SelfResume: return "自分で動き出した";
                case ServingRobotController.StopResolve.Replanned: return "経路を引き直した";
                case ServingRobotController.StopResolve.Stuck: return "動けなくなった";
                // 依頼 v9 の 5 章
                case ServingRobotController.StopResolve.Detoured: return "回り込んで動き出した";
                case ServingRobotController.StopResolve.YieldedToRobot: return "よける場所へ動いた";
                default: return "入口で待った";
            }
        }

        readonly List<Row> _rows = new List<Row>();
        readonly List<StopRow> _stops = new List<StopRow>();

        public int StopCount { get { return _stops.Count; } }

        /// <summary>
        /// 一時停止の回数の合計（pauses）。
        /// 往復ごとの n_stops（自分で動き出した止まり）を足す。
        /// 通路の入口で待った分と、動けなくなった分は入らない
        /// </summary>
        public int TotalStopCount()
        {
            int n = 0;
            for (int i = 0; i < _rows.Count; i++) n += _rows[i].nStops;
            return n;
        }
        public IReadOnlyList<StopRow> Stops { get { return _stops; } }

        public void AddStop(StopRow row) { _stops.Add(row); }

        public int RowCount { get { return _rows.Count; } }
        public IReadOnlyList<Row> Rows { get { return _rows; } }

        public void Add(Row row)
        {
            _rows.Add(row);
        }

        public void Clear()
        {
            _rows.Clear();
            _stops.Clear();
            _groups.Clear();
        }

        // ---------------- trips.csv ----------------

        public const string Header =
            "condition_id,aisle_width_m,crowd,robots,seed,robot_id,trip_id,table_id," +
            "distance_m,total_s,stopped_s,n_stops,stop_durations_s,stuck,timeout,wait_aisle_s," +
            "max_decel_mps2,min_clearance_m,met_robot," +
            "table_ids,dishes,dish_ready_s,dish_arrived_s,undelivered," +
            // v5 3章：往復ごとの測り（設計書2章の4つ）。⑦は狭い通路の中と外を分ける
            "speed_mps,stuck_count,stuck_wait_s,via_narrow," +
            "stops_in_narrow,stops_out_narrow,stopped_in_narrow_s,stopped_out_narrow_s," +
            // 依頼 v8 の 2：後ろに足した列
            "run_start_s,dish_ids,stop_arrived_s," +
            // 依頼 v8b の A：打ち切りで置き直した先
            "reset_to," +
            // 依頼 v9 の 5 章：地図の上で自分で道を引く
            "trip_end_s,path_mode,replans,detours,robot_yields," +
            "site_route_m,detour_extra_m,narrow_used,plan_ms_max";

        public void WriteTripsCsv(string path)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(Header).Append('\n');
            for (int i = 0; i < _rows.Count; i++) AppendRow(sb, _rows[i]);
            File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        static void AppendRow(StringBuilder sb, Row r)
        {
            sb.Append(r.conditionId).Append(',')
              .Append(F(r.aisleWidthM)).Append(',')
              .Append(r.crowd).Append(',')
              .Append(r.robots).Append(',')
              .Append(r.seed).Append(',')
              .Append(r.robotId).Append(',')
              .Append(r.tripId).Append(',')
              .Append(r.tableId).Append(',')
              .Append(F(r.distanceM)).Append(',')
              .Append(F(r.totalS)).Append(',')
              .Append(F(r.stoppedS)).Append(',')
              .Append(r.nStops).Append(',')
              .Append(Join(r.stopDurationsS)).Append(',')
              .Append(r.stuck ? 1 : 0).Append(',')
              .Append(r.timeout ? 1 : 0).Append(',')
              .Append(F(r.waitAisleS)).Append(',')
              .Append(F(r.maxDecelMps2)).Append(',')
              .Append(F(r.minClearanceM)).Append(',')
              .Append(r.metRobot ? 1 : 0).Append(',')
              .Append(JoinInt(r.tableIds)).Append(',')
              .Append(r.dishes).Append(',')
              .Append(Join(r.dishReadyS)).Append(',')
              .Append(Join(r.dishArrivedS)).Append(',')
              .Append(r.undelivered).Append(',')
              .Append(F(r.speedMps)).Append(',')
              .Append(r.stuckCount).Append(',')
              .Append(F(r.stuckWaitS)).Append(',')
              .Append(r.viaNarrow ? 1 : 0).Append(',')
              .Append(r.stopsInNarrow).Append(',')
              .Append(r.stopsOutNarrow).Append(',')
              .Append(F(r.stoppedInNarrowS)).Append(',')
              .Append(F(r.stoppedOutNarrowS)).Append(',')
              .Append(F(r.runStartS)).Append(',')
              .Append(JoinInt(r.dishIds)).Append(',')
              .Append(Join(r.stopArrivedS)).Append(',')
              .Append(r.resetTo ?? "").Append(',')
              // 依頼 v9 の 5 章
              .Append(F(r.tripEndS)).Append(',')
              .Append(r.pathMode ?? "").Append(',')
              .Append(r.replans).Append(',')
              .Append(r.detours).Append(',')
              .Append(r.robotYields).Append(',')
              .Append(F(r.siteRouteM)).Append(',')
              .Append(F(r.detourExtraM)).Append(',')
              .Append(r.narrowUsed ? 1 : 0).Append(',')
              .Append(F(r.planMsMax))
              .Append('\n');
        }

        static string Join(float[] values)
        {
            if (values == null || values.Length == 0) return "";
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < values.Length; i++)
            {
                if (i > 0) sb.Append(';');
                sb.Append(F(values[i]));
            }
            return sb.ToString();
        }

        static string JoinInt(int[] values)
        {
            if (values == null || values.Length == 0) return "";
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < values.Length; i++)
            {
                if (i > 0) sb.Append(';');
                sb.Append(values[i]);
            }
            return sb.ToString();
        }

        static string F(float v)
        {
            return v.ToString("0.####", CultureInfo.InvariantCulture);
        }

        // ---------------- groups.csv ----------------

        readonly List<GroupRow> _groups = new List<GroupRow>();
        public int GroupRowCount { get { return _groups.Count; } }
        public void AddGroup(GroupRow r) { _groups.Add(r); }

        /// <summary>table_seats は座った卓の大きさ（2＝2人席・4＝4人席・0＝座れなかった）</summary>
        public const string GroupHeader =
            "condition_id,seed,group_id,size,table_id,table_seats,arrive_s,guide_start_s,seated_s," +
            "eat_end_s,pay_start_s,pay_s,left_s,waited_min,warmup,state," +
            // 依頼 v8 の 2：後ろに足した列
            "eat_start_s";

        public void WriteGroupsCsv(string path)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(GroupHeader).Append('\n');
            for (int i = 0; i < _groups.Count; i++)
            {
                GroupRow r = _groups[i];
                sb.Append(r.conditionId).Append(',').Append(r.seed).Append(',')
                  .Append(r.groupId).Append(',').Append(r.size).Append(',').Append(r.tableId).Append(',')
                  .Append(r.tableSeats).Append(',')
                  .Append(F(r.arriveS)).Append(',').Append(F(r.guideStartS)).Append(',')
                  .Append(F(r.seatedS)).Append(',').Append(F(r.eatEndS)).Append(',')
                  .Append(F(r.payStartS)).Append(',')
                  .Append(F(r.payS)).Append(',').Append(F(r.leftS)).Append(',')
                  .Append(F(r.waitedMin)).Append(',').Append(r.warmup ? 1 : 0).Append(',')
                  .Append(r.state).Append(',')
                  .Append(F(r.eatStartS)).Append('\n');
            }
            File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        // ---------------- people_jams.csv ----------------

        public const string JamHeader =
            "condition_id,seed,t_s,x,z,aisle,people,duration_s,resolved,robot_blocked,robot_id";

        public static void WriteJamsCsv(string path, string conditionId, int seed,
                                        IReadOnlyList<JamWatcher.JamRow> rows)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(JamHeader).Append('\n');
            if (rows != null)
            {
                for (int i = 0; i < rows.Count; i++)
                {
                    JamWatcher.JamRow r = rows[i];
                    sb.Append(conditionId).Append(',').Append(seed).Append(',')
                      .Append(F(r.tS)).Append(',').Append(F(r.x)).Append(',').Append(F(r.z)).Append(',')
                      .Append(r.aisle).Append(',').Append(r.people).Append(',')
                      .Append(F(r.durationS)).Append(',').Append(r.resolved).Append(',')
                      .Append(r.robotBlocked ? 1 : 0).Append(',').Append(r.robotId).Append('\n');
                }
            }
            File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        // ---------------- give_ups.csv（B1）----------------

        public const string GiveUpHeader =
            "condition_id,seed,t_s,who,job,table_id,to_x,to_z,x,z,aisle,waited_s,action";

        /// <summary>あきらめた・移した・卓を空けたことを1回1行で書く</summary>
        public static void WriteGiveUpsCsv(string path, string conditionId, int seed,
                                           IReadOnlyList<GiveUpRow> rows)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(GiveUpHeader).Append('\n');
            if (rows != null)
            {
                for (int i = 0; i < rows.Count; i++)
                {
                    GiveUpRow r = rows[i];
                    sb.Append(conditionId).Append(',').Append(seed).Append(',')
                      .Append(F(r.tS)).Append(',').Append(r.who).Append(',').Append(r.job).Append(',')
                      .Append(r.tableId).Append(',')
                      .Append(F(r.toX)).Append(',').Append(F(r.toZ)).Append(',')
                      .Append(F(r.x)).Append(',').Append(F(r.z)).Append(',')
                      .Append(r.aisle).Append(',').Append(F(r.waitedS)).Append(',')
                      .Append(r.action).Append('\n');
                }
            }
            File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        // ---------------- contacts.csv（人がロボットに近づきすぎた）----------------

        public const string ContactHeader =
            "condition_id,seed,t_s,x,z,aisle,place,who,careless,robot_id,gap_cm";

        /// <summary>人がロボットの外周の近くまで来た1回を1行で書く</summary>
        public static void WriteContactsCsv(string path, string conditionId, int seed,
                                            IReadOnlyList<ContactRow> rows)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(ContactHeader).Append('\n');
            if (rows != null)
            {
                for (int i = 0; i < rows.Count; i++)
                {
                    ContactRow r = rows[i];
                    sb.Append(conditionId).Append(',').Append(seed).Append(',')
                      .Append(F(r.tS)).Append(',').Append(F(r.x)).Append(',').Append(F(r.z)).Append(',')
                      .Append(r.aisle).Append(',').Append(r.place).Append(',')
                      .Append(r.who).Append(',')
                      .Append(r.careless ? 1 : 0).Append(',').Append(r.robotId).Append(',')
                      .Append(F(r.gapM * 100f)).Append('\n');
                }
            }
            File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        // ---------------- 往復のまとめ（段階Bの台帳が写す・2026-10-23）----------------

        /// <summary>止まっていた秒数の合計（stops.csv の duration_s の合計）</summary>
        float StopSecondsTotal()
        {
            float s = 0f;
            for (int i = 0; i < _stops.Count; i++) s += _stops[i].durationS;
            return s;
        }

        /// <summary>
        /// 往復のまとめを summary.json に出す（段階Bの台帳が写す）。
        ///
        /// robot_speed_mps は、サイトの common.robot_speed（role=unity_measures
        /// ＝「Unity で測ってサイトに戻すための仮の値」）と同じ意味で Unity が測った値。
        ///   走れた速さ ＝ 走った距離の合計 ÷（かかった秒数 − 止まっていた秒数）
        /// pause_s_mean は、サイトの common.pause_s（同じ role）と同じ意味で、
        /// 自分で動き出した止まり1回の長さの平均（秒）。
        ///
        /// サイトの pause_rate・stuck_rate は「hop_m（3 m）進むごとの起きやすさ」だが、
        /// hop_m は role=site_only（試算の中だけの設定）で Unity は読まない決まりなので、
        /// ここでは出さない。かわりに、割り出すのに要る素の数を出す
        /// （pauses ＝ stops.count、stuck_count、distance_m_total）。
        /// </summary>
        void AppendTripSummary(StringBuilder sb)
        {
            int trips = _rows.Count;
            int timeouts = 0, stuckTrips = 0;
            float distance = 0f, total = 0f, stopped = 0f;

            for (int i = 0; i < _rows.Count; i++)
            {
                Row r = _rows[i];
                if (r.timeout) timeouts++;
                if (r.stuck) stuckTrips++;
                distance += r.distanceM;
                total += r.totalS;
                stopped += r.stoppedS;
            }

            float moving = Mathf.Max(0f, total - stopped);

            // 自分で動き出した止まりの長さの平均（pause_s と同じ意味）
            float pauseSum = 0f;
            int pauseN = 0;
            for (int i = 0; i < _rows.Count; i++)
            {
                float[] d = _rows[i].stopDurationsS;
                if (d == null) continue;
                for (int k = 0; k < d.Length; k++) { pauseSum += d[k]; pauseN++; }
            }

            sb.Append("  \"trips\": {");
            sb.Append("\"trips\": ").Append(trips).Append(", ");
            sb.Append("\"timeouts\": ").Append(timeouts).Append(", ");
            sb.Append("\"stuck_trips\": ").Append(stuckTrips).Append(", ");
            sb.Append("\"distance_m_total\": ").Append(F(distance)).Append(", ");
            sb.Append("\"total_s\": ").Append(F(total)).Append(", ");
            sb.Append("\"stopped_s\": ").Append(F(stopped)).Append(", ");
            sb.Append("\"robot_speed_mps\": ")
              .Append(F(moving > 0.001f ? distance / moving : 0f)).Append(", ");
            sb.Append("\"pause_s_mean\": ")
              .Append(F(pauseN > 0 ? pauseSum / pauseN : 0f)).Append(", ");
            sb.Append("\"pause_count\": ").Append(pauseN).Append(", ");
            sb.Append("\"pause_rate_note\": ")
              .Append(Str("hop_m は role=site_only で Unity は読まないため出さない。" +
                          "割り出すときは stops.count ÷（distance_m_total ÷ hop_m）")).Append(", ");
            sb.Append("\"stuck_rate_note\": ")
              .Append(Str("同じ理由で出さない。stops.stuck_count ÷（distance_m_total ÷ hop_m）"));
            sb.Append("},\n");
        }

        // ---------------- summary に出す譲り合いと角の止まり（2026-10-23）----------------

        /// <summary>
        /// 譲った回数を summary に出す。譲った側の種類ごと（ロボット・スタッフ・客）と、
        /// そのうち相手の道から横へどけた回数・角で起きた回数・待った秒数の合計。
        /// </summary>
        void AppendYields(StringBuilder sb)
        {
            int all = 0, robot = 0, staff = 0, guest = 0, stepped = 0, inCorner = 0, passed = 0;
            float waited = 0f, staffWaited = 0f;

            if (_sim != null)
            {
                for (int i = 0; i < _sim.yields.Count; i++)
                {
                    YieldRow y = _sim.yields[i];
                    all++;
                    if (y.rank == RobotSpec.RankRobot) robot++;
                    else if (y.rank == RobotSpec.RankStaff) { staff++; staffWaited += y.waitedS; }
                    else guest++;
                    if (y.steppedAside) stepped++;
                    if (y.inCorner) inCorner++;
                    if (y.passed) passed++;
                    waited += y.waitedS;
                }
            }

            sb.Append("\"yields\": ").Append(all).Append(", ");
            sb.Append("\"yields_by_robot\": ").Append(robot).Append(", ");
            sb.Append("\"yields_by_staff\": ").Append(staff).Append(", ");
            sb.Append("\"yields_by_guest\": ").Append(guest).Append(", ");
            sb.Append("\"yields_stepped_aside\": ").Append(stepped).Append(", ");
            sb.Append("\"yields_in_corner\": ").Append(inCorner).Append(", ");
            sb.Append("\"yields_passed\": ").Append(passed).Append(", ");
            sb.Append("\"yield_wait_s\": ").Append(F(waited)).Append(", ");
            sb.Append("\"yield_wait_mean_s\": ").Append(F(all > 0 ? waited / all : 0f)).Append(", ");

            // スタッフが譲った回数・合計秒数と、スタッフの時間に占める割合（v6b）。
            // 分母はスタッフの時間の合計（staff_time_s の全部。待機もふくむ
            // ＝ スタッフの人数 × 回した秒数）
            float staffTotal = 0f;
            if (_sim != null)
                for (int i = 0; i < _sim.staffTime.Count; i++) staffTotal += _sim.staffTime[i].Value;

            sb.Append("\"staff_yields\": ").Append(staff).Append(", ");
            sb.Append("\"staff_yield_wait_s\": ").Append(F(staffWaited)).Append(", ");
            sb.Append("\"staff_yield_mean_s\": ")
              .Append(F(staff > 0 ? staffWaited / staff : 0f)).Append(", ");
            sb.Append("\"staff_yield_share_of_staff_time\": ")
              .Append(F(staffTotal > 0.001f ? staffWaited / staffTotal : 0f)).Append(", ");
            sb.Append("\"staff_time_total_s\": ").Append(F(staffTotal)).Append(", ");
        }

        /// <summary>
        /// 受け渡し口と客席をつなぐ角（RobotSpec の範囲）での、ロボットの止まりの
        /// 回数・秒数と、止まり全体に対する割合を summary に出す。
        /// 1311・1536 の「止まった時間の約3分の1がこの角」と比べるため
        /// </summary>
        void AppendCornerStops(StringBuilder sb)
        {
            int n = 0, allN = 0;
            float secs = 0f, allSecs = 0f;

            bool haveRange = _sim != null && _sim.cornerMaxX > _sim.cornerMinX;
            for (int i = 0; i < _stops.Count; i++)
            {
                StopRow r = _stops[i];
                allN++;
                allSecs += r.durationS;
                if (!haveRange) continue;
                if (r.x < _sim.cornerMinX || r.x > _sim.cornerMaxX) continue;
                if (r.z < _sim.cornerMinZ || r.z > _sim.cornerMaxZ) continue;
                n++;
                secs += r.durationS;
            }

            sb.Append("\"corner_stops\": ").Append(n).Append(", ");
            sb.Append("\"corner_stop_s\": ").Append(F(secs)).Append(", ");
            sb.Append("\"corner_stop_count_share\": ")
              .Append(F(allN > 0 ? (float)n / allN : 0f)).Append(", ");
            sb.Append("\"corner_stop_time_share\": ")
              .Append(F(allSecs > 0.001f ? secs / allSecs : 0f)).Append(", ");
            sb.Append("\"corner_range\": \"x ")
              .Append(F(_sim != null ? _sim.cornerMinX : 0f)).Append("-")
              .Append(F(_sim != null ? _sim.cornerMaxX : 0f)).Append(" / z ")
              .Append(F(_sim != null ? _sim.cornerMinZ : 0f)).Append("-")
              .Append(F(_sim != null ? _sim.cornerMaxZ : 0f)).Append("\", ");
        }

        // ---------------- yields.csv（誰が誰に譲ったか・2026-10-23）----------------

        public const string YieldHeader =
            "condition_id,seed,t_s,who,rank,to_whom,x,z,aisle,place,in_corner," +
            "waited_s,stepped_aside,passed";

        /// <summary>
        /// 譲り1回を1行で書く（yields.csv）。
        /// rank は譲った側の順番（0=ロボット・1=スタッフ・2=客）、
        /// stepped_aside は相手の道から横へ 0.6 m 以上どけたか、
        /// passed は相手が通り過ぎて終わったか（0 なら待ちすぎてやめた）。
        /// </summary>
        public static void WriteYieldsCsv(string path, string conditionId, int seed,
                                          IReadOnlyList<YieldRow> rows)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(YieldHeader).Append('\n');
            if (rows != null)
            {
                for (int i = 0; i < rows.Count; i++)
                {
                    YieldRow r = rows[i];
                    sb.Append(conditionId).Append(',').Append(seed).Append(',')
                      .Append(F(r.tS)).Append(',').Append(r.who).Append(',')
                      .Append(r.rank).Append(',').Append(r.toWhom).Append(',')
                      .Append(F(r.x)).Append(',').Append(F(r.z)).Append(',')
                      .Append(r.aisle).Append(',').Append(r.place).Append(',')
                      .Append(r.inCorner ? 1 : 0).Append(',')
                      .Append(F(r.waitedS)).Append(',')
                      .Append(r.steppedAside ? 1 : 0).Append(',')
                      .Append(r.passed ? 1 : 0).Append('\n');
                }
            }
            File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        // ---------------- passes.csv（ロボットどうしのすれ違い・依頼 v13）----------------

        public const string PassHeader =
            "condition_id,seed,t_start_s,t_end_s,robot_a,robot_b,x,z," +
            "min_gap_m,min_speed_mps,outcome";

        /// <summary>
        /// すれ違い1回を1行で書く（passes.csv）。
        /// min_gap_m は いちばん近づいたときの体どうしの間、
        /// min_speed_mps は そのあいだで2台のうちいちばん遅かった速さ、
        /// outcome は すれ違えた／幅が足りず今の決まりへ／向かい合って止まった。
        /// passByEnabled が切の回でも同じように書く（入の回と比べるため）
        /// </summary>
        public static void WritePassesCsv(string path, string conditionId, int seed,
                                          IReadOnlyList<PassRow> rows)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(PassHeader).Append('\n');
            if (rows != null)
            {
                for (int i = 0; i < rows.Count; i++)
                {
                    PassRow r = rows[i];
                    sb.Append(conditionId).Append(',').Append(seed).Append(',')
                      .Append(F(r.startS)).Append(',').Append(F(r.endS)).Append(',')
                      .Append(r.robotA).Append(',').Append(r.robotB).Append(',')
                      .Append(F(r.x)).Append(',').Append(F(r.z)).Append(',')
                      .Append(F(r.minGapM)).Append(',').Append(F(r.minSpeedMps)).Append(',')
                      .Append(r.outcome).Append('\n');
                }
            }
            File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        // ---------------- dishes.csv（皿ごと・サイトの試算 v2.7）----------------

        public const string DishHeader =
            "condition_id,seed,dish_id,table_id,group_id,fragile," +
            "ready_s,first_taken_s,arrived_s,carried_by,pass_wait_s,carry_s,requeue_lost_s," +
            "requeued,staff_from_start,robot_wait_expired,late_staff_pickup,deferred_for_aisle";

        /// <summary>
        /// 届いた料理1皿を1行で書く（dishes.csv）。
        /// carried_by は「ロボット」か「スタッフ」、pass_wait_s は受け渡し口で待った秒数、
        /// late_staff_pickup は robot_first_wait_s を過ぎて人が拾った皿（1 なら該当）。
        /// </summary>
        public static void WriteDishesCsv(string path, string conditionId, int seed,
                                          IReadOnlyList<DishRow> rows)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(DishHeader).Append('\n');
            if (rows != null)
            {
                for (int i = 0; i < rows.Count; i++)
                {
                    DishRow r = rows[i];
                    sb.Append(conditionId).Append(',').Append(seed).Append(',')
                      .Append(r.dishId).Append(',').Append(r.tableId).Append(',')
                      .Append(r.groupId).Append(',').Append(r.fragile ? 1 : 0).Append(',')
                      .Append(F(r.readyS)).Append(',').Append(F(r.firstTakenS)).Append(',')
                      .Append(F(r.arrivedS)).Append(',')
                      .Append(r.byRobot ? "ロボット" : "スタッフ").Append(',')
                      .Append(F(r.passWaitS)).Append(',').Append(F(r.carryS)).Append(',')
                      .Append(F(r.requeueS)).Append(',').Append(r.requeued).Append(',')
                      .Append(r.staffFromStart ? 1 : 0).Append(',')
                      .Append(r.robotWaitExpired ? 1 : 0).Append(',')
                      .Append(r.lateStaffPickup ? 1 : 0).Append(',')
                      .Append(r.deferredForAisle ? 1 : 0).Append('\n');
                }
            }
            File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        // ---------------- requeues.csv（B8）----------------

        public const string RequeueHeader =
            "condition_id,seed,t_s,dish_id,table_id,robot_id,fragile,ready_s,waited_s,requeued_so_far,why,to_staff";

        /// <summary>届けられずに受け渡し口へ戻した1回を1行で書く</summary>
        public static void WriteRequeuesCsv(string path, string conditionId, int seed,
                                            IReadOnlyList<RequeueRow> rows)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(RequeueHeader).Append('\n');
            if (rows != null)
            {
                for (int i = 0; i < rows.Count; i++)
                {
                    RequeueRow r = rows[i];
                    sb.Append(conditionId).Append(',').Append(seed).Append(',')
                      .Append(F(r.tS)).Append(',').Append(r.dishId).Append(',').Append(r.tableId).Append(',')
                      .Append(r.robotId).Append(',').Append(r.fragile ? 1 : 0).Append(',')
                      .Append(F(r.readyS)).Append(',').Append(F(r.tS - r.readyS)).Append(',')
                      .Append(r.requeuedSoFar).Append(',').Append(r.why).Append(',')
                      .Append(r.toStaff ? 1 : 0).Append('\n');
                }
            }
            File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        // ---------------- stops.csv ----------------

        public const string StopHeader =
            "condition_id,seed,robot_id,trip_id,t_s,x,z,aisle,in_narrow,cause,cause_detail,path_blocked," +
            "duration_s,resolved,clearance_left_cm,clearance_right_cm,min_gap_cm," +
            "blocked_s,no_path_s,yield_s,clear_wait_s,replans," +
            // 道が引けない原因を追うための列（記録だけ。動きは変えない）
            "solve_from,solve_to,solve_status,solve_corners,off_mesh_m";

        public void WriteStopsCsv(string path)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(StopHeader).Append('\n');
            for (int i = 0; i < _stops.Count; i++)
            {
                StopRow r = _stops[i];
                sb.Append(r.conditionId).Append(',')
                  .Append(r.seed).Append(',')
                  .Append(r.robotId).Append(',')
                  .Append(r.tripId).Append(',')
                  .Append(F(r.tS)).Append(',')
                  .Append(F(r.x)).Append(',')
                  .Append(F(r.z)).Append(',')
                  .Append(r.aisle).Append(',')
                  .Append(r.inNarrow ? 1 : 0).Append(',')
                  .Append(r.cause).Append(',')
                  .Append(r.causeDetail).Append(',')
                  .Append(r.pathBlocked ? 1 : 0).Append(',')
                  .Append(F(r.durationS)).Append(',')
                  .Append(r.resolved).Append(',')
                  .Append(F(r.clearanceLeftCm)).Append(',')
                  .Append(F(r.clearanceRightCm)).Append(',')
                  .Append(F(r.minGapCm)).Append(',')
                  .Append(F(r.blockedS)).Append(',')
                  .Append(F(r.noPathS)).Append(',')
                  .Append(F(r.yieldS)).Append(',')
                  .Append(F(r.clearWaitS)).Append(',')
                  .Append(r.replans).Append(',')
                  .Append(r.solveFrom).Append(',')
                  .Append(r.solveTo).Append(',')
                  .Append(r.solveStatus).Append(',')
                  .Append(r.solveCorners).Append(',')
                  .Append(F(r.offMeshM))
                  .Append('\n');
            }
            File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        // ---------------- run_info.json（1回ぶんの回し方とコマの粗さ）----------------

        /// <summary>1回ぶんの回し方と、コマの粗さの記録</summary>
        public class RunInfo
        {
            public string runLabel = "";
            public string conditionId = "";
            public string timeMode = "";
            public string storeKind = "";
            public string crowd = "";
            public int tripsRequested;
            public int tripsDone;
            public int robots;
            public int seed;
            public float aisleWidthM;

            /// <summary>卓の停止点を通路の中心からずらす距離（m）。.asset に入れた値（C1）</summary>
            public float deliveryOffsetM;

            /// <summary>そのうち、実際に使った分（通路が狭いときは NavMesh に乗る範囲に抑える）</summary>
            public float deliveryOffsetUsedM;

            public float simSeconds;
            public float wallSeconds;
            public float timeScale;
            public float captureDeltaTime;
            public bool stoppedEarly;

            /// <summary>
            /// 同じ通路に2台を集めない決まり（v5・A）で回したか。
            /// Unity だけの決まりなので、どちらで回したかを残す
            /// </summary>
            public bool oneRobotPerAisle;

            /// <summary>譲る順番の決まりで回したか（v6。RobotSpec.yieldOrderEnabled）</summary>
            public bool yieldOrderEnabled;

            // ---- すれ違い（依頼 v13 の 依頼1）----
            /// <summary>すれ違いの走り方を入れて回したか（RobotSpec.passByEnabled）</summary>
            public bool passByEnabled;

            /// <summary>そのときの3つの値（m・m/秒・m）</summary>
            public float passByDistanceM, passBySpeedMps, passBySeparationM;

            /// <summary>回し始めの Unity Version Control の changeset（段階B。メニューが入れる）</summary>
            public string unityCs = "";

            // ---- 誰が回したか（依頼 v12 の 2 章・2026-10-26）----
            /// <summary>
            /// 回した人（入れ物）。"editor"（段階B・Site Run）/ "headless"（画面なしの実行ファイル）。
            /// 夜の回とエディタの回を、台帳と出力から取り違えないため
            /// </summary>
            public string runner = "editor";

            /// <summary>画面なしの実行ファイルの版（build_info.json の build_id）。エディタのときは空</summary>
            public string buildId = "";

            /// <summary>その夜の名前（例 20261026）。夜の実行係が渡す。エディタのときは空</summary>
            public string night = "";

            /// <summary>
            /// 夜の実行係に **決めた** 同時本数（N）。夜の実行係が渡す。エディタのときは 0。
            /// 並びの最後の N-1 本は、これより少ない本数で回る
            /// </summary>
            public int parallel;

            /// <summary>
            /// この回を起動したときに **実際に** 同時に走ることになった本数
            /// （この回を含む）。夜の実行係が -ssRunning で渡す。
            /// 0 は「分からない」（実行係を通していない起動）。
            ///
            /// なぜ分けるか（2026-10-26・busycheck）:
            ///   同時本数は stuck_count・rescues・robot_share・late_staff_dishes を
            ///   動かす。並びの最後の数本は薄い本数で回るので、
            ///   あとで「薄い回だったか」を見分けられるようにしておく。
            ///   ※ これは **起動したとき** の本数。回している途中で減ることはある。
            ///     正確な重なりは night/{夜}/runner_log.txt の始まり・終わりの時刻から出せる
            /// </summary>
            public int parallelActual;

            /// <summary>
            /// 回し始めに保留中の変更があったか（段階B）。
            /// あれば「保留あり N 件」。status に関係なく残す（依頼 v7 の 2）
            /// </summary>
            public string pendingNote = "";

            // ---- ずれの切り分け（依頼 v11 の 5 章。試しだけ。check-in しない）----
            /// <summary>入れた試しの切り替え（空なら cs:2 と同じ動き）</summary>
            public string detCheck = "";

            /// <summary>
            /// この回で実際に入った道探しの枠（NavMesh.pathfindingIterationsPerFrame）。
            /// 2026-10-26：隠れた設定が残っていないかを、回ごとに確かめられるようにした
            /// </summary>
            public int pathIterationsPerFrame;

            /// <summary>前のセンサーの箱に入った当たりのいちばん多かった数と、切り捨てた回数</summary>
            public int sensorMaxHits, sensorOverflows;

            /// <summary>店員優先の版で回したか（v5。人のよけ方の切り替え）</summary>
            public bool staffFirstYield;

            /// <summary>1コマの時間（秒）。Time.deltaTime をそのまま</summary>
            public readonly List<float> frameDeltas = new List<float>();

            /// <summary>1コマでロボットが進んだ距離（m）。台のうちいちばん大きいもの</summary>
            public readonly List<float> robotSteps = new List<float>();

            /// <summary>
            /// 1コマで人が進んだ距離（m）。通路を歩く人のうちいちばん大きいもの（B10）。
            /// スタッフは 1.0 m/秒 なので、1コマ 0.05 秒だと 0.05 m でちょうど境目になる。
            /// </summary>
            public readonly List<float> peopleSteps = new List<float>();

            /// <summary>椅子を引いた回数（B6）</summary>
            public int chairPulls;

            /// <summary>椅子を引いたきっかけごとの回数（B6）</summary>
            public readonly List<KeyValuePair<string, int>> chairPullsByReason =
                new List<KeyValuePair<string, int>>();

            // ---- 依頼 v9 の 5 章：地図の上で自分で道を引く ----

            /// <summary>実際に使った道の決め方（MapPlanner / SiteRoutes）</summary>
            public string pathMode = "";

            /// <summary>地図の版の番号（map.json の meta.generation）</summary>
            public string mapGeneration = "";

            /// <summary>道の引き方の数字</summary>
            public float narrowCellCost, centerWeight, preferClearM, detourAfterSec;

            /// <summary>1 回の道探しにかかった時間（ミリ秒。往復ごとの最大を集めたもの）</summary>
            public readonly List<float> planMs = new List<float>();

            /// <summary>
            /// 出発点が地図では通れないマス（0）だったために寄せた回数の合計。
            /// 通路の入口で待つ点・横へどいた点は free_class では 0 になるので、ここに出る
            /// </summary>
            public int mapSnapStarts;

            /// <summary>道が引けなかった回数の合計</summary>
            public int planFails;
        }

        /// <summary>コマの粗さを1行にまとめる（Console 用）</summary>
        public static string FrameStatsText(RunInfo info)
        {
            if (info == null || info.frameDeltas.Count == 0) return "コマの記録なし";
            int over = 0;
            for (int i = 0; i < info.robotSteps.Count; i++) if (info.robotSteps[i] > 0.05f) over++;
            return "コマ " + info.frameDeltas.Count +
                   " / 1コマの時間 中央 " + F(Percentile(info.frameDeltas, 0.5f)) +
                   "s・90% " + F(Percentile(info.frameDeltas, 0.9f)) +
                   "s・最大 " + F(Percentile(info.frameDeltas, 1f)) + "s" +
                   " / 1コマで進む距離 最大 " + F(Percentile(info.robotSteps, 1f)) +
                   "m・90% " + F(Percentile(info.robotSteps, 0.9f)) + "m" +
                   " / 0.05 m を超えたコマ ロボット " + over + "（" +
                   (info.robotSteps.Count > 0 ? (100f * over / info.robotSteps.Count).ToString("0.0") : "0") +
                   "%）・人 " + CountOver(info.peopleSteps, 0.05f) + "（" +
                   (info.peopleSteps.Count > 0
                        ? (100f * CountOver(info.peopleSteps, 0.05f) / info.peopleSteps.Count).ToString("0.0")
                        : "0") + "%）";
        }

        public static void WriteRunInfoJson(string path, RunInfo info)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("{\n");
            sb.Append("  \"run_label\": ").Append(Str(info.runLabel)).Append(",\n");
            sb.Append("  \"written_at\": ").Append(Str(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))).Append(",\n");
            sb.Append("  \"condition_id\": ").Append(Str(info.conditionId)).Append(",\n");
            sb.Append("  \"store_kind\": ").Append(Str(info.storeKind)).Append(",\n");
            sb.Append("  \"time_mode\": ").Append(Str(info.timeMode)).Append(",\n");
            sb.Append("  \"time_scale\": ").Append(F(info.timeScale)).Append(",\n");
            sb.Append("  \"capture_delta_time\": ").Append(F(info.captureDeltaTime)).Append(",\n");
            sb.Append("  \"aisle_width_m\": ").Append(F(info.aisleWidthM)).Append(",\n");
            sb.Append("  \"delivery_offset_m\": ").Append(F(info.deliveryOffsetM)).Append(",\n");
            sb.Append("  \"delivery_offset_used_m\": ").Append(F(info.deliveryOffsetUsedM)).Append(",\n");
            sb.Append("  \"crowd\": ").Append(Str(info.crowd)).Append(",\n");
            sb.Append("  \"robots\": ").Append(info.robots).Append(",\n");
            sb.Append("  \"seed\": ").Append(info.seed).Append(",\n");
            sb.Append("  \"trips_requested\": ").Append(info.tripsRequested).Append(",\n");
            sb.Append("  \"trips_done\": ").Append(info.tripsDone).Append(",\n");
            sb.Append("  \"stopped_early\": ").Append(info.stoppedEarly ? "true" : "false").Append(",\n");
            sb.Append("  \"one_robot_per_aisle\": ")
              .Append(info.oneRobotPerAisle ? "true" : "false").Append(",\n");
            sb.Append("  \"yield_order_enabled\": ")
              .Append(info.yieldOrderEnabled ? "true" : "false").Append(",\n");
            // 依頼 v13 の 依頼1：すれ違いの入切と3つの値
            sb.Append("  \"pass_by\": {");
            sb.Append("\"enabled\": ").Append(info.passByEnabled ? "true" : "false").Append(", ");
            sb.Append("\"distance_m\": ").Append(F(info.passByDistanceM)).Append(", ");
            sb.Append("\"speed_mps\": ").Append(F(info.passBySpeedMps)).Append(", ");
            sb.Append("\"separation_m\": ").Append(F(info.passBySeparationM));
            sb.Append("},\n");
            sb.Append("  \"unity_cs\": ").Append(Str(info.unityCs)).Append(",\n");
            // 依頼 v12 の 2 章：誰が回したか（夜の回とエディタの回を取り違えないため）
            sb.Append("  \"runner\": ").Append(Str(info.runner)).Append(",\n");
            sb.Append("  \"build_id\": ").Append(Str(info.buildId)).Append(",\n");
            sb.Append("  \"night\": ").Append(Str(info.night)).Append(",\n");
            sb.Append("  \"parallel\": ").Append(info.parallel).Append(",\n");
            sb.Append("  \"parallel_actual\": ").Append(info.parallelActual).Append(",\n");
            // 依頼 v11 の 5 章（試しだけ）。det_check が空なら cs:2 と同じ動き
            sb.Append("  \"det_check\": ").Append(Str(info.detCheck)).Append(",\n");
            // 2026-10-26：実際に入った道探しの枠（既定 100）
            sb.Append("  \"path_iterations_per_frame\": ")
              .Append(info.pathIterationsPerFrame).Append(",\n");
            sb.Append("  \"sensor_max_hits\": ").Append(info.sensorMaxHits).Append(",\n");
            sb.Append("  \"sensor_overflows\": ").Append(info.sensorOverflows).Append(",\n");
            sb.Append("  \"pending_changes\": ").Append(Str(info.pendingNote)).Append(",\n");
            sb.Append("  \"staff_first_yield\": ")
              .Append(info.staffFirstYield ? "true" : "false").Append(",\n");
            sb.Append("  \"sim_seconds\": ").Append(F(info.simSeconds)).Append(",\n");
            sb.Append("  \"wall_seconds\": ").Append(F(info.wallSeconds)).Append(",\n");
            sb.Append("  \"speedup\": ").Append(F(info.wallSeconds > 0.01f ? info.simSeconds / info.wallSeconds : 0f)).Append(",\n");

            sb.Append("  \"frame_delta_s\": ");
            AppendStats(sb, info.frameDeltas);
            sb.Append(",\n");

            sb.Append("  \"robot_step_m\": ");
            AppendStats(sb, info.robotSteps);
            sb.Append(",\n");

            sb.Append("  \"people_step_m\": ");
            AppendStats(sb, info.peopleSteps);
            sb.Append(",\n");

            // すり抜けの確かめ（検査3）。ロボットと人を分けて出す
            int over = CountOver(info.robotSteps, 0.05f);
            int overPeople = CountOver(info.peopleSteps, 0.05f);
            sb.Append("  \"frames_over_005m\": ").Append(over).Append(",\n");
            sb.Append("  \"frames_over_005m_ratio\": ")
              .Append(F(info.robotSteps.Count > 0 ? (float)over / info.robotSteps.Count : 0f)).Append(",\n");
            sb.Append("  \"people_frames_over_005m\": ").Append(overPeople).Append(",\n");
            sb.Append("  \"people_frames_over_005m_ratio\": ")
              .Append(F(info.peopleSteps.Count > 0 ? (float)overPeople / info.peopleSteps.Count : 0f)).Append(",\n");

            // 椅子を引いた回数（B6）
            sb.Append("  \"chair_pulls\": ").Append(info.chairPulls).Append(",\n");
            sb.Append("  \"chair_pulls_by_reason\": {");
            for (int i = 0; i < info.chairPullsByReason.Count; i++)
            {
                if (i > 0) sb.Append(", ");
                sb.Append(Str(info.chairPullsByReason[i].Key)).Append(": ")
                  .Append(info.chairPullsByReason[i].Value);
            }
            sb.Append("},\n");

            // 依頼 v9 の 5 章：地図の上で自分で道を引く
            sb.Append("  \"path_mode\": ").Append(Str(info.pathMode)).Append(",\n");
            sb.Append("  \"map_generation\": ").Append(Str(info.mapGeneration)).Append(",\n");
            sb.Append("  \"narrow_cell_cost\": ").Append(F(info.narrowCellCost)).Append(",\n");
            sb.Append("  \"center_weight\": ").Append(F(info.centerWeight)).Append(",\n");
            sb.Append("  \"prefer_clear_m\": ").Append(F(info.preferClearM)).Append(",\n");
            sb.Append("  \"detour_after_sec\": ").Append(F(info.detourAfterSec)).Append(",\n");
            sb.Append("  \"plan_ms_max_per_trip\": ");
            AppendStats(sb, info.planMs);
            sb.Append(",\n");
            sb.Append("  \"map_snap_starts\": ").Append(info.mapSnapStarts).Append(",\n");
            sb.Append("  \"plan_fails\": ").Append(info.planFails).Append("\n");
            sb.Append("}\n");

            File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        /// <summary>しきい値を超えたコマの数</summary>
        static int CountOver(List<float> values, float limit)
        {
            if (values == null) return 0;
            int n = 0;
            for (int i = 0; i < values.Count; i++) if (values[i] > limit) n++;
            return n;
        }

        static void AppendStats(StringBuilder sb, List<float> values)
        {
            if (values == null || values.Count == 0) { sb.Append("null"); return; }
            float sum = 0f;
            for (int i = 0; i < values.Count; i++) sum += values[i];
            sb.Append("{\"count\": ").Append(values.Count);
            sb.Append(", \"mean\": ").Append(F(sum / values.Count));
            sb.Append(", \"min\": ").Append(F(Percentile(values, 0f)));
            sb.Append(", \"p50\": ").Append(F(Percentile(values, 0.5f)));
            sb.Append(", \"p90\": ").Append(F(Percentile(values, 0.9f)));
            sb.Append(", \"p99\": ").Append(F(Percentile(values, 0.99f)));
            sb.Append(", \"max\": ").Append(F(Percentile(values, 1f)));
            sb.Append("}");
        }

        // ---------------- conditions.json ----------------

        /// <summary>
        /// 18条件の中身と、設定ファイル（.asset）の値すべてを書く。
        /// 値は実行したときのものを JsonUtility でそのまま出す。
        /// </summary>
        public static void WriteConditionsJson(string path, string runName,
                                               StoreLayout layout, RobotSpec robotSpec,
                                               PeopleSpec peopleSpec, ExperimentPlan plan,
                                               List<ExperimentPlan.Condition> conditions)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("{\n");
            sb.Append("  \"run_name\": ").Append(Str(runName)).Append(",\n");
            sb.Append("  \"written_at\": ").Append(Str(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))).Append(",\n");
            sb.Append("  \"unity_version\": ").Append(Str(Application.unityVersion)).Append(",\n");
            sb.Append("  \"trips_csv_columns\": ").Append(Str(Header)).Append(",\n");
            sb.Append("  \"groups_csv_columns\": ").Append(Str(GroupHeader)).Append(",\n");
            sb.Append("  \"dishes_csv_columns\": ").Append(Str(DishHeader)).Append(",\n");

            sb.Append("  \"settings\": {\n");
            sb.Append("    \"store_layout\": ").Append(Dump(layout)).Append(",\n");
            sb.Append("    \"robot_spec\": ").Append(Dump(robotSpec)).Append(",\n");
            sb.Append("    \"people_spec\": ").Append(Dump(peopleSpec)).Append(",\n");
            sb.Append("    \"experiment_plan\": ").Append(Dump(plan)).Append("\n");
            sb.Append("  },\n");

            // 組み立てた形から出る値（conditions.json だけ見れば店の大きさが分かるように）
            sb.Append("  \"derived\": {\n");
            if (layout != null)
            {
                StorePlan sp = layout.NewPlan();
                sb.Append("    \"store_kind\": ").Append(Str(layout.StoreKindName)).Append(",\n");
                sb.Append("    \"aisle_width_m\": ").Append(F(layout.AisleWidth)).Append(",\n");
                if (sp != null)
                {
                    sb.Append("    \"floor_min_x\": ").Append(F(sp.floor.xMin)).Append(",\n");
                    sb.Append("    \"floor_max_x\": ").Append(F(sp.floor.xMax)).Append(",\n");
                    sb.Append("    \"floor_min_z\": ").Append(F(sp.floor.zMin)).Append(",\n");
                    sb.Append("    \"floor_max_z\": ").Append(F(sp.floor.zMax)).Append(",\n");
                    sb.Append("    \"outer_size_x_m\": ").Append(F(sp.floor.SizeX)).Append(",\n");
                    sb.Append("    \"outer_size_z_m\": ").Append(F(sp.floor.SizeZ)).Append(",\n");
                    sb.Append("    \"table_count\": ").Append(sp.seats.Count).Append(",\n");
                    sb.Append("    \"aisle_count\": ").Append(sp.aisles.Count).Append(",\n");
                    sb.Append("    \"narrow_aisle_count\": ").Append(sp.NarrowAisleCount).Append("\n");
                }
                else
                {
                    sb.Append("    \"plan\": null\n");
                }
            }
            sb.Append("  },\n");

            sb.Append("  \"conditions\": [\n");
            if (conditions != null)
            {
                for (int i = 0; i < conditions.Count; i++)
                {
                    ExperimentPlan.Condition c = conditions[i];
                    sb.Append("    {");
                    sb.Append("\"condition_id\": ").Append(Str(c.id)).Append(", ");
                    sb.Append("\"aisle_width_m\": ").Append(F(c.aisleWidthM)).Append(", ");
                    sb.Append("\"aisle_width_index\": ").Append(c.aisleWidthIndex).Append(", ");
                    sb.Append("\"crowd\": ").Append(Str(ExperimentPlan.CrowdName(c.crowd))).Append(", ");
                    sb.Append("\"walkers\": ").Append(peopleSpec != null ? peopleSpec.WalkerCount(c.crowd) : 0).Append(", ");
                    sb.Append("\"robots\": ").Append(c.robots).Append(", ");
                    sb.Append("\"trips\": ").Append(plan != null ? plan.tripsPerCondition : 0).Append(", ");
                    sb.Append("\"seeds\": ").Append(IntArray(plan != null ? plan.seeds : null)).Append(", ");
                    sb.Append("\"aisle_yielding\": ").Append(robotSpec != null && robotSpec.aisleYielding ? "true" : "false");
                    sb.Append('}');
                    if (i < conditions.Count - 1) sb.Append(',');
                    sb.Append('\n');
                }
            }
            sb.Append("  ]\n");
            sb.Append("}\n");

            File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        static string Dump(ScriptableObject asset)
        {
            if (asset == null) return "null";
            return JsonUtility.ToJson(asset, false);
        }

        static string IntArray(int[] values)
        {
            if (values == null || values.Length == 0) return "[]";
            StringBuilder sb = new StringBuilder("[");
            for (int i = 0; i < values.Length; i++)
            {
                if (i > 0) sb.Append(", ");
                sb.Append(values[i]);
            }
            return sb.Append(']').ToString();
        }

        static string Str(string s)
        {
            if (s == null) return "null";
            StringBuilder sb = new StringBuilder("\"");
            for (int i = 0; i < s.Length; i++)
            {
                char c = s[i];
                if (c == '"' || c == '\\') sb.Append('\\').Append(c);
                else if (c == '\n') sb.Append("\\n");
                else if (c == '\r') sb.Append("\\r");
                else if (c == '\t') sb.Append("\\t");
                else sb.Append(c);
            }
            return sb.Append('"').ToString();
        }

        // ---------------- summary.json ----------------

        /// <summary>条件ごとにまとめる</summary>
        /// <summary>試算（haizen_sim.py）と同じ意味の数字。名前も同じにする</summary>
        public class SimLike
        {
            public List<float> dishWaitMin = new List<float>();
            public List<float> seatWaitMin = new List<float>();
            public List<float> turnMin = new List<float>();
            public float coldMin = 3f;
            public int robotDishes, staffDishes, requeuedDishes, groups;

            /// <summary>
            /// 往復の打ち切りが続いて「スタッフだけが運ぶ」に回した皿の数
            /// （2026-10-27）。0 なら上限に届いた皿が1枚も無かった
            /// </summary>
            public int dishesToStaffAfterTimeout;

            /// <summary>あきらめた・移した・卓を空けた回数（B1）</summary>
            public int gaveUp, warped, tableFreed;

            /// <summary>人どうしの詰まりのうち、近くでロボットが止まっていたもの（B9）</summary>
            public int robotBlockedJams;

            /// <summary>人がロボットに近づきすぎた回数・待った回数と秒数・回り道した回数</summary>
            public int robotContacts, robotYieldWaits, robotYieldDetours;
            public float robotYieldWaitSeconds;

            // ---- ロボットどうしのすれ違い（依頼 v13 の 依頼1）----
            /// <summary>すれ違いの回数（同じ2台の1回は1回）と、減速していた秒数の合計</summary>
            public int passCount;
            public float passSlowS;

            /// <summary>向かい合って止まった回数と、いちばん長かった秒数（入でも切でも数える）</summary>
            public int faceStopCount;
            public float faceStopMaxS;

            // 2026-10-26（依頼 v13 の直し③）：向かい合いを場所で分けて数える。
            // 受け渡し口は点が1つなので2台が必ず向かい合う。通路のふさぎ合いと混ぜない
            public int faceStopAtPassCount, faceStopAtStandbyCount;
            public float faceStopAtPassMaxS, faceStopAtStandbyMaxS;
            public int faceStopInAisleCount, faceStopOverlapCount;
            public float faceStopInAisleMaxS;

            /// <summary>レジの前にいた人数（いちばん多いとき・平均）</summary>
            public int registerCrowdMax;
            public float registerCrowdMean;

            /// <summary>会計の回数と、そのうちスタッフが店員側にいなかった回数（0 になるはず）</summary>
            public int registerPayCount, registerPayNotAtBackCount;

            /// <summary>立つ点から離れて会計した回数（0 になるはず）と、その距離</summary>
            public int registerPayOffSpotCount;
            public float registerPayMeanOffsetM, registerPayMaxOffsetM;

            /// <summary>会計でないスタッフが店員側にいた回数と秒数（どちらも 0 になるはず）</summary>
            public int registerBackIntruderCount;
            public float registerBackIntruderSeconds;

            /// <summary>スタッフの時間の使い方（仕事の名前 → 秒）</summary>
            public readonly List<KeyValuePair<string, float>> staffTime =
                new List<KeyValuePair<string, float>>();

            /// <summary>仕事が板に出てから取られるまでの秒数の平均（仕事の名前 → 秒）</summary>
            public readonly List<KeyValuePair<string, float>> jobWaitMean =
                new List<KeyValuePair<string, float>>();

            /// <summary>届いた料理の、届くまでの内訳（ロボット・スタッフ別に分ける）</summary>
            public readonly List<ServiceOps.DishParts> dishParts = new List<ServiceOps.DishParts>();

            /// <summary>
            /// 2分を過ぎて人が拾った皿の数（サイトの試算 v2.7）。
            /// ロボットだけを待つ時間（robot_first_wait_s）を過ぎてからスタッフが運んだ皿
            /// </summary>
            public int lateStaffDishes;

            /// <summary>その回に使った robot_first_wait_s（秒）。サイトから読んだ値</summary>
            public float robotFirstWaitS;

            // ---- 譲り合い（2026-10-23・譲る順番 v6）----
            /// <summary>誰が誰に譲った1回ずつ（yields.csv と同じもの）</summary>
            public readonly List<YieldRow> yields = new List<YieldRow>();

            /// <summary>
            /// 受け渡し口と客席をつなぐ角の範囲（RobotSpec の1か所から写す）。
            /// 角での止まりの回数・秒数を summary に出すのに使う
            /// </summary>
            public float cornerMinX, cornerMaxX, cornerMinZ, cornerMaxZ;

            /// <summary>椅子を引いた回数（B6）</summary>
            public int chairPulls;

            /// <summary>ドリンクバー：並んだ回数・あきらめた回数・並んで待った分（B4）</summary>
            public int drinkBarQueued, drinkBarGaveUp;
            public readonly List<float> drinkBarWaitMin = new List<float>();
            public int jamCount; public float jamTotalS;
            public float walkersMean;
            public float measuredMinutes;

            // ---- サイトと同じ名前で出す値（v5 3章・meta.metrics）----
            /// <summary>入店待ちの内訳のもと（分）。分け方はサイトの試算と同じ</summary>
            public readonly List<float> entryWaitMin = new List<float>();
            public readonly List<float> fullWaitMin = new List<float>();
            public readonly List<float> blockedWaitMin = new List<float>();
            public readonly List<float> guideWaitMin = new List<float>();
            public readonly List<float> staffWaitMin = new List<float>();

            /// <summary>
            /// pay_wait_mean のもと（分。依頼 v10 の 2）。
            /// 組ごとに（会計が終わった時刻 − 食べ終えた時刻 − pay_s）。
            /// 逃がし弁で卓を空けられた組と、会計が終わっていない組は入れない
            /// </summary>
            public readonly List<float> payWaitMin = new List<float>();

            /// <summary>
            /// 同じものを、食べ終えた時刻ではなく会計に入った時刻（pay_start_s）から数えた分。
            /// 遅れて届いた皿・立ち歩きの人を待った分が pay_wait_mean にどれだけ混ざっているかを見る
            /// </summary>
            public readonly List<float> payWaitFromStartMin = new List<float>();

            /// <summary>
            /// 片付け・会計の仕事が板に載ってから取られるまでの最大（秒）と、
            /// そのうち長びいた数（依頼 v10 の 2）。仕事の名前 → 値
            /// </summary>
            public readonly List<KeyValuePair<string, float>> jobWaitMax =
                new List<KeyValuePair<string, float>>();
            public readonly List<KeyValuePair<string, int>> jobWaitOver =
                new List<KeyValuePair<string, int>>();

            /// <summary>長びいたと数える境目（秒）。tableStuckFreeSec と同じ 600 秒</summary>
            public float jobWaitOverSec;

            /// <summary>walk_km_per_staff・staff_util・robot_util</summary>
            public float walkKmPerStaff, staffUtil, robotUtil;

            /// <summary>
            /// 同じ通路に2台を集めないために、料理を後に回した回数（v5・A）
            /// </summary>
            public int aisleDeferrals;

            /// <summary>
            /// 店員優先の版の逃げ道：相手が止まったままで店員が回り道した回数（v5）
            /// </summary>
            public int robotStuckDetours;

            /// <summary>店員優先の版で回したか（v5）</summary>
            public bool staffFirstYield;

            /// <summary>rescues：立ち往生でスタッフを呼んだ回数（着いて動かした回数も残す）</summary>
            public int rescues, rescuesDone;
            public float rescueWaitS;

            /// <summary>pauses：一時停止の回数（自分で動き出した止まりの合計）</summary>
            public int pauses;

            /// <summary>サイトの条件で回したか（v5 3章）と、その条件の名前</summary>
            public bool siteRun;
            public string siteConditionId = "";

            /// <summary>
            /// サイトの試算の答え（unity_conditions の site_result）。
            /// 同じ summary.json に入れておけば、並べた表をあとから手で写さずに作れる
            /// </summary>
            public readonly List<KeyValuePair<string, float>> siteResult =
                new List<KeyValuePair<string, float>>();

            /// <summary>来客を受け付けた分と、全員が帰るまでの分（打ち切りなら everyoneLeft が false）</summary>
            public float arrivalsMinutes, totalMinutes;
            public bool everyoneLeft;

            // ---- 時間帯ごとの記録の確かめ（依頼 v10b の 3 A）----
            /// <summary>timeline.csv の行数と、そのときの卓・スタッフの数</summary>
            public int timelineRows, timelineTables, timelineStaff;

            /// <summary>合計が合わなかった行の数（どちらも 0 になるはず）</summary>
            public int timelineBadTableRows, timelineBadStaffRows;

            /// <summary>合わなかった行のうち、いちばん早いもの（合っていれば空）</summary>
            public string timelineFirstBad = "";
        }

        SimLike _sim;
        public void SetSimLike(SimLike s) { _sim = s; }

        public void WriteSummaryJson(string path, string runName)
        {
            // 条件ごとに行を分ける（出てきた順を保つ）
            List<string> order = new List<string>();
            Dictionary<string, List<Row>> byCondition = new Dictionary<string, List<Row>>();
            for (int i = 0; i < _rows.Count; i++)
            {
                string id = _rows[i].conditionId;
                if (!byCondition.ContainsKey(id)) { byCondition[id] = new List<Row>(); order.Add(id); }
                byCondition[id].Add(_rows[i]);
            }

            StringBuilder sb = new StringBuilder();
            sb.Append("{\n");
            sb.Append("  \"run_name\": ").Append(Str(runName)).Append(",\n");
            sb.Append("  \"written_at\": ").Append(Str(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))).Append(",\n");
            sb.Append("  \"note\": ").Append(Str(
                "speed_mps = distance_m / (total_s - stopped_s)。" +
                "stopped_s には、動けなくなった60秒と、スタッフを待った秒数（stuck_wait_s）、" +
                "通路の入口で待った秒数も入る。" +
                "pause_s は stop_durations_s（自分で動き出した止まりだけ）の分布。" +
                "site_metrics はサイトの meta.metrics と同じ名前・同じ定義。" +
                "入店待ちの分け方もサイトの試算と同じで、" +
                "blocked_wait は片付け待ちの卓の数だけ待っている列の前から順に足した分、" +
                "full_wait は seat_wait − blocked_wait、" +
                "staff_wait は blocked_wait + guide_wait。" +
                "pay_wait は（会計が終わった時刻 − 食べ終えた時刻 − pay_s）で、" +
                "逃がし弁で卓を空けられた組は入れない。" +
                "Unity は会計の人とスタッフがレジまで歩く時間が入るので、試算より少し長く出る。" +
                "pauses と rescues は、サイト側が unity_measures の仮の値から出した数なので出どころが違う。"))
              .Append(",\n");
            if (_sim != null)
            {
                sb.Append("  \"sim_like\": {");
                sb.Append("\"dish_wait_mean\": ").Append(F(Mean(_sim.dishWaitMin))).Append(", ");
                sb.Append("\"dish_wait_p90\": ").Append(F(Percentile(_sim.dishWaitMin, 0.9f))).Append(", ");
                sb.Append("\"cold_share\": ").Append(F(Share(_sim.dishWaitMin, _sim.coldMin))).Append(", ");
                sb.Append("\"seat_wait_mean\": ").Append(F(Mean(_sim.seatWaitMin))).Append(", ");
                sb.Append("\"turn_mean\": ").Append(F(Mean(_sim.turnMin))).Append(", ");
                sb.Append("\"robot_share\": ").Append(F(_sim.robotDishes + _sim.staffDishes > 0
                        ? (float)_sim.robotDishes / (_sim.robotDishes + _sim.staffDishes) : 0f)).Append(", ");
                sb.Append("\"robot_dishes\": ").Append(_sim.robotDishes).Append(", ");
                sb.Append("\"staff_dishes\": ").Append(_sim.staffDishes).Append(", ");
                sb.Append("\"requeued_dishes\": ").Append(_sim.requeuedDishes).Append(", ");
                // 2026-10-27：往復の打ち切りが2回続いてスタッフに回した皿の数。
                // すれ違い「切」の回では 0 のはず（基準データ v3 に効かないことの確かめに使う）
                sb.Append("\"dishes_to_staff_after_timeout\": ")
                  .Append(_sim.dishesToStaffAfterTimeout).Append(", ");
                // サイトの試算 v2.7：2分（robot_first_wait_s）を過ぎて人が拾った皿
                sb.Append("\"late_staff_dishes\": ").Append(_sim.lateStaffDishes).Append(", ");
                sb.Append("\"late_staff_share\": ").Append(F(_sim.robotDishes + _sim.staffDishes > 0
                        ? (float)_sim.lateStaffDishes / (_sim.robotDishes + _sim.staffDishes) : 0f)).Append(", ");
                sb.Append("\"robot_first_wait_s\": ").Append(F(_sim.robotFirstWaitS)).Append(", ");
                // 譲る順番 v6：譲った回数（譲った側の種類ごと）と、角での止まり
                AppendYields(sb);
                AppendCornerStops(sb);
                sb.Append("\"groups\": ").Append(_sim.groups).Append(", ");
                sb.Append("\"people_jams\": ").Append(_sim.jamCount).Append(", ");
                sb.Append("\"robot_blocked_jams\": ").Append(_sim.robotBlockedJams).Append(", ");
                sb.Append("\"robot_blocked_jam_share\": ").Append(F(_sim.jamCount > 0
                        ? (float)_sim.robotBlockedJams / _sim.jamCount : 0f)).Append(", ");
                sb.Append("\"chair_pulls\": ").Append(_sim.chairPulls).Append(", ");
                sb.Append("\"robot_contacts\": ").Append(_sim.robotContacts).Append(", ");
                sb.Append("\"robot_yield_waits\": ").Append(_sim.robotYieldWaits).Append(", ");
                sb.Append("\"robot_yield_wait_s\": ").Append(F(_sim.robotYieldWaitSeconds)).Append(", ");
                sb.Append("\"robot_yield_detours\": ").Append(_sim.robotYieldDetours).Append(", ");
                // ロボットどうしのすれ違い（依頼 v13 の 依頼1）。切の回でも数える
                sb.Append("\"pass_count\": ").Append(_sim.passCount).Append(", ");
                sb.Append("\"pass_slow_s\": ").Append(F(_sim.passSlowS)).Append(", ");
                sb.Append("\"face_stop_count\": ").Append(_sim.faceStopCount).Append(", ");
                sb.Append("\"face_stop_max_s\": ").Append(F(_sim.faceStopMaxS)).Append(", ");
                // 2026-10-26（直し③）：場所で分けた数。
                // すれ違いの直しが効いたかは face_stop_in_aisle_* で見る
                sb.Append("\"face_stop_in_aisle_count\": ")
                  .Append(_sim.faceStopInAisleCount).Append(", ");
                sb.Append("\"face_stop_in_aisle_max_s\": ")
                  .Append(F(_sim.faceStopInAisleMaxS)).Append(", ");
                sb.Append("\"face_stop_at_pass_count\": ")
                  .Append(_sim.faceStopAtPassCount).Append(", ");
                sb.Append("\"face_stop_at_pass_max_s\": ")
                  .Append(F(_sim.faceStopAtPassMaxS)).Append(", ");
                sb.Append("\"face_stop_at_standby_count\": ")
                  .Append(_sim.faceStopAtStandbyCount).Append(", ");
                sb.Append("\"face_stop_at_standby_max_s\": ")
                  .Append(F(_sim.faceStopAtStandbyMaxS)).Append(", ");
                sb.Append("\"face_stop_overlap_count\": ")
                  .Append(_sim.faceStopOverlapCount).Append(", ");
                sb.Append("\"register_crowd_max\": ").Append(_sim.registerCrowdMax).Append(", ");
                sb.Append("\"register_crowd_mean\": ").Append(F(_sim.registerCrowdMean)).Append(", ");
                sb.Append("\"register_pay_count\": ").Append(_sim.registerPayCount).Append(", ");
                sb.Append("\"register_pay_not_at_back\": ").Append(_sim.registerPayNotAtBackCount).Append(", ");
                sb.Append("\"register_pay_off_spot\": ").Append(_sim.registerPayOffSpotCount).Append(", ");
                sb.Append("\"register_pay_offset_mean_m\": ").Append(F(_sim.registerPayMeanOffsetM)).Append(", ");
                sb.Append("\"register_pay_offset_max_m\": ").Append(F(_sim.registerPayMaxOffsetM)).Append(", ");
                sb.Append("\"register_back_intruders\": ").Append(_sim.registerBackIntruderCount).Append(", ");
                sb.Append("\"register_back_intruder_s\": ").Append(F(_sim.registerBackIntruderSeconds)).Append(", ");
                sb.Append("\"drink_bar_queued\": ").Append(_sim.drinkBarQueued).Append(", ");
                sb.Append("\"drink_bar_gave_up\": ").Append(_sim.drinkBarGaveUp).Append(", ");
                sb.Append("\"drink_bar_wait_mean\": ").Append(F(Mean(_sim.drinkBarWaitMin))).Append(", ");
                sb.Append("\"drink_bar_wait_p90\": ").Append(F(Percentile(_sim.drinkBarWaitMin, 0.9f))).Append(", ");
                sb.Append("\"gave_up\": ").Append(_sim.gaveUp).Append(", ");
                sb.Append("\"warped\": ").Append(_sim.warped).Append(", ");
                sb.Append("\"table_freed\": ").Append(_sim.tableFreed).Append(", ");
                sb.Append("\"people_jam_total_s\": ").Append(F(_sim.jamTotalS)).Append(", ");
                sb.Append("\"walkers_mean\": ").Append(F(_sim.walkersMean)).Append(", ");
                sb.Append("\"measured_minutes\": ").Append(F(_sim.measuredMinutes));
                sb.Append("},\n");

                AppendSiteMetrics(sb);

                // スタッフの時間の使い方（割合も出す）
                float staffTotal = 0f;
                for (int i = 0; i < _sim.staffTime.Count; i++) staffTotal += _sim.staffTime[i].Value;
                sb.Append("  \"staff_time_s\": {");
                for (int i = 0; i < _sim.staffTime.Count; i++)
                {
                    if (i > 0) sb.Append(", ");
                    sb.Append(Str(_sim.staffTime[i].Key)).Append(": ").Append(F(_sim.staffTime[i].Value));
                }
                sb.Append("},\n");
                // 料理ができてから届くまでの内訳（受け渡し口で待った／運んでいた／戻されて失った）
                sb.Append("  \"dish_wait_parts\": {");
                AppendDishParts(sb, "robot", true);
                sb.Append(", ");
                AppendDishParts(sb, "staff", false);
                sb.Append(", ");
                AppendDishParts(sb, "all", null);
                sb.Append("},\n");

                sb.Append("  \"job_wait_mean_s\": {");
                for (int i = 0; i < _sim.jobWaitMean.Count; i++)
                {
                    if (i > 0) sb.Append(", ");
                    sb.Append(Str(_sim.jobWaitMean[i].Key)).Append(": ").Append(F(_sim.jobWaitMean[i].Value));
                }
                sb.Append("},\n");

                // 依頼 v10 の 2：仕事が板に載ってから取られるまでの最大と、長びいた数。
                // 直す前は、片付け・会計の長い待ちが逃がし弁「卓を空けた」で消えていたので、
                // ここに出てくるようになるはず
                sb.Append("  \"job_wait_max_s\": {");
                for (int i = 0; i < _sim.jobWaitMax.Count; i++)
                {
                    if (i > 0) sb.Append(", ");
                    sb.Append(Str(_sim.jobWaitMax[i].Key)).Append(": ").Append(F(_sim.jobWaitMax[i].Value));
                }
                sb.Append("},\n");
                sb.Append("  \"job_wait_over_count\": {");
                for (int i = 0; i < _sim.jobWaitOver.Count; i++)
                {
                    if (i > 0) sb.Append(", ");
                    sb.Append(Str(_sim.jobWaitOver[i].Key)).Append(": ").Append(_sim.jobWaitOver[i].Value);
                }
                sb.Append("},\n");
                sb.Append("  \"job_wait_note\": {");
                sb.Append("\"over_s\": ").Append(F(_sim.jobWaitOverSec)).Append(", ");
                sb.Append("\"pay_wait_from_pay_start_mean\": ")
                  .Append(F(Mean(_sim.payWaitFromStartMin))).Append(", ");
                sb.Append("\"note\": ").Append(Str(
                    "job_wait_* は、仕事が板に載ってからスタッフが取るまでの秒数。" +
                    "取られないまま終わった仕事（卓を空けたときに板から外した分）は入らない。" +
                    "あきらめて板に戻った仕事は、取られるたびに1回ずつ数える。" +
                    "over_s は長びいたと数える境目で、tableStuckFreeSec と同じ。" +
                    "pay_wait_from_pay_start_mean は pay_wait_mean を" +
                    "食べ終えた時刻ではなく会計に入った時刻から数えたもの（分）"));
                sb.Append("},\n");
                sb.Append("  \"staff_time_share\": {");
                for (int i = 0; i < _sim.staffTime.Count; i++)
                {
                    if (i > 0) sb.Append(", ");
                    sb.Append(Str(_sim.staffTime[i].Key)).Append(": ")
                      .Append(F(staffTotal > 0.01f ? _sim.staffTime[i].Value / staffTotal : 0f));
                }
                sb.Append("},\n");
            }

            // 止まり全体のまとめ（B7 の前後を並べるため）
            sb.Append("  \"stops\": {");
            sb.Append("\"count\": ").Append(_stops.Count).Append(", ");
            sb.Append("\"stuck_count\": ").Append(StuckStopCount()).Append(", ");
            sb.Append("\"partial_path_count\": ").Append(PartialPathStopCount()).Append(", ");
            sb.Append("\"stop_s_total\": ").Append(F(StopSecondsTotal()));
            sb.Append("},\n");

            // 往復のまとめ（段階Bの台帳が写す）
            AppendTripSummary(sb);

            sb.Append("  \"stop_causes\": [\n");
            AppendCauseSummary(sb);
            sb.Append("  ],\n");

            sb.Append("  \"conditions\": [\n");

            for (int i = 0; i < order.Count; i++)
            {
                List<Row> rows = byCondition[order[i]];
                AppendConditionSummary(sb, order[i], rows);
                if (i < order.Count - 1) sb.Append(',');
                sb.Append('\n');
            }

            sb.Append("  ]\n");
            sb.Append("}\n");
            File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        /// <summary>
        /// サイトと同じ名前・同じ定義で出す欄（v5 3章・meta.metrics）。
        /// site_result と並べるときは、この欄だけを見れば足りるようにする。
        ///
        /// 入店待ちの分け方は、サイトの試算（haizen_sim.py）と同じ:
        ///   seat_wait    店に来てから案内が始まるまで
        ///   blocked_wait そのうち、卓は空いたのに片付けが終わらず待った分
        ///                （片付け待ちの卓の数だけ、待っている列の前から順に足す。サイトの _acc と同じ）
        ///   full_wait    seat_wait − blocked_wait
        ///   guide_wait   案内の仕事が板に出てから、スタッフが取るまで
        ///   staff_wait   blocked_wait ＋ guide_wait
        ///   entry_wait   店に来てから卓に着くまで（案内の歩きと guide_s を含む）
        ///
        /// pay_wait_mean は v2.8 で足された欄（依頼 v10 の 2）:
        ///   組ごとに（会計が終わった時刻 − 食べ終えた時刻 − pay_s）の平均（分）。
        ///   逃がし弁で卓を空けられた組と、会計が終わっていない組は入れない。
        ///   pay_wait_groups は、その平均に入れた組の数
        /// </summary>
        void AppendSiteMetrics(StringBuilder sb)
        {
            sb.Append("  \"site_metrics\": {");
            sb.Append("\"entry_wait_mean\": ").Append(F(Mean(_sim.entryWaitMin))).Append(", ");
            sb.Append("\"full_wait_mean\": ").Append(F(Mean(_sim.fullWaitMin))).Append(", ");
            sb.Append("\"staff_wait_mean\": ").Append(F(Mean(_sim.staffWaitMin))).Append(", ");
            sb.Append("\"blocked_wait_mean\": ").Append(F(Mean(_sim.blockedWaitMin))).Append(", ");
            sb.Append("\"guide_wait_mean\": ").Append(F(Mean(_sim.guideWaitMin))).Append(", ");
            sb.Append("\"seat_wait_mean\": ").Append(F(Mean(_sim.seatWaitMin))).Append(", ");
            sb.Append("\"dish_wait_mean\": ").Append(F(Mean(_sim.dishWaitMin))).Append(", ");
            sb.Append("\"dish_wait_p90\": ").Append(F(Percentile(_sim.dishWaitMin, 0.9f))).Append(", ");
            sb.Append("\"cold_share\": ").Append(F(Share(_sim.dishWaitMin, _sim.coldMin))).Append(", ");
            sb.Append("\"turn_mean\": ").Append(F(Mean(_sim.turnMin))).Append(", ");
            // 依頼 v10 の 2：食べ終えてから会計が始まるまで（分）。
            // Unity はレジまで歩く時間が入るので、試算より少し長く出てよい
            sb.Append("\"pay_wait_mean\": ").Append(F(Mean(_sim.payWaitMin))).Append(", ");
            sb.Append("\"pay_wait_groups\": ").Append(_sim.payWaitMin.Count).Append(", ");
            sb.Append("\"walk_km_per_staff\": ").Append(F(_sim.walkKmPerStaff)).Append(", ");
            sb.Append("\"staff_util\": ").Append(F(_sim.staffUtil)).Append(", ");
            sb.Append("\"robot_util\": ").Append(F(_sim.robotUtil)).Append(", ");
            sb.Append("\"robot_share\": ").Append(F(_sim.robotDishes + _sim.staffDishes > 0
                    ? (float)_sim.robotDishes / (_sim.robotDishes + _sim.staffDishes) : 0f)).Append(", ");
            sb.Append("\"rescues\": ").Append(_sim.rescues).Append(", ");
            sb.Append("\"pauses\": ").Append(_sim.pauses).Append(", ");
            sb.Append("\"groups\": ").Append(_sim.groups);
            sb.Append("},\n");

            sb.Append("  \"site_run\": {");
            sb.Append("\"site_run\": ").Append(_sim.siteRun ? "true" : "false").Append(", ");
            sb.Append("\"condition_id\": ").Append(Str(_sim.siteConditionId)).Append(", ");
            sb.Append("\"arrivals_minutes\": ").Append(F(_sim.arrivalsMinutes)).Append(", ");
            sb.Append("\"total_minutes\": ").Append(F(_sim.totalMinutes)).Append(", ");
            sb.Append("\"everyone_left\": ").Append(_sim.everyoneLeft ? "true" : "false").Append(", ");
            sb.Append("\"aisle_deferrals\": ").Append(_sim.aisleDeferrals).Append(", ");
            sb.Append("\"staff_first_yield\": ").Append(_sim.staffFirstYield ? "true" : "false").Append(", ");
            sb.Append("\"robot_stuck_detours\": ").Append(_sim.robotStuckDetours).Append(", ");
            sb.Append("\"rescues_done\": ").Append(_sim.rescuesDone).Append(", ");
            sb.Append("\"rescue_wait_s\": ").Append(F(_sim.rescueWaitS)).Append(", ");
            // 逃がし弁（0 回に近いことを確かめる。仕組みは消さない）
            sb.Append("\"gave_up\": ").Append(_sim.gaveUp).Append(", ");
            sb.Append("\"warped\": ").Append(_sim.warped).Append(", ");
            sb.Append("\"table_freed\": ").Append(_sim.tableFreed);
            sb.Append("},\n");

            // 時間帯ごとの記録の確かめ（依頼 v10b の 3 A）。
            // bad_table_rows・bad_staff_rows はどちらも 0 になるはず
            sb.Append("  \"timeline_check\": {");
            sb.Append("\"rows\": ").Append(_sim.timelineRows).Append(", ");
            sb.Append("\"step_s\": 60, ");
            sb.Append("\"tables\": ").Append(_sim.timelineTables).Append(", ");
            sb.Append("\"staff\": ").Append(_sim.timelineStaff).Append(", ");
            sb.Append("\"bad_table_rows\": ").Append(_sim.timelineBadTableRows).Append(", ");
            sb.Append("\"bad_staff_rows\": ").Append(_sim.timelineBadStaffRows).Append(", ");
            sb.Append("\"first_bad\": ").Append(Str(_sim.timelineFirstBad)).Append(", ");
            sb.Append("\"note\": ").Append(Str(
                "timeline.csv の各行で、seated_groups + tables_free + tables_dirty_untaken + " +
                "tables_clearing が tables と同じか、staff_* の合計が staff と同じかを数えたもの。" +
                "合わない行があれば bad_* が 0 より大きくなり、Console にも出る"));
            sb.Append("},\n");

            // サイトの試算の答え（並べる表のもと）
            sb.Append("  \"site_result\": {");
            for (int i = 0; i < _sim.siteResult.Count; i++)
            {
                if (i > 0) sb.Append(", ");
                sb.Append(Str(_sim.siteResult[i].Key)).Append(": ").Append(F(_sim.siteResult[i].Value));
            }
            sb.Append("},\n");
        }

        /// <summary>
        /// 原因ごとの回数と秒数の合計。
        /// stuck_count は「動けなくなった」で終わった止まり（止まりの上限に達した回数）。
        /// partial_path_count は「途中までの経路」に縛られて引き直して出た止まり（B7）。
        /// この2つを、停止点を変えた2回で並べれば「椅子が原因で上限に達した回数」の差が出る。
        /// </summary>
        void AppendCauseSummary(StringBuilder sb)
        {
            string stuckLabel = ResolveLabel(ServingRobotController.StopResolve.Stuck);

            List<string> order = new List<string>();
            Dictionary<string, int> count = new Dictionary<string, int>();
            Dictionary<string, float> secs = new Dictionary<string, float>();
            Dictionary<string, float> longest = new Dictionary<string, float>();
            Dictionary<string, int> blocked = new Dictionary<string, int>();
            Dictionary<string, int> stuck = new Dictionary<string, int>();
            Dictionary<string, int> partial = new Dictionary<string, int>();
            Dictionary<string, float> noPath = new Dictionary<string, float>();

            for (int i = 0; i < _stops.Count; i++)
            {
                string c = _stops[i].cause;
                if (!count.ContainsKey(c))
                {
                    order.Add(c); count[c] = 0; secs[c] = 0f; longest[c] = 0f; blocked[c] = 0;
                    stuck[c] = 0; partial[c] = 0; noPath[c] = 0f;
                }
                count[c]++;
                secs[c] += _stops[i].durationS;
                if (_stops[i].durationS > longest[c]) longest[c] = _stops[i].durationS;
                if (_stops[i].pathBlocked) blocked[c]++;
                if (_stops[i].resolved == stuckLabel) stuck[c]++;
                if (HasPartialPathMark(_stops[i].causeDetail)) partial[c]++;
                noPath[c] += _stops[i].noPathS;
            }

            for (int i = 0; i < order.Count; i++)
            {
                string c = order[i];
                sb.Append("    {\"cause\": ").Append(Str(c))
                  .Append(", \"count\": ").Append(count[c])
                  .Append(", \"total_s\": ").Append(F(secs[c]))
                  .Append(", \"mean_s\": ").Append(F(count[c] > 0 ? secs[c] / count[c] : 0f))
                  .Append(", \"longest_s\": ").Append(F(longest[c]))
                  .Append(", \"path_blocked_count\": ").Append(blocked[c])
                  .Append(", \"stuck_count\": ").Append(stuck[c])
                  .Append(", \"partial_path_count\": ").Append(partial[c])
                  .Append(", \"no_path_total_s\": ").Append(F(noPath[c]))
                  .Append('}');
                if (i < order.Count - 1) sb.Append(',');
                sb.Append('\n');
            }
        }

        /// <summary>
        /// 届くまでの内訳を1つぶん書く。byRobot が null なら全部。
        /// 秒は「分」に直して出す（dish_wait_mean と同じ単位にそろえる）。
        /// </summary>
        void AppendDishParts(StringBuilder sb, string name, bool? byRobot)
        {
            int n = 0, requeued = 0;
            float pass = 0f, carry = 0f, requeue = 0f;

            if (_sim != null)
            {
                for (int i = 0; i < _sim.dishParts.Count; i++)
                {
                    ServiceOps.DishParts p = _sim.dishParts[i];
                    if (byRobot.HasValue && p.byRobot != byRobot.Value) continue;
                    n++;
                    pass += p.passWaitS;
                    carry += p.carryS;
                    requeue += p.requeueS;
                    requeued += p.requeued;
                }
            }

            float d = n > 0 ? n : 1f;
            sb.Append(Str(name)).Append(": {");
            sb.Append("\"dishes\": ").Append(n).Append(", ");
            sb.Append("\"pass_wait_mean_min\": ").Append(F(pass / d / 60f)).Append(", ");
            sb.Append("\"carry_mean_min\": ").Append(F(carry / d / 60f)).Append(", ");
            sb.Append("\"requeue_lost_mean_min\": ").Append(F(requeue / d / 60f)).Append(", ");
            sb.Append("\"total_mean_min\": ").Append(F((pass + carry + requeue) / d / 60f)).Append(", ");
            sb.Append("\"requeued_per_dish\": ").Append(F(requeued / d)).Append(", ");
            sb.Append("\"requeued_total\": ").Append(requeued);
            sb.Append("}");
        }

        /// <summary>この止まりが「途中までの経路」に縛られたものか（B7）</summary>
        static bool HasPartialPathMark(string causeDetail)
        {
            return !string.IsNullOrEmpty(causeDetail) &&
                   causeDetail.Contains(ServingRobotController.PartialPathMark);
        }

        /// <summary>「動けなくなった」で終わった止まりの数（止まりの上限に達した回数）</summary>
        int StuckStopCount()
        {
            string label = ResolveLabel(ServingRobotController.StopResolve.Stuck);
            int n = 0;
            for (int i = 0; i < _stops.Count; i++) if (_stops[i].resolved == label) n++;
            return n;
        }

        /// <summary>「途中までの経路」から引き直して出た止まりの数（B7）</summary>
        int PartialPathStopCount()
        {
            int n = 0;
            for (int i = 0; i < _stops.Count; i++) if (HasPartialPathMark(_stops[i].causeDetail)) n++;
            return n;
        }

        static void AppendConditionSummary(StringBuilder sb, string id, List<Row> rows)
        {
            List<float> speeds = new List<float>();
            List<float> stopped = new List<float>();
            List<float> pauses = new List<float>();
            List<float> waits = new List<float>();
            int stuckCount = 0, timeoutCount = 0, pausedCount = 0, metRobotCount = 0;
            float aisleWidth = 0f; int robots = 0; string crowd = "";

            for (int i = 0; i < rows.Count; i++)
            {
                Row r = rows[i];
                aisleWidth = r.aisleWidthM; robots = r.robots; crowd = r.crowd;

                float moving = r.totalS - r.stoppedS;
                if (moving > 0.01f) speeds.Add(r.distanceM / moving);
                stopped.Add(r.stoppedS);
                waits.Add(r.waitAisleS);
                if (r.stopDurationsS != null)
                    for (int k = 0; k < r.stopDurationsS.Length; k++) pauses.Add(r.stopDurationsS[k]);
                if (r.stuck) stuckCount++;
                if (r.timeout) timeoutCount++;
                if (r.nStops >= 1) pausedCount++;
                if (r.metRobot) metRobotCount++;
            }

            int n = rows.Count;
            sb.Append("    {");
            sb.Append("\"condition_id\": ").Append(Str(id)).Append(", ");
            sb.Append("\"aisle_width_m\": ").Append(F(aisleWidth)).Append(", ");
            sb.Append("\"crowd\": ").Append(Str(crowd)).Append(", ");
            sb.Append("\"robots\": ").Append(robots).Append(", ");
            sb.Append("\"trips\": ").Append(n).Append(", ");
            sb.Append("\"robot_speed_mps_median\": ").Append(F(Percentile(speeds, 0.5f))).Append(", ");
            sb.Append("\"stopped_s_median\": ").Append(F(Percentile(stopped, 0.5f))).Append(", ");
            sb.Append("\"stopped_s_p90\": ").Append(F(Percentile(stopped, 0.9f))).Append(", ");
            sb.Append("\"pause_s_median\": ").Append(F(Percentile(pauses, 0.5f))).Append(", ");
            sb.Append("\"pause_s_p90\": ").Append(F(Percentile(pauses, 0.9f))).Append(", ");
            sb.Append("\"pause_count\": ").Append(pauses.Count).Append(", ");
            sb.Append("\"wait_aisle_s_median\": ").Append(F(Percentile(waits, 0.5f))).Append(", ");
            sb.Append("\"stuck_rate\": ").Append(F(n > 0 ? stuckCount / (float)n : 0f)).Append(", ");
            sb.Append("\"stuck_count\": ").Append(stuckCount).Append(", ");
            sb.Append("\"timeout_rate\": ").Append(F(n > 0 ? timeoutCount / (float)n : 0f)).Append(", ");
            sb.Append("\"timeout_count\": ").Append(timeoutCount).Append(", ");
            sb.Append("\"pause_rate\": ").Append(F(n > 0 ? pausedCount / (float)n : 0f)).Append(", ");
            sb.Append("\"met_robot_rate\": ").Append(F(n > 0 ? metRobotCount / (float)n : 0f));
            sb.Append('}');
        }

        static float Mean(List<float> v)
        {
            if (v == null || v.Count == 0) return -1f;
            float s = 0f;
            for (int i = 0; i < v.Count; i++) s += v[i];
            return s / v.Count;
        }

        static float Share(List<float> v, float over)
        {
            if (v == null || v.Count == 0) return -1f;
            int n = 0;
            for (int i = 0; i < v.Count; i++) if (v[i] > over) n++;
            return (float)n / v.Count;
        }

        /// <summary>小さい順に並べて p の位置の値を返す（0〜1）。空なら -1</summary>
        public static float Percentile(List<float> values, float p)
        {
            if (values == null || values.Count == 0) return -1f;
            List<float> v = new List<float>(values);
            v.Sort();
            if (v.Count == 1) return v[0];
            float pos = Mathf.Clamp01(p) * (v.Count - 1);
            int lo = Mathf.FloorToInt(pos);
            int hi = Mathf.Min(lo + 1, v.Count - 1);
            float t = pos - lo;
            return Mathf.Lerp(v[lo], v[hi], t);
        }
    }
}
```

### Assets/ServiceSim/Experiment/RobotTrackLogger.cs

```csharp
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>
    /// ロボット1台ずつの位置と状態を、**店の中の時間で5秒おきに** 残す
    /// （2026-10-27・エイイチの指示）。出力は robot_track.csv。
    ///
    /// なぜ要るか
    ///   10/27 の夜の 1回（③2台・すれ違い入・種0）が 720 分終わらなかった。
    ///   ロボットが皿を持ったまま 600 秒の打ち切りを 56 回くり返したのだが、
    ///   **そのときロボットがどこで何をしていたかが記録に無かった**。
    ///   stops.csv は「止まった」ときだけ、trips.csv は往復ごとなので、
    ///   「ずっと同じ所に居た」ことが後から言えなかった。
    ///
    /// なぜ 5 秒おき・店の中の時間なのか
    ///   ・1コマ（0.05 秒）ごとに書くと、書く重さで Mac の忙しさが変わり、
    ///     数字そのものが動いてしまう（busycheck で、忙しさが
    ///     stuck_count・robot_share を動かすと分かっている）。
    ///   ・刻みを **ops.NowS（店の中の時間）** で決めるので、
    ///     実時間が速くても遅くても同じ行が出る（取り違えない）。
    ///   ・5 秒おきなら 1回（店の中 200 分）で 2 台 × 2400 = 4800 行ほど。
    ///     上限の 720 分でも 17,280 行で、軽い。
    ///
    /// det_frames.csv（1コマごと・位置の合計）との違い
    ///   det_frames は切り分けの試し用で、ロボット位置を **合計値** でしか
    ///   持たないので1台ずつ追えない。こちらは常に書き、1台ずつ追える。
    /// </summary>
    public class RobotTrackLogger
    {
        public struct Row
        {
            public float tS;
            public int robotId;
            public float x, z;
            public string phase;      // いまの仕事（卓へ / 受け渡し口へ / 立ち往生 など）
            public bool passBy;       // すれ違いの走り方で動いている
            public bool backingOff;   // すれ違いをあきらめて後ずさりしている
            public bool narrow;       // すれ違う幅が足りないと見た
            public int tableId;       // 向かっている卓（無ければ -1）
        }

        readonly List<Row> _rows = new List<Row>();

        public int RowCount { get { return _rows.Count; } }
        public IReadOnlyList<Row> Rows { get { return _rows; } }
        public void Clear() { _rows.Clear(); }

        /// <summary>いまの仕事を日本語で（trips.csv などと言葉をそろえる）</summary>
        public static string PhaseName(ServingRobotController.Phase p)
        {
            switch (p)
            {
                case ServingRobotController.Phase.Idle: return "待機";
                case ServingRobotController.Phase.ToTable: return "卓へ";
                case ServingRobotController.Phase.Dwell: return "卓で止まる";
                case ServingRobotController.Phase.ToPass: return "受け渡し口へ";
                case ServingRobotController.Phase.StuckWait: return "立ち往生";
                case ServingRobotController.Phase.Done: return "往復おわり";
            }
            return "分からない";
        }

        /// <summary>
        /// その時刻のロボットを1台ずつ1行に。
        /// null の枠は飛ばす（ロボット0台の条件では1行も出ない）
        /// </summary>
        public void Sample(float tS, IList<ServingRobotController> robots)
        {
            if (robots == null) return;
            for (int i = 0; i < robots.Count; i++)
            {
                ServingRobotController r = robots[i];
                if (r == null) continue;
                Vector3 p = r.transform.position;
                Row row = new Row();
                row.tS = tS;
                row.robotId = r.robotId;
                row.x = p.x;
                row.z = p.z;
                row.phase = PhaseName(r.CurrentPhase);
                row.passBy = r.PassByMoving;
                row.backingOff = r.PassByBackingOff;
                row.narrow = r.PassByNarrow;
                row.tableId = r.CurrentPhase == ServingRobotController.Phase.Idle ? -1 : r.TableId;
                _rows.Add(row);
            }
        }

        public const string Header =
            "condition_id,seed,t_s,robot_id,x,z,phase,pass_by,backing_off,narrow,table_id";

        public void Write(string path, string conditionId, int seed)
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(Header).Append('\n');
            for (int i = 0; i < _rows.Count; i++)
            {
                Row r = _rows[i];
                sb.Append(conditionId).Append(',').Append(seed.ToString(CultureInfo.InvariantCulture)).Append(',')
                  .Append(F(r.tS)).Append(',')
                  .Append(r.robotId.ToString(CultureInfo.InvariantCulture)).Append(',')
                  .Append(F(r.x)).Append(',').Append(F(r.z)).Append(',')
                  .Append(r.phase).Append(',')
                  .Append(r.passBy ? 1 : 0).Append(',')
                  .Append(r.backingOff ? 1 : 0).Append(',')
                  .Append(r.narrow ? 1 : 0).Append(',')
                  .Append(r.tableId.ToString(CultureInfo.InvariantCulture)).Append('\n');
            }
            System.IO.File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
        }

        static string F(float v)
        {
            return v.ToString("0.####", CultureInfo.InvariantCulture);
        }
    }
}
```

### Assets/ServiceSim/Robot/RobotSpec.cs

```csharp
using UnityEngine;

namespace ServiceSim
{
    /// <summary>
    /// 道の決め方（依頼 v9 の 2 章）。
    ///   MapPlanner … ロボットが地図（サイトの {名前}_map.json）の上で自分で道を引く。NavMesh は使わない
    ///   SiteRoutes … サイトの routes の点を順にたどる（v5b〜v8b と同じ。比べるために残す）
    /// </summary>
    public enum PathMode
    {
        MapPlanner,
        SiteRoutes
    }

    /// <summary>
    /// 配膳ロボットの数字。ロボットに関する数字はすべてこの .asset に置く。
    /// </summary>
    [CreateAssetMenu(menuName = "ServiceSim/Robot Spec", fileName = "RobotSpec")]
    public class RobotSpec : ScriptableObject
    {
        [Header("大きさ")]
        [Tooltip("直径（m）")]
        public float diameter = 0.5f;

        [Tooltip("高さ（m）")]
        public float height = 1.2f;

        [Header("走り")]
        [Tooltip("最高速度（m/秒）")]
        public float maxSpeed = 0.6f;

        [Tooltip("加速（m/秒^2）")]
        public float acceleration = 1.0f;

        [Tooltip("減速（m/秒^2）")]
        public float deceleration = 1.0f;

        [Tooltip("向きを変える速さ（度/秒）。仮の値には無かったので追加した")]
        public float turnSpeedDegPerSec = 180f;

        [Header("前のセンサー")]
        [Tooltip("進む向きの前、どこまで見るか（m）")]
        public float sensorRange = 0.6f;

        [Tooltip("センサーの幅（m）。ロボットの直径に合わせる")]
        public float sensorWidth = 0.5f;

        [Tooltip("センサーの高さ（m）")]
        public float sensorHeight = 1.2f;

        [Header("止まる・動き出す")]
        [Tooltip("前がこの秒数だけ空いたら動き出す（秒）")]
        public float resumeAfterClearSec = 1.0f;

        [Tooltip("止まってこの秒数たったら経路を引き直す（秒）")]
        public float replanAfterStoppedSec = 10f;

        [Tooltip("この秒数動けなかったら「動けなくなった」と記録する（秒）")]
        public float stuckAfterStoppedSec = 60f;

        [Tooltip("使っていない（v5 3章で、受け渡し口へ移すのをやめた）。" +
                 "動けなくなったらスタッフを呼び、ServiceSpec.rescueSec（サイトの rescue_s）動かしてもらう")]
        public float stuckRecoveryWaitSec = 15f;

        [Tooltip("1往復がこの秒数を超えたら、受け渡し口へ置き直して往復を打ち切る（秒）。無限に止まらないための保険")]
        public float maxTripSec = 600f;

        [Header("通路の譲り合い")]
        [Tooltip("行き先の細い通路にもう1台のロボットがいたら、太い通路側の入口で待つ。場面2の撮影では切る")]
        public bool aisleYielding = true;

        [Header("台どうしの譲り合い（Unity だけの仮の値。サイトには無い）")]
        [Tooltip("入口で順番を待つ上限（秒）。これを過ぎたら譲らずに進む。" +
                 "人がロボットを待つ上限（PeopleSpec.robotYieldMaxSec 20 秒）と同じ考え方")]
        public float aisleWaitMaxSec = 20f;

        [Tooltip("入口で待つとき、入口の点から通路の外へ下がる距離（m）。" +
                 "入口の点そのものに立つと、中の台が出られなくなる")]
        public float aisleWaitSetbackM = 0.9f;

        [Tooltip("入口で待つとき、中の台が出てくる道をふさがないよう横へずれる距離（m）")]
        public float aisleWaitSideM = 0.6f;

        [Tooltip("もう1台にふさがれてから、譲り始めるまでの秒数（秒）。" +
                 "すれ違うだけで譲り合わないよう、少し待ってから決める")]
        public float robotYieldAfterSec = 2f;

        [Tooltip("譲って下がったあと、相手を待つ上限（秒）。これを過ぎたら戻って進む")]
        public float robotYieldMaxSec = 20f;

        [Tooltip("相手が通り過ぎたとみなす、外周どうしの空き（m）")]
        public float robotYieldClearM = 0.6f;

        [Tooltip("下がる先が見つからないときに、その場から後ろへ下がる距離（m）")]
        public float robotYieldBackM = 0.9f;

        [Header("受け渡し口の前の通り方（Unity だけの決まり。サイトには無い）")]
        [Tooltip("戻る台が、受け渡し口の前の点を通らず、その少し下を通って右の待機場所へ抜けるか。" +
                 "線が通れる床に乗らないときは、使わずに今までどおり受け渡し口の前へ戻る")]
        public bool useReturnLine = true;

        [Tooltip("戻りの線を、受け渡し口の前の点より何 m 下（−z）に通すか")]
        public float passReturnOffsetM = 1.0f;

        [Tooltip("戻りの線を、出ていく台の線（受け渡し口の前の点を通る線）から" +
                 "中心どうしで何 m 以上離すか")]
        public float passReturnSeparationM = 1.0f;

        [Tooltip("戻りの線を、ドリンクバーの並ぶ点から何 m 以上離すか")]
        public float passReturnKeepAwayM = 0.6f;

        [Tooltip("戻りの線が通れる床に乗っているかを調べる刻み（m）")]
        public float passReturnCheckStepM = 0.05f;

        [Header("同じ通路に2台を集めない（Unity だけの決まり。サイトには無い）")]
        [Tooltip("料理を受け持つとき、ほかの台が向かっている・中にいる細い通路（店の形の aisles）の" +
                 "卓を後に回すか。後に回せる料理が無ければ、通路の入口ではなく待機場所で待つ。" +
                 "外すと、これまでどおり近い卓から順に受け持つ")]
        public bool oneRobotPerAisle = true;

        [Header("譲る順番と、道から横へどく（Unity だけの仮の値。サイトには無い）")]
        [Tooltip("譲る順番の決まりを使うか。外すと、前のとおり互いに相手が動くのを待つ")]
        public bool yieldOrderEnabled = true;

        [Tooltip("譲る側が、相手の道（サイトの routes の線）から横へどく最小の距離（m）")]
        public float yieldSideClearM = 0.6f;

        [Tooltip("どく点を探す広さ（m）。相手の道から横へこの距離まで探す")]
        public float yieldSideSearchM = 2.5f;

        [Tooltip("どく点を探す刻み（m）")]
        public float yieldSideStepM = 0.2f;

        [Tooltip("どいて相手を待つ上限（秒）。過ぎたら元の道へ戻って進む")]
        public float yieldSideWaitMaxSec = 30f;

        [Tooltip("スタッフが譲るかを決めるとき、ロボットの「これから進む道」" +
                 "（サイトの routes の線の、ロボットの今の位置から先）を何 m 分見るか。" +
                 "この中に、横 yieldSideClearM＋体の半径 以内で入っているときだけ譲る。" +
                 "ただのすれ違い（近くにいるだけ）では譲らない（v6b）")]
        public float yieldPathAheadM = 2.0f;

        [Header("角の止まりを数える範囲（Unity だけの仮の値。記録だけ）")]
        [Tooltip("受け渡し口と客席をつなぐ角。x の下限（m）")]
        public float cornerMinX = 10.5f;

        [Tooltip("同じく x の上限（m）")]
        public float cornerMaxX = 13.5f;

        [Tooltip("同じく z の下限（m）")]
        public float cornerMinZ = 3.5f;

        [Tooltip("同じく z の上限（m）")]
        public float cornerMaxZ = 5.8f;

        [Header("卓での止まり")]
        [Tooltip("卓に着いたら止まる秒数（秒）。客が料理を取る時間。「止まった」には数えない")]
        public float tableDwellSec = 25f;

        [Header("経路をたどる（仮の値には無かったので追加した）")]
        [Tooltip("行き先にこれだけ近づいたら着いたとみなす（m）")]
        public float arriveRadius = 0.25f;

        [Tooltip("経路の角にこれだけ近づいたら次の角へ進む（m）")]
        public float cornerReachDistance = 0.12f;

        [Tooltip("これより速度が小さければ「止まっている」とみなす（m/秒）")]
        public float stoppedSpeedEpsilon = 0.02f;

        [Header("止まったときに測る左右の空き")]
        [Tooltip("左右を見る高さ（m）。椅子（0.45 m）に当たる高さにする")]
        public float clearanceProbeHeight = 0.3f;

        [Tooltip("左右を見る距離（m）。これより遠いものは見ない")]
        public float clearanceProbeRange = 3.0f;

        [Tooltip("道が引けないとき、ふさいでいるものを探す半径（m）")]
        public float pathBlockSearchRadius = 2.0f;

        [Tooltip("NavMeshAgent の譲り合いの優先度。小さいほど譲らない。人より小さくして人がよけるようにする")]
        public int avoidancePriority = 0;

        [Tooltip("経路を引き直しても見つからないとき、次に試すまでの間（秒）")]
        public float repathRetrySec = 1.0f;

        [Header("途中までの経路から出る（B7）")]
        [Tooltip("止まっているあいだの「道が引けていなかった秒数（no_path_s）」がこれを超えたら、" +
                 "repathRetrySec を待たずに強く引き直す（秒）")]
        public float noPathForceReplanSec = 10f;

        [Tooltip("強く引き直してもまだ途中までしか引けないとき、いちど通路の入口を目指して出る")]
        public bool escapeToAisleEntranceOnPartialPath = true;

        public float Radius { get { return diameter * 0.5f; } }

        // ---- 譲る順番（1か所にまとめる）----

        /// <summary>
        /// 譲る順番。**小さいほど先に通る**（譲られる側）。
        ///   ロボット（0）＞ スタッフ（1）＞ 客（2）
        ///
        /// 立ち往生を助けに行くスタッフは、この順番の外（PersonRobotYield.ignoreRobot）。
        /// その台には近づかないと仕事ができないので、今までどおりよけない。
        /// </summary>
        public const int RankRobot = 0;
        public const int RankStaff = 1;
        public const int RankGuest = 2;

        /// <summary>その人の順番</summary>
        public static int RankOf(PersonKind kind)
        {
            return kind == PersonKind.Staff ? RankStaff : RankGuest;
        }

        /// <summary>その人はロボットに譲るか（順番が後ろなら譲る）</summary>
        public static bool YieldsToRobot(PersonKind kind)
        {
            return RankOf(kind) > RankRobot;
        }

        /// <summary>受け渡し口と客席をつなぐ角の中か（記録のため）</summary>
        public bool InCorner(Vector3 p)
        {
            return p.x >= cornerMinX && p.x <= cornerMaxX &&
                   p.z >= cornerMinZ && p.z <= cornerMaxZ;
        }
        [Header("道順（v5b・サイトから受け取る）")]
        [Tooltip("サイトの routes（曲がり角の点の並び）をたどるか。" +
                 "外すと、今までどおり行き先まで Unity の道探しで直接行く（比べるとき用）")]
        public bool followSiteRoutes = true;

        [Tooltip("道順の点に着いたとみなす距離（m）。最後の点（止まる位置）だけは arriveRadius を使う")]
        public float waypointReachDistance = 0.25f;

        [Tooltip("道順の最後の点（止まる位置）を、焼いた面へ寄せるときに探す半径（m）。" +
                 "サイトが通れる所に置いているので、寄せない（0.1 m 以下）。" +
                 "この半径で面に乗らなければ広げて引き直し、報告に回数を出す")]
        public float routePointSnapM = 0.1f;

        // ================= 依頼 v9：地図の上で自分で道を引く =================

        [Header("道の決め方（依頼 v9 の 2 章）")]
        [Tooltip("MapPlanner … 地図の上でロボットが自分で道を引く（新しい・既定）。" +
                 "SiteRoutes … サイトの routes の点をたどる（v8b までと同じ。比べるとき用）。" +
                 "戻りの線（useReturnLine）・横へどく点（yieldOrderEnabled）・角の箱（cornerMin/Max）は " +
                 "SiteRoutes のときだけ働く")]
        public PathMode pathMode = PathMode.MapPlanner;

        [Tooltip("通れる幅の道だけのマス（free_class 1）を通るときの重さの倍。" +
                 "狭い所は必要な所だけ通らせるため（目安 3 倍）")]
        public float narrowCellCost = 3f;

        [Tooltip("通路の真ん中寄りを走らせる強さ。1 歩の重さに " +
                 "centerWeight × max(0, preferClearM − clear) / preferClearM を足す（エイイチの決定）")]
        public float centerWeight = 2f;

        [Tooltip("障害物からこれだけ離れていれば、真ん中寄りの重さを足さない（m）。" +
                 "1.2 m の通路ならちょうど真ん中（エイイチの決定）")]
        public float preferClearM = 0.6f;

        [Tooltip("道を引き直すのは、1 台につきこの秒数に 1 回まで（秒）")]
        public float replanMinIntervalSec = 0.5f;

        [Tooltip("1 回の道探しで広げるマスの数の上限。超えたら引けないことにする。" +
                 "いつまでも探して回が止まらないようにするための保険")]
        public int planMaxExpand = 200000;

        [Tooltip("ロボットがいま立っている所が地図では通れないマス（0）のとき、" +
                 "近くの通れるマスを探す半径（m）。" +
                 "NavMesh は体の半径 0.25 m で焼いてあるのに free_class は道の幅で削ってあるので、" +
                 "通路の入口で待つ点・横へどいた点は 0 になる。地図は塗り直さず、出発点だけ寄せる")]
        public float mapSnapM = 1.0f;

        [Tooltip("曲がり角だけに縮めるとき、1 本の近道の長さの上限（m）。" +
                 "長すぎる近道を試すと道探しが重くなる")]
        public float planSimplifyMaxM = 6f;

        [Header("人や物で前がふさがったとき（依頼 v9 の 3 章）")]
        [Tooltip("止まってこの秒数たっても前が空かなければ、回り込む道を探す（秒。エイイチの決定）。" +
                 "引けなければ待ち、この秒数ごとに探し直す")]
        public float detourAfterSec = 3f;

        [Tooltip("回り込む道を探すとき、通れないマスとして置く人・ロボットを探す半径（m）")]
        public float detourNearM = 3f;

        [Tooltip("回り込む道と人のあいだに足す余裕（m）。" +
                 "人の半径＋ロボットの半径＋この値 を通れないマスにする")]
        public float detourPersonMarginM = 0.1f;

        [Tooltip("回り込む道が、もとの残りの道のこの倍＋detourExtraAddM より長ければ取らずに待つ")]
        public float detourMaxFactor = 2f;

        [Tooltip("同じく、足し算のぶん（m）")]
        public float detourExtraAddM = 5f;

        [Header("ロボットどうし（依頼 v9 の 4 章・エイイチの決定）")]
        [Tooltip("相手の「先の道」を何 m 分見るか（m）")]
        public float robotPathAheadM = 4f;

        [Tooltip("優先が高い台の先の道から、2 台の半径の和＋この値 以内のマスを重くする（m）")]
        public float robotPathCostMarginM = 0.2f;

        [Tooltip("重くする強さ（1 歩の重さに足す）。相手の道とかぶらない道があればそちらを選ばせる")]
        public float robotPathCostAdd = 6f;

        [Tooltip("よける場所は、相手の先の道から 2 台の半径の和＋この値 以上離れていること（m）")]
        public float robotYieldSeparationM = 0.1f;

        [Tooltip("よける場所を探す広さ（m）。自分のまわりこの距離まで探す")]
        public float robotYieldSpotSearchM = 2.5f;

        [Tooltip("優先が高い台がこの距離まで来て、先の道がかぶるときに引き直す（m）")]
        public float robotReplanNearM = 3f;

        [Tooltip("2 台が向かい合ってこの秒数止まったら、優先が低い方がよける場所へ動く（秒）")]
        public float headOnStuckSec = 10f;

        [Tooltip("「向かい合っている」と見る、2 台の向きの内積の上限。" +
                 "−1 が真正面。−0.5 は 120 度より正面寄り（依頼に数字が無かったのでここで決めた）")]
        public float headOnDotMax = -0.5f;

        // ================= 依頼 v13：ロボットどうしのすれ違い =================

        [Header("すれ違い（依頼 v13 の 依頼1。実店舗の動画に合わせた）")]
        [Tooltip("すれ違いの走り方を使うか。切のときは cs:3 と同じ動き（向かい合ったら" +
                 "譲る順番の決まりに入る）。入にすると、向かい合った2台は止まらずに" +
                 "速さを落として右へ寄り、体どうしの間を詰めてすれ違う。" +
                 "標準は入（2026-10-27 エイイチの決定。③2台の2夜分で、向かい合って" +
                 "止まった一番長い回が 入 のほうがはっきり短かった）")]
        public bool passByEnabled = true;

        [Tooltip("進む先どれだけ以内に向かってくる台がいたら「すれ違い中」に入るか（m）。" +
                 "中心どうしの距離で見る。向かい合って止まった回数もこの距離の中で数える")]
        public float passByDistanceM = 2.0f;

        [Tooltip("すれ違い中の速さの上限（m/秒）。止まらずにこの速さまで落とす")]
        public float passBySpeedMps = 0.3f;

        [Tooltip("すれ違うときに詰める、体どうしの間（m）。" +
                 "前のセンサーは、相手がロボットのときだけこの間まで全停止しない")]
        public float passBySeparationM = 0.1f;

        [Tooltip("すれ違うときに、人が横を通れるよう残す幅（m。人の直径ぶん）。" +
                 "2026-10-26：1.2 m の通路で2台が寄ると壁際 0.05 m しか残らず、" +
                 "人がまったく通れなくなっていた（通路の詰まりが 35件→845件）。" +
                 "いまは『体の幅×2 ＋ 間 ＋ この値』が無い所では すれ違わず、" +
                 "今までの決まり（料理を載せた方が先・3秒で回り込む）に渡す。" +
                 "0 にすると 2026-10-26 より前の動きに戻る")]
        public float passByNeedPersonM = 0.4f;

        [Tooltip("すれ違いをやめて今までの決まりに渡す前に、いったん離れる間（m）。" +
                 "2026-10-26：鼻先 0.06 m で渡すと、譲る場所も回り込む道も見つからず" +
                 "545 秒固まった。前のセンサーが届く 0.6 m まで下がってから渡す")]
        public float passByBackOffM = 0.6f;

        [Tooltip("離れるときの速さ（m/秒）。すれ違い中の速さと同じで十分")]
        public float passByBackSpeedMps = 0.3f;

        /// <summary>
        /// すれ違うときに、進む向きの右へ寄る距離（m）。
        /// 2台が左右に分かれるので、中心どうしは この2倍 ＝ 体の幅＋間 だけ離れる。
        /// 値は上の3つから出す（別の数字を増やさない）
        /// </summary>
        public float PassByShiftM { get { return (diameter + passBySeparationM) * 0.5f; } }

        /// <summary>
        /// すれ違うのに要る通路の幅（m）。
        /// 体の幅×2 ＋ 2台の間 ＋ 人が横を通る分（2026-10-26）
        /// </summary>
        public float PassByNeedWidthM
        {
            get { return diameter * 2f + passBySeparationM + Mathf.Max(0f, passByNeedPersonM); }
        }

        [Header("見せる（依頼 v9 の 6 章。測る回でも出してよい）")]
        [Tooltip("いま引いている道を床の上に細い線で出すか")]
        public bool showPathLine = true;

        [Tooltip("線の太さ（m）")]
        public float pathLineWidthM = 0.04f;

        [Tooltip("線を床から浮かせる高さ（m）。床の塗りより上に出す")]
        public float pathLineHeightM = 0.04f;

        /// <summary>MapPlanner で走るか（依頼 v9）</summary>
        public bool UseMapPlanner { get { return pathMode == PathMode.MapPlanner; } }

        /// <summary>道の引き方の数字を GridPlanner に渡す形にする</summary>
        public GridPlanner.Settings PlannerSettings()
        {
            GridPlanner.Settings s = new GridPlanner.Settings();
            s.narrowCellCost = narrowCellCost;
            s.centerWeight = centerWeight;
            s.preferClearM = preferClearM;
            s.maxExpand = planMaxExpand;
            s.snapM = mapSnapM;
            s.simplifyMaxM = planSimplifyMaxM;
            return s;
        }
    }
}
```

### Assets/ServiceSim/Robot/ServingRobotController.cs

```csharp
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

namespace ServiceSim
{
    /// <summary>
    /// 配膳ロボット1台。受け渡し口 → 卓 → 受け渡し口 の1往復を走り、
    /// 止まる・動き出す・経路を引き直す・動けなくなるを自分で決める。
    ///
    /// NavMeshAgent は経路を探すのと、人に自分をよけてもらうためだけに付ける。
    /// 自動でよける機能（obstacleAvoidanceType）は切り、位置はこの script が動かす。
    ///
    /// この script はロボットだけを扱う。人も店も動かさない。
    /// 細い通路の区画と入口は、店の側で決めた値を StartTrip で受け取るだけ。
    /// </summary>
    [RequireComponent(typeof(NavMeshAgent))]
    public class ServingRobotController : MonoBehaviour
    {
        public enum Phase
        {
            Idle,        // 往復していない
            ToTable,     // 卓へ向かう（細い通路の入口で待つこともある）
            Dwell,       // 卓で25秒止まる（「止まった」に数えない）
            ToPass,      // 受け渡し口へ戻る
            StuckWait,   // 動けなくなって、スタッフが来て動かしてくれるのを待つ（v5 3章）
            Done         // 往復が終わった
        }

        [Header("設定")]
        public RobotSpec spec;
        public FrontSensor sensor;

        [Tooltip("1台めは1、2台めは2")]
        public int robotId = 1;

        [Tooltip("立ち往生したときにスタッフを呼ぶ先（ServiceOps が入れる。v5 3章）")]
        public ServiceOps ops;

        // ---- 戻りの線（v5。受け渡し口の前の点を通らずに待機場所へ抜ける）----
        Vector3 _standbyPoint;
        bool _hasStandby;
        readonly List<Vector3> _drinkQueues = new List<Vector3>();
        int _returnLineFallbacks;
        bool _returnLineUsed;

        /// <summary>この台の待機場所（ServiceOps が入れる）</summary>
        public void SetStandbyPoint(Vector3 p)
        {
            _standbyPoint = new Vector3(p.x, 0f, p.z);
            _hasStandby = true;
        }

        /// <summary>戻りの線が使えず、今までどおり受け渡し口の前へ戻った回数（報告用）</summary>
        public int ReturnLineFallbacks { get { return _returnLineFallbacks; } }

        /// <summary>直前の戻りで、戻りの線を使えたか（報告用）</summary>
        public bool ReturnLineUsed { get { return _returnLineUsed; } }

        [Header("いまの様子（読むだけ）")]
        [SerializeField] Phase _phase = Phase.Idle;
        [SerializeField] bool _enteredAisle;
        [SerializeField] bool _yieldHold;

        // ---- 1往復の測りもの ----
        public int TableId { get; private set; }

        /// <summary>
        /// この走りを始めた、店の中の時刻（秒）。始めた側が渡す。渡されなければ -1。
        /// 「いつ始まったか」をロボット自身が持つ。外から覚えておくと、
        /// 走り終わってすぐ次の仕事が入ったときに取りこぼす。
        /// </summary>
        public float RunStartS { get; private set; }

        public float DistanceM { get; private set; }
        public float TotalS { get; private set; }
        public float StoppedS { get; private set; }
        public int StopCount { get; private set; }
        public float MaxDecelMps2 { get; private set; }
        public float MinClearanceM { get; private set; }
        public bool Stuck { get; private set; }
        public bool MetRobot { get; private set; }
        public bool Aborted { get; private set; }

        /// <summary>
        /// この往復で動けなくなってスタッフを呼んだ回数（v5 3章）。
        /// 動かしてもらったあと、また動けなくなれば2回めを呼ぶので、Stuck（印）とは別に数える。
        /// </summary>
        public int StuckCount { get; private set; }

        /// <summary>スタッフが来るのを待った秒数の合計（StoppedS にも入っている）</summary>
        public float StuckWaitS { get; private set; }

        /// <summary>1往復が打ち切り（maxTripSec 超え）で終わったか</summary>
        public bool TimedOut { get; private set; }

        /// <summary>細い通路の入口で、ほかの台を待った秒数（秒）。stop_durations_s には入れない</summary>
        public float WaitAisleS { get; private set; }

        // ---- 依頼 v9：地図の上で自分で道を引く ----

        /// <summary>この往復で道を引き直した回数（回り込み・よけを含む。trips.csv の replans）</summary>
        public int Replans { get; private set; }

        /// <summary>3 章の回り込みで道を変えた回数（trips.csv の detours）</summary>
        public int Detours { get; private set; }

        /// <summary>
        /// 4 章でよけた回数（trips.csv の robot_yields）。
        /// よける場所へ動いた回数 ＋ 相手の道をよけた道に引き直した回数
        /// </summary>
        public int RobotYields { get; private set; }

        /// <summary>この往復で、1 回の道探しにかかった最大の時間（ミリ秒。trips.csv の plan_ms_max）</summary>
        public float PlanMsMax { get; private set; }

        /// <summary>
        /// この往復で、サイトの道順をたどったときの長さ（routes の length の和, m。
        /// trips.csv の site_route_m）。走りには使わない
        /// </summary>
        public float SiteRouteM { get; private set; }

        /// <summary>
        /// 往復が実際に終わった店の中の時刻（秒。卓での 25 秒を含む。trips.csv の trip_end_s）。
        /// 終わっていなければ -1
        /// </summary>
        public float TripEndS { get; private set; }

        /// <summary>
        /// この往復で、出発点が地図では通れないマス（0）だったために近くへ寄せた回数。
        /// 通路の入口で待つ点・横へどいた点は free_class では 0 になるので、ここに出る
        /// </summary>
        public int MapSnapStarts { get; private set; }

        /// <summary>この往復で、道が引けなかった回数</summary>
        public int PlanFails { get; private set; }

        /// <summary>いま走っているのが MapPlanner か（地図が無ければ SiteRoutes に落とす）</summary>
        public bool UseMapPlanner
        {
            get { return spec != null && spec.UseMapPlanner && _planner != null; }
        }

        /// <summary>run_info.json に出す、実際に使った道の決め方</summary>
        public PathMode ActivePathMode
        {
            get { return UseMapPlanner ? PathMode.MapPlanner : PathMode.SiteRoutes; }
        }

        /// <summary>
        /// 譲る順番（依頼 v9 の 4 章・エイイチの決定）。**小さいほど先に通る**。
        ///   0 … 料理を載せているロボット（卓へ向かっている。卓から次の卓へ向かう間も含む）
        ///   1 … 空で戻るロボット（受け渡し口・待機場所へ向かっている、待機場所へ動いている）
        /// 同じ状態なら 1 号機が先（robotId の小さい方）。
        /// </summary>
        public int YieldRank
        {
            get
            {
                Phase p = _phase == Phase.StuckWait ? _phaseBeforeStuck : _phase;
                return p == Phase.ToTable || p == Phase.Dwell ? 0 : 1;
            }
        }

        /// <summary>自分が相手より後に通る側か（＝自分がよける）</summary>
        public bool YieldsTo(ServingRobotController other)
        {
            if (other == null) return false;
            if (YieldRank != other.YieldRank) return YieldRank > other.YieldRank;
            return robotId > other.robotId;
        }

        /// <summary>いま回り込んだ道を走っているか（モニターが読む）</summary>
        public bool Detouring { get { return _onDetour; } }

        /// <summary>いま相手をよけて待っているか（モニターが読む）</summary>
        public bool YieldingToRobot { get { return _mapYield; } }

        // ---- すれ違い（依頼 v13 の 依頼1）----

        /// <summary>いますれ違っている相手（いなければ null）</summary>
        public ServingRobotController PassByOther { get { return _passByOther; } }

        /// <summary>
        /// すれ違いの走り方（速さを落として右へ寄る）をしているか。
        /// passByEnabled が切のとき、譲っている最中、幅が足りないときは false
        /// </summary>
        public bool PassByMoving
        {
            get { return _passByOther != null && !_passByNarrow && _passByShiftM > 0f; }
        }

        /// <summary>
        /// 向かい合ってはいるが、寄っても「体の幅×2 ＋ 体どうしの間」に足りないか。
        /// このときは今までの決まり（譲る順番・回り込み）に任せる
        /// </summary>
        public bool PassByNarrow { get { return _passByNarrow; } }

        /// <summary>直前の1コマで進んだ距離（m）。コマの粗さを確かめるのに使う</summary>
        public float LastStepM { get; private set; }

        /// <summary>止まった原因</summary>
        public enum StopCause
        {
            Guest,           // 歩く客とすれ違った
            Staff,           // スタッフとすれ違った
            PersonStanding,  // 人が立ち止まっていた
            Chair,           // 椅子
            OtherRobot,      // もう1台のロボット（Unity だけ）
            NoPath,          // 行き先への道が引けない（通路の幅が足りない）
            AisleQueue,      // 細い通路の順番待ち（Unity だけ）
            Unknown          // 分からない
        }

        /// <summary>どう動き出したか</summary>
        public enum StopResolve
        {
            SelfResume,      // 自分で動き出した
            Replanned,       // 経路を引き直した
            Stuck,           // 動けなくなった
            AisleWait,       // 入口で待った
            Detoured,        // 回り込んで動き出した（依頼 v9 の 3 章）
            YieldedToRobot   // よける場所へ動いた（依頼 v9 の 4 章）
        }

        /// <summary>止まり1回ぶんの記録</summary>
        public struct StopEvent
        {
            public float startTimeS;        // 店の中の時刻（秒）
            public Vector3 position;        // 止まった位置
            public float durationS;         // 止まっていた秒数
            public StopCause cause;
            public string causeDetail;      // 何に当たったか
            public StopResolve resolve;
            public float clearanceLeftM;    // 左の空き（ロボットの外周から）
            public float clearanceRightM;   // 右の空き
            public bool pathBlocked;        // 行き先への道が引けていなかったか

            // ---- 止まっている間、何で動けなかったかの内訳（秒）。B7 を調べるため ----
            /// <summary>前のセンサーが「ふさがっている」と言っていた秒数</summary>
            public float blockedS;
            /// <summary>行き先への道が引けていなかった秒数</summary>
            public float noPathS;
            /// <summary>細い通路の入口で順番を待っていた秒数</summary>
            public float yieldS;
            /// <summary>ふさがりが解けたあと、動き出すまで待っていた秒数</summary>
            public float clearWaitS;

            // ---- 道を引いたときの中身（調べるためだけ。動きは変えない）----
            /// <summary>道を引いた出発点（焼いた面へ寄せたあと）</summary>
            public Vector3 solveFrom;
            /// <summary>道を引いた行き先（同じく寄せたあと）</summary>
            public Vector3 solveTo;
            /// <summary>CalculatePath の結果（PathComplete / PathPartial / PathInvalid / なし）</summary>
            public string solveStatus;
            /// <summary>引けた角の数</summary>
            public int solveCorners;
            /// <summary>
            /// ロボットのいる位置から、いちばん近い焼いた面までの距離（m）。
            /// 面の上に立っていれば 0 に近い。探す半径で見つからなければ -1
            /// </summary>
            public float offMeshM;
            /// <summary>止まっている間に経路を引き直した回数</summary>
            public int replans;
        }

        readonly List<StopEvent> _stopEvents = new List<StopEvent>();

        /// <summary>この往復で止まった1回ずつの記録</summary>
        public IReadOnlyList<StopEvent> StopEvents { get { return _stopEvents; } }

        readonly List<float> _stopDurations = new List<float>();

        /// <summary>自分で動き出した止まりの長さ（秒）。動けなくなった分と、入口で待った分は入れない</summary>
        public IReadOnlyList<float> StopDurationsS { get { return _stopDurations; } }

        public Phase CurrentPhase { get { return _phase; } }
        public bool TripFinished { get { return _phase == Phase.Done; } }
        public bool HasEnteredAisle { get { return _enteredAisle; } }

        /// <summary>いまの速さ（m/秒。字幕が読むだけ）</summary>
        public float SpeedMps { get { return _speed; } }

        /// <summary>
        /// いま続けて動けていない秒数（人のよけ方と、キッチンのモニターが読む）。
        /// 動けなくなって人を待っているあいだは、60 秒＋待った分として続けて数える
        /// </summary>
        public float StoppedNowS
        {
            get
            {
                if (_phase != Phase.StuckWait) return _stoppedTimer;
                float base_ = spec != null ? spec.stuckAfterStoppedSec : 60f;
                return base_ + _stuckWaitTimer;
            }
        }

        /// <summary>いま止まっている理由の名前（キッチンのモニターが読む。記録と同じ言葉）</summary>
        public string StopCauseNow
        {
            get
            {
                if (_phase == Phase.StuckWait) return "立ち往生";
                if (_yieldHold) return "通路の順番待ち";
                if (_robotYield || _mapYield) return "もう1台のロボット";
                if (!_recordingStop) return "";
                return TripLoggerCauseName(_open.cause);
            }
        }

        static string TripLoggerCauseName(StopCause c)
        {
            switch (c)
            {
                case StopCause.Guest: return "お客さんとすれ違った";
                case StopCause.Staff: return "スタッフとすれ違った";
                case StopCause.PersonStanding: return "人が立ち止まっていた";
                case StopCause.Chair: return "椅子・荷物・ベビーカー";
                case StopCause.NoPath: return "通路の幅が足りない";
                case StopCause.OtherRobot: return "もう1台のロボット";
                case StopCause.AisleQueue: return "通路の順番待ち";
                default: return "分からない";
            }
        }

        /// <summary>細い通路の入口で待っている最中か（字幕が読むだけ）</summary>
        public bool YieldHolding { get { return _yieldHold; } }

        /// <summary>卓での受け渡しの残り秒数（字幕が読むだけ）</summary>
        public float DwellRemainS
        {
            get { return spec != null ? Mathf.Max(0f, spec.tableDwellSec - _dwellTimer) : 0f; }
        }

        /// <summary>
        /// スタッフを待っている秒数（字幕が読むだけ。v5 3章）。
        /// 受け渡し口へ移すのをやめたので「残り」ではなく、呼んでから待っている長さを出す
        /// </summary>
        public float StuckWaitRemainS
        {
            get { return _stuckWaitTimer; }
        }
        public Bounds AisleZone { get { return _aisleZone; } }

        /// <summary>
        /// いまたどっている区間の線（1つ前の点 → いま目指す点）。
        /// 譲る側が「この道から横へどく」ために読む（2026-10-23）。
        ///
        /// サイトの routes の点をたどっているときは、その2点をそのまま返す。
        /// 地図で自分の道を引いているとき（MapPlanner・依頼 v9）は、引いた道の
        /// いまの区間を返す（人のよけ方は変えないので、ここは新しい道を返すようにして残す）。
        /// 点が無い・長さが 0 のときは、いまの位置と向きから2 m 先までの線にする。
        /// </summary>
        public bool CurrentLegSegment(out Vector3 a, out Vector3 b)
        {
            int n = WayCount, i = WayIndex;
            if (n > 0 && i >= 0 && i < n)
            {
                b = WayPoint(i);
                a = i > 0 ? WayPoint(i - 1) : transform.position;
                a.y = 0f;
                if ((b - a).sqrMagnitude > 0.0001f) return true;
            }

            a = transform.position; a.y = 0f;
            Vector3 f = transform.forward; f.y = 0f;
            if (f.sqrMagnitude < 0.0001f) { b = a; return false; }
            b = a + f.normalized * 2f;
            return true;
        }

        /// <summary>
        /// これから進む道（v6b）。**いまの位置から先**を aheadM 分だけ、
        /// 残りの点をつないで返す（into の先頭はいまの位置）。
        ///
        /// SiteRoutes ではサイトの routes の残りの点、
        /// MapPlanner（依頼 v9）では地図で引いた道の残りの曲がり角。
        /// 譲る側（人・ほかのロボット）が「自分がこの道の上にいるか」を見るために読む。
        /// 点が無いときは、いまの向きへ aheadM の線1本にする。
        /// </summary>
        public void PathAhead(float aheadM, List<Vector3> into)
        {
            if (into == null) return;
            into.Clear();
            if (aheadM <= 0f) return;

            Vector3 here = transform.position; here.y = 0f;
            into.Add(here);

            float left = aheadM;
            Vector3 from = here;

            int n = WayCount;
            for (int i = WayIndex; i >= 0 && i < n && left > 0f; i++)
            {
                Vector3 to = WayPoint(i);
                Vector3 d = to - from;
                float len = d.magnitude;
                if (len < 0.0001f) continue;

                if (len >= left)
                {
                    into.Add(from + d / len * left);
                    return;
                }
                into.Add(to);
                left -= len;
                from = to;
            }

            if (left > 0f && into.Count == 1)
            {
                // 道順の点が無い。いまの向きへ伸ばす
                Vector3 f = transform.forward; f.y = 0f;
                if (f.sqrMagnitude > 0.0001f) into.Add(here + f.normalized * aheadM);
            }
        }


        /// <summary>1走りで寄る卓1つぶん</summary>
        public struct RunStop
        {
            public int tableId;
            public Vector3 point;          // ロボットが止まる位置（通路の上）
            public Vector3 aisleEntrance;  // その通路に入る前に待つ位置
            public Bounds aisleZone;       // 譲り合いの区画
        }

        readonly List<RunStop> _stops = new List<RunStop>();
        readonly List<int> _tableIds = new List<int>();
        int _stopIndex;

        /// <summary>この走りで寄る卓の番号</summary>
        public IReadOnlyList<int> TableIds { get { return _tableIds; } }

        /// <summary>この走りで何卓に届けたか</summary>
        public int StopsCompleted { get; private set; }

        /// <summary>この走りで寄る卓の数</summary>
        public int RunStopCount { get { return _stops.Count; } }

        readonly List<float> _stopArrivedS = new List<float>();

        /// <summary>
        /// 止まる所ごとに着いた時刻（店の中, 秒）。着いていない所は -1（依頼 v8 の 2）。
        /// trips.csv の stop_arrived_s に出す
        /// </summary>
        public IReadOnlyList<float> StopArrivedS { get { return _stopArrivedS; } }

        /// <summary>
        /// 止まる所に着いた（引数：ロボット・止まる所の番号）。依頼 v8 の 1。
        /// ServiceOps が受けて、その所の皿を「届いた」にする。
        /// 受け渡しの 25 秒（tableDwellSec）の **前** に出る
        /// </summary>
        public event Action<ServingRobotController, int> StopArrived;

        /// <summary>1往復が終わったときに呼ばれる</summary>
        public event Action<ServingRobotController> TripCompleted;

        // ---- いまシーンにいるロボット（譲り合いの判定に使う）----
        static readonly List<ServingRobotController> _all = new List<ServingRobotController>();
        public static IReadOnlyList<ServingRobotController> All { get { return _all; } }

        // ---- 内部 ----
        NavMeshAgent _agent;
        NavMeshPath _path;      // Awake で作る。フィールドの初期化で new すると
                                // 「InitializeNavMeshPath is not allowed to be called
                                //  from a MonoBehaviour constructor」になるため
        Vector3[] _corners = new Vector3[0];
        int _cornerIndex;
        Vector3 _destination;
        Vector3 _destinationOnMesh;
        bool _hasDestinationOnMesh;
        Vector3 _tablePoint;
        Vector3 _passPoint;
        Vector3 _aisleEntrance;
        Bounds _aisleZone;
        bool _hasAisleZone;

        float _speed;
        float _dwellTimer;
        float _clearTimer;
        float _stoppedTimer;        // いま続けて止まっている秒数
        float _lastReplanAtStopped;
        float _stuckWaitTimer;
        Phase _phaseBeforeStuck = Phase.ToTable;   // 動けなくなる直前に向かっていた先（v5 3章）

        // ---- 入口での順番待ち（v5）----
        float _yieldHoldS;          // 入口で待っている秒数
        bool _yieldGaveUp;          // 待ちすぎたので、この区間はもう譲らない
        Vector3 _aisleWaitPoint;    // 入口の手前で待つ点

        // ---- 台どうしの譲り合い（v5）----
        bool _robotYield;                    // いま譲って下がっている最中
        float _robotYieldS;                  // 譲り始めてからの秒数
        float _blockedByRobotS;              // もう1台にふさがれている秒数
        ServingRobotController _robotYieldTo;// 道をゆずる相手
        float _robotYieldStartS;             // 譲り始めた時刻（yields.csv）
        Vector3 _robotYieldAt;               // 譲り始めた位置（yields.csv）
        bool _robotYieldStepped;             // 相手の道から横へどけたか（false なら来た道へ戻った）
        float _repathCooldown;
        bool _inStop;
        float _currentStopS;
        bool _inWait;
        float _currentWaitS;
        Vector3 _pathEndPoint;
        bool _hasPathEnd;
        bool _wasBlocked;
        bool _hasBrakeCause;
        StopCause _brakeCause;
        string _brakeDetail = "";
        string _aisleBlockerName = "";
        bool _replannedInStop;
        bool _recordingStop;
        StopEvent _open;
        bool _pathValid;

        // ---- 途中までの経路から出る（B7）----
        /// <summary>この止まりの中で、最後に強く引き直したときの no_path_s</summary>
        float _lastEscapeAtNoPathS;
        /// <summary>いま通路の入口へ出ようとしているか</summary>
        bool _escapeToEntrance;

        // ---- 依頼 v9：地図の上で自分で道を引く（内部）----
        RobotMap _map;
        GridPlanner _planner;

        /// <summary>前がふさがって止まっている秒数（3 章の 3 秒を数える）</summary>
        float _blockedFrontS;

        /// <summary>いま回り込んだ道を走っているか</summary>
        bool _onDetour;

        /// <summary>向かい合って止まっている秒数（4 章の 10 秒を数える）</summary>
        float _headOnS;

        /// <summary>いまよける場所へ動いて待っているか（4 章）</summary>
        bool _mapYield;
        float _mapYieldS;
        ServingRobotController _mapYieldTo;

        /// <summary>いま道を引き直してよけている相手（1 回の出会いを 1 回だけ数えるため）</summary>
        ServingRobotController _avoidTarget;
        float _mapYieldStartS;
        Vector3 _mapYieldAt;
        bool _mapYieldStepped;

        /// <summary>この止まりの終わり方を上書きする（回り込み・よけ）</summary>
        bool _hasResolveOverride;
        StopResolve _resolveOverride;

        /// <summary>先の道を入れる作業場（毎コマ作り直さない）</summary>
        readonly List<Vector3> _aheadMine = new List<Vector3>();
        readonly List<Vector3> _aheadOther = new List<Vector3>();

        static readonly Vector3[] NoCorners = new Vector3[0];

        // ---- いまたどっている点の並び（サイトの道順と、地図で引いた道をまとめて見る）----

        /// <summary>
        /// いまたどっている点の数。
        /// SiteRoutes では道順の点（_legPoints）、MapPlanner では引いた道の曲がり角（_corners）。
        /// PathAhead・CurrentLegSegment は、どちらでも「これから進む道」を返せるように、ここを見る
        /// </summary>
        int WayCount { get { return _legPoints != null ? _legPoints.Count : _corners.Length; } }

        /// <summary>いま目指している点の番号</summary>
        int WayIndex { get { return _legPoints != null ? _legIndex : _cornerIndex; } }

        /// <summary>i 番目の点（y は 0）</summary>
        Vector3 WayPoint(int i)
        {
            Vector3 p = _legPoints != null ? _legPoints[i] : _corners[i];
            p.y = 0f;
            return p;
        }

        /// <summary>
        /// ロボットが歩く NavMesh の種類（agentTypeID）。既定の 0。
        /// NavMesh が人用とロボット用の2枚になったので、引くときは必ず種類を指定する
        /// （指定しないと「どの種類か決められない」と警告が出て、思わぬ面を拾う）。
        /// </summary>
        public const int RobotAgentTypeId = 0;

        static NavMeshQueryFilter RobotFilter
        {
            get
            {
                NavMeshQueryFilter f = new NavMeshQueryFilter();
                f.agentTypeID = RobotAgentTypeId;
                f.areaMask = NavMesh.AllAreas;
                return f;
            }
        }

        /// <summary>
        /// stops.csv の cause_detail に書く印。
        /// この印がある止まりは「途中までの経路に縛られて、引き直して出た」もの。
        /// CSV を割らないように、区切りはカンマを使わない。
        /// </summary>
        public const string PartialPathMark = "途中までの経路";

        void Awake()
        {
            _path = new NavMeshPath();
            _agent = GetComponent<NavMeshAgent>();
            ApplySpecToAgent();
        }

        void OnEnable()
        {
            if (!_all.Contains(this)) _all.Add(this);
        }

        void OnDisable()
        {
            _all.Remove(this);
        }

        void ApplySpecToAgent()
        {
            if (_agent == null || spec == null) return;
            _agent.radius = spec.Radius;
            _agent.height = spec.height;
            _agent.speed = spec.maxSpeed;
            _agent.acceleration = spec.acceleration;
            _agent.angularSpeed = spec.turnSpeedDegPerSec;
            // 自動でよける機能は使わない（実機のよけ方と違うため）
            // ロボットはロボット用の面（半径 navAgentRadius で焼いたもの）を歩く
            if (_agent.agentTypeID != RobotAgentTypeId) _agent.agentTypeID = RobotAgentTypeId;
            _agent.obstacleAvoidanceType = ObstacleAvoidanceType.NoObstacleAvoidance;
            _agent.avoidancePriority = spec.avoidancePriority;
            // 位置と向きはこの script が動かす。agent は経路を探すのと、人に見てもらうためだけ
            _agent.updatePosition = false;
            _agent.updateRotation = false;
            _agent.autoRepath = false;
            // isStopped は NavMesh に乗っている agent にしか使えない
            if (_agent.enabled && _agent.isOnNavMesh) _agent.isStopped = true;
        }

        /// <summary>受け渡し口に置き直して、次の往復を始める準備をする</summary>
        /// <summary>
        /// 仕事がないときの待機場所へ、ゆっくり下がる（B2）。
        /// 往復ではないので、距離・止まり・動けなくなったの記録には数えない。
        /// 止まる決まり（前のセンサー・譲り合い）もここでは働かせない。
        /// すでにその場所の近くにいれば何もしない。
        /// </summary>
        public void GoStandby(Vector3 spot)
        {
            if (_phase != Phase.Idle && _phase != Phase.Done) return;
            if (_agent == null || !Application.isPlaying) return;
            if (!_agent.enabled || !_agent.isOnNavMesh) return;

            Vector3 here = transform.position; here.y = 0f;
            Vector3 to = spot; to.y = 0f;
            if (Vector3.Distance(here, to) <= spec.arriveRadius) { _agent.isStopped = true; return; }

            _agent.isStopped = false;
            _agent.speed = spec.maxSpeed;
            _agent.SetDestination(spot);
        }

        public void PlaceAt(Vector3 position, Quaternion rotation)
        {
            transform.SetPositionAndRotation(position, rotation);
            _speed = 0f;

            if (_agent == null) return;
            if (!Application.isPlaying) return;      // 編集モードでは NavMeshAgent を触らない

            if (!_agent.enabled) _agent.enabled = true;
            _agent.Warp(position);
            if (_agent.isOnNavMesh)
            {
                _agent.isStopped = true;
                _agent.nextPosition = position;
            }
        }

        /// <summary>1往復を始める（細い通路の譲り合いなし。場面の再現などに使う）</summary>
        public void StartTrip(int tableId, Vector3 tablePoint, Vector3 passPoint)
        {
            StartTrip(tableId, tablePoint, passPoint, passPoint, new Bounds());
        }

        /// <summary>1往復を始める（1卓だけ）</summary>
        public void StartTrip(int tableId, Vector3 tablePoint, Vector3 passPoint,
                              Vector3 aisleEntrance, Bounds aisleZone)
        {
            RunStop s = new RunStop();
            s.tableId = tableId;
            s.point = tablePoint;
            s.aisleEntrance = aisleEntrance;
            s.aisleZone = aisleZone;
            List<RunStop> one = new List<RunStop>();
            one.Add(s);
            StartRun(one, passPoint);
        }

        /// <summary>
        /// 1走りを始める。受け渡し口を出て、渡された卓を順に回り、受け渡し口へ戻る。
        /// 止まる決まり（前のセンサー・譲り合い・動けなくなったときの扱い）は1卓のときと同じ。
        /// </summary>
        public void StartRun(IList<RunStop> stops, Vector3 passPoint)
        {
            StartRun(stops, passPoint, -1f);
        }

        /// <summary>始めた時刻（店の中, 秒）も渡して1走りを始める。記録に使う</summary>
        public void StartRun(IList<RunStop> stops, Vector3 passPoint, float startS)
        {
            RunStartS = startS;
            _stops.Clear();
            _tableIds.Clear();
            _stopArrivedS.Clear();
            if (stops != null)
            {
                for (int i = 0; i < stops.Count; i++)
                {
                    _stops.Add(stops[i]);
                    _tableIds.Add(stops[i].tableId);
                    _stopArrivedS.Add(-1f);     // 着いたら時刻を入れる（依頼 v8 の 2）
                }
            }
            _stopIndex = 0;
            StopsCompleted = 0;
            ResetTo = "";               // 打ち切りで置き直したときだけ入れる（依頼 v8b の A）

            if (_stops.Count == 0) { _phase = Phase.Idle; return; }
            BeginRun(passPoint);
        }

        void BeginRun(Vector3 passPoint)
        {
            RunStop first = _stops[0];
            int tableId = first.tableId;
            Vector3 tablePoint = first.point;
            Vector3 aisleEntrance = first.aisleEntrance;
            Bounds aisleZone = first.aisleZone;

            if (_path == null) _path = new NavMeshPath();
            ApplySpecToAgent();
            if (Application.isPlaying && _agent != null)
            {
                if (!_agent.enabled) _agent.enabled = true;
                if (!_agent.isOnNavMesh) _agent.Warp(transform.position);
            }

            TableId = tableId;
            _tablePoint = tablePoint;
            _passPoint = passPoint;
            _aisleEntrance = aisleEntrance;
            _aisleZone = aisleZone;
            _hasAisleZone = aisleZone.size.x > 0.01f && aisleZone.size.z > 0.01f;

            DistanceM = 0f;
            TotalS = 0f;
            StoppedS = 0f;
            StopCount = 0;
            MaxDecelMps2 = 0f;
            MinClearanceM = float.PositiveInfinity;
            Stuck = false;
            StuckCount = 0;
            StuckWaitS = 0f;
            LegWasNarrow = false;
            MetRobot = false;
            Aborted = false;
            TimedOut = false;
            WaitAisleS = 0f;
            _stopDurations.Clear();
            _stopEvents.Clear();

            _speed = 0f;
            _dwellTimer = 0f;
            _clearTimer = 0f;
            _hasDestinationOnMesh = false;
            _wasBlocked = false;
            _hasBrakeCause = false;
            _stoppedTimer = 0f;
            _lastReplanAtStopped = 0f;
            _stuckWaitTimer = 0f;
            _phaseBeforeStuck = Phase.ToTable;
            _yieldHoldS = 0f;
            _yieldGaveUp = false;
            _robotYield = false;
            _robotYieldS = 0f;
            _robotYieldTo = null;
            _robotYieldStepped = false;
            _robotYieldStartS = 0f;
            _blockedByRobotS = 0f;
            _repathCooldown = 0f;
            _inStop = false;
            _currentStopS = 0f;
            _lastEscapeAtNoPathS = 0f;
            _escapeToEntrance = false;

            // ---- 依頼 v9 ----
            Replans = 0;
            Detours = 0;
            RobotYields = 0;
            PlanMsMax = 0f;
            SiteRouteM = 0f;
            TripEndS = -1f;
            MapSnapStarts = 0;
            PlanFails = 0;
            _blockedFrontS = 0f;
            _onDetour = false;
            _headOnS = 0f;
            _mapYield = false;
            _mapYieldS = 0f;
            _mapYieldTo = null;
            _avoidTarget = null;
            _hasResolveOverride = false;

            // 譲り合いを使わない／区画が無いときは、はじめから「入った」扱いにする
            _enteredAisle = !(spec != null && spec.aisleYielding) || !_hasAisleZone;
            _yieldHold = false;

            _phase = Phase.ToTable;
            // v5b: サイトの道順を受け取る（店を建て直すこともあるので、1 走りごとに引き直す）
            StoreBuilder store = FindObjectOfType<StoreBuilder>();
            _routes = store != null ? store.Routes : null;

            // 依頼 v9: ロボットが走れる所の地図を受け取る。道探しはこの上でやる（NavMesh は使わない）。
            // 地図が無いのに MapPlanner だと走れないので、はっきり言ってから SiteRoutes に落とす
            _map = store != null ? store.RobotMap : null;
            _planner = _map != null ? _map.Planner : null;
            if (spec != null && spec.pathMode == PathMode.MapPlanner && _planner == null)
                Debug.LogError("[ServiceSim] ロボット" + robotId + "号機: pathMode が MapPlanner ですが、" +
                               "店に地図（map.json）がありません。この回は SiteRoutes で走ります" +
                               "（JsonStoreLayout の mapJson を確かめてください）");
            // ドリンクバーの並ぶ点（戻りの線を離すために使う。v5）
            _drinkQueues.Clear();
            if (store != null)
            {
                IList<Vector3> qs = store.PointsOf(PlaceKind.DrinkBarQueue);
                for (int i = 0; i < qs.Count; i++) _drinkQueues.Add(new Vector3(qs[i].x, 0f, qs[i].z));
            }
            _snapFallbacks = 0;
            bool narrow;
            BeginLeg(RouteTo(0, tableId, out narrow), _tablePoint, narrow);
        }

        /// <summary>次の卓へ向かう。もう無ければ受け渡し口へ戻る</summary>
        void NextStopOrReturn()
        {
            if (_stopIndex + 1 < _stops.Count)
            {
                int prevTableId = _stops[_stopIndex].tableId;
                _stopIndex++;
                RunStop s = _stops[_stopIndex];
                TableId = s.tableId;
                _tablePoint = s.point;
                _aisleEntrance = s.aisleEntrance;
                _aisleZone = s.aisleZone;
                _hasAisleZone = s.aisleZone.size.x > 0.01f && s.aisleZone.size.z > 0.01f;
                _enteredAisle = !(spec != null && spec.aisleYielding) || !_hasAisleZone;
                _yieldHold = false;
                _escapeToEntrance = false;
                _lastEscapeAtNoPathS = 0f;
                _phase = Phase.ToTable;
                bool narrowNext;
                BeginLeg(RouteTo(prevTableId, s.tableId, out narrowNext), _tablePoint, narrowNext);
                return;
            }
            _phase = Phase.ToPass;
            _escapeToEntrance = false;
            _lastEscapeAtNoPathS = 0f;
            bool narrowBack;
            List<Vector3> back = RouteToPass(_stops[_stopIndex].tableId, out narrowBack);

            // 受け渡し口の前の点を通らず、その少し下を通って右の待機場所へ抜ける（v5）。
            // 線が通れる床に乗らないときは、今までどおり受け渡し口の前へ戻る
            List<Vector3> line = ReturnLine(back);
            _returnLineUsed = line != null;
            if (line != null) back = line;

            BeginLeg(back, _returnLineUsed ? _standbyPoint : _passPoint, narrowBack);
        }

        // ---------------- 戻りの線（v5）----------------

        /// <summary>
        /// 戻る台の線。受け渡し口の前の点のかわりに、その少し下（passReturnOffsetM）の点へ入り、
        /// 右へ抜けて自分の待機場所で終わる。
        ///
        /// 1006 の回で、受け渡し口の前（x 14.0〜15.8 / z 4.8〜6.6）の「もう1台のロボット」が
        /// 209 回あり、そのうち 2 号機の 182 回では、相手の 91% が
        /// 「戻ってきた台」か「待機場所にいる台」だった。出る台と戻る台がここで交差していたため。
        ///
        /// サイトの道順は変えない（最後の点だけ差し替える）。
        /// 線が使えないときは null を返し、今までどおり受け渡し口の前へ戻る。
        /// </summary>
        List<Vector3> ReturnLine(List<Vector3> routeToPass)
        {
            // 依頼 v9：戻りの線は SiteRoutes のときだけ働く。
            // MapPlanner では地図の上で受け渡し口まで自分で引く（後から補う線は使わない）
            if (UseMapPlanner) return null;
            if (spec == null || !spec.useReturnLine || !_hasStandby) return null;
            if (routeToPass == null || routeToPass.Count < 2) return null;

            Vector3 pass = _passPoint; pass.y = 0f;
            float z = pass.z - spec.passReturnOffsetM;
            Vector3 p1 = new Vector3(pass.x, 0f, z);
            Vector3 p2 = new Vector3(_standbyPoint.x, 0f, z);
            Vector3 p3 = _standbyPoint;

            List<Vector3> pts = new List<Vector3>(routeToPass);
            pts[pts.Count - 1] = p1;        // 受け渡し口の前の点を、その少し下へ差し替える
            pts.Add(p2);
            pts.Add(p3);

            // 道順から分かれたあとの区間だけを確かめる（分かれる前はサイトの道順そのまま）
            int from = Mathf.Max(0, pts.Count - 4);
            if (!ReturnLineWalkable(pts, from) ||
                !ReturnLineClear(p1, p2, p3))
            {
                _returnLineFallbacks++;
                return null;
            }
            return pts;
        }

        /// <summary>戻りの線が、ロボット用の焼いた面に乗っているか（刻みで調べる）</summary>
        bool ReturnLineWalkable(List<Vector3> pts, int from)
        {
            float step = Mathf.Max(0.01f, spec.passReturnCheckStepM);
            NavMeshHit hit;

            for (int i = from; i < pts.Count - 1; i++)
            {
                Vector3 a = pts[i], b = pts[i + 1];
                float len = Vector3.Distance(a, b);
                int n = Mathf.Max(1, Mathf.CeilToInt(len / step));
                for (int k = 0; k <= n; k++)
                {
                    Vector3 p = Vector3.Lerp(a, b, (float)k / n);
                    if (!NavMesh.SamplePosition(p, out hit, spec.Radius, RobotFilter)) return false;
                }
            }
            return true;
        }

        /// <summary>
        /// 戻りの線が、出ていく台の線（受け渡し口の前の点を通る線）と
        /// ドリンクバーの並ぶ点から、決めた距離だけ離れているか。
        /// 離れを見るのは、道順から分かれたあとの3点と、そのあいだだけ
        /// （分かれる前はサイトの道順そのままなので、出る線と重なっていてよい）。
        /// </summary>
        bool ReturnLineClear(Vector3 p1, Vector3 p2, Vector3 p3)
        {
            float step = Mathf.Max(0.01f, spec.passReturnCheckStepM);
            Vector3 pass = _passPoint; pass.y = 0f;
            Vector3[] line = { p1, p2, p3 };

            for (int i = 0; i < line.Length - 1; i++)
            {
                float len = Vector3.Distance(line[i], line[i + 1]);
                int n = Mathf.Max(1, Mathf.CeilToInt(len / step));
                for (int k = 0; k <= n; k++)
                {
                    Vector3 p = Vector3.Lerp(line[i], line[i + 1], (float)k / n);
                    if (Vector3.Distance(p, pass) < spec.passReturnSeparationM) return false;
                    for (int q = 0; q < _drinkQueues.Count; q++)
                        if (Vector3.Distance(p, _drinkQueues[q]) < spec.passReturnKeepAwayM) return false;
                }
            }
            return true;
        }

        /// <summary>往復を打ち切る</summary>
        public void AbortTrip()
        {
            _legPoints = null;
            if (_phase == Phase.Idle || _phase == Phase.Done) return;
            Aborted = true;
            Finish();
        }

        void Update()
        {
            if (spec == null) return;
            if (_phase == Phase.Idle || _phase == Phase.Done)
            {
                if (_pathLine != null) _pathLine.Hide();   // 走っていないときは線を消す（6 章）
                return;
            }

            float dt = Time.deltaTime;
            if (dt <= 0f) return;

            LastStepM = 0f;

            FrontSensor.Reading reading = sensor != null ? sensor.Sense() : new FrontSensor.Reading();
            if (reading.sawRobot) MetRobot = true;

            // 卓での25秒。測りの時間には入れない
            if (_phase == Phase.Dwell)
            {
                _dwellTimer += dt;
                if (_dwellTimer >= spec.tableDwellSec) NextStopOrReturn();
                SyncAgent();
                return;
            }

            TotalS += dt;
            UpdateClearance();

            // 1往復の打ち切り（この往復は timeout として1行に残す）
            if (TotalS > spec.maxTripSec)
            {
                TimedOut = true;
                // ほかの台と重ならない所へ置き直す（依頼 v8b の A）。
                // 前は必ず受け渡し口の点に置いていたので、2台が打ち切られると
                // 同じ点で重なり、お互いを「もう1台のロボット」として止まり続けていた
                Vector3 to = PickResetPoint();
                PlaceAt(to, transform.rotation);
                Finish();
                return;
            }

            // 動けなくなって、スタッフが来るのを待っているあいだ（v5 3章）。
            // 受け渡し口へ移す（warp）のはやめた。スタッフが歩いて来て rescue_s 動かすと
            // Rescued() が呼ばれ、止まっていた所から走りを続ける。
            // 待っているあいだは「止まっていた」に数える。
            // いつまでも来ないときは、上の maxTripSec の打ち切りが働く（timeout=1）。
            if (_phase == Phase.StuckWait)
            {
                StoppedS += dt;
                StuckWaitS += dt;
                _stuckWaitTimer += dt;
                SyncAgent();
                return;
            }

            // ---- 通路の入口へ出ている途中（B7）。着いたら、もう一度もとの行き先を目指す ----
            // MapPlanner では escape を使わない（依頼 v9 の 3 章）
            if (!UseMapPlanner) UpdateEscape();

            // ---- ロボットどうしのすれ違い（依頼 v13 の 依頼1）----
            // 向かい合った2台が、止まらずに速さを落として右へ寄れるかを見る。
            // passByEnabled が切なら何もしない（cs:3 と同じ動き）
            UpdatePassBy(dt);

            // ---- 台どうしでふさぎ合ったら、決まった順で片方が譲る ----
            // MapPlanner では「料理を載せている方が先」（依頼 v9 の 4 章）、
            // SiteRoutes ではこれまでの「番号の小さい台が先」（v5〜v8b）
            //
            // すれ違いの走り方に入っているあいだは、譲り合いに入らない（依頼 v13）。
            // 寄っても幅が足りないときだけ、今までどおりここに任せる
            if (!PassByMoving)
            {
                if (UseMapPlanner) UpdateRobotYieldMap(dt, reading);
                else UpdateRobotYield(dt, reading);
            }

            // ---- 細い通路の譲り合い ----
            bool waitingAtEntrance = !_robotYield && !_mapYield && UpdateAisleYielding();

            // ---- 走る・止まる ----
            bool blocked = reading.blocked;

            // すれ違い中は、前にいるのが相手のロボットだけなら、体どうしの間
            // （passBySeparationM）まで全停止しない（依頼 v13）。
            // 人・椅子が前にいるときは、今までどおり止まる
            if (PassByMoving && blocked && reading.nearestIsRobot &&
                !reading.sawPerson && !reading.sawChair &&
                reading.nearestDistance > spec.passBySeparationM)
                blocked = false;

            // 原因は、ブレーキを踏み始めた瞬間（前が塞がったと分かった瞬間）のものを覚えておく。
            // 速さが0になるまで待つと、そのあいだに相手が通り過ぎて「分からない」になってしまう
            if (blocked && !_wasBlocked)
            {
                _brakeCause = CauseOf(reading);
                _brakeDetail = string.IsNullOrEmpty(reading.nearestName) ? "" : reading.nearestName;
                _hasBrakeCause = true;
            }
            _wasBlocked = blocked;

            if (blocked) _clearTimer = 0f;
            else _clearTimer += dt;

            // 行き先への道が引けていないときも止まる（進めないのに走っている扱いにしない）
            bool hasPath = _corners.Length > 0 && _cornerIndex < _corners.Length;
            bool wantMove = !blocked && _clearTimer >= spec.resumeAfterClearSec && !waitingAtEntrance && hasPath;

            // すれ違い中は速さの上限を passBySpeedMps にする（依頼 v13）。止まらない
            float topSpeed = PassByMoving
                ? Mathf.Min(spec.maxSpeed, spec.passBySpeedMps) : spec.maxSpeed;

            // 2026-10-26（直し②-a）：1コマで体どうしの間を passBySeparationM より
            // 詰めない速さにする。2台が近づくので間は 2 台ぶんの速さで縮むため、
            // 残りを 2 で割る。これで 0.1 m を行き過ぎて 0.06 m になるのを防ぐ
            if (PassByMoving && _passByOther != null && dt > 0f)
            {
                float room = Gap(_passByOther) - spec.passBySeparationM;
                if (room < 0f) room = 0f;
                float cap = room / (2f * dt);
                if (cap < topSpeed) topSpeed = cap;
            }

            float target = wantMove ? topSpeed : 0f;
            float prevSpeed = _speed;
            if (target > _speed) _speed = Mathf.Min(target, _speed + spec.acceleration * dt);
            else _speed = Mathf.Max(target, _speed - spec.deceleration * dt);

            float decel = (prevSpeed - _speed) / dt;
            if (decel > MaxDecelMps2) MaxDecelMps2 = decel;

            if (_speed <= spec.stoppedSpeedEpsilon)
            {
                StoppedS += dt;

                if (waitingAtEntrance)
                {
                    // 譲り合いで入口に待っている分。止まった回数・長さには数えない
                    WaitAisleS += dt;
                    _stoppedTimer = 0f;
                    _lastReplanAtStopped = 0f;
                    if (!_inWait)
                    {
                        _inWait = true;
                        _currentWaitS = 0f;
                        BeginStopRecord(StopCause.AisleQueue,
                            _aisleBlockerName.Length > 0 ? "先に入っている台：" + _aisleBlockerName : "順番待ち", false);
                    }
                    _currentWaitS += dt;
                    _open.yieldS += dt;
                }
                else
                {
                    if (_inWait) { _inWait = false; EndStopRecord(_currentWaitS, StopResolve.AisleWait); }

                    _stoppedTimer += dt;
                    if (!_inStop)
                    {
                        _inStop = true;
                        _currentStopS = 0f;
                        _replannedInStop = false;
                        _lastEscapeAtNoPathS = 0f;   // no_path_s は止まりごとに数え直す（B7）

                        string detail;
                        StopCause cause;
                        // 道をゆずって下がっているあいだは、相手の台が原因（v5）。
                        // 下がり終えると道が引けていない状態になるが、それは「幅が足りない」ではない
                        if (_robotYield)
                        {
                            cause = StopCause.OtherRobot;
                            detail = _robotYieldTo != null
                                ? "道をゆずって下がった：" + _robotYieldTo.name : "道をゆずって下がった";
                        }
                        else if (_mapYield)
                        {
                            // よける場所へ動いて相手を待っているあいだ（依頼 v9 の 4 章）
                            cause = StopCause.OtherRobot;
                            detail = _mapYieldTo != null
                                ? "よける場所で待った：" + _mapYieldTo.name : "よける場所で待った";
                        }
                        else if (!hasPath) cause = CauseOfNoPath(out detail);
                        else if (_hasBrakeCause)
                        {
                            cause = _brakeCause;      // ブレーキを踏み始めたときに見たもの
                            detail = _brakeDetail;
                        }
                        else
                        {
                            cause = CauseOf(reading);
                            detail = string.IsNullOrEmpty(reading.nearestName) ? "" : reading.nearestName;
                        }
                        BeginStopRecord(cause, detail, !hasPath);
                        _hasBrakeCause = false;
                    }
                    _currentStopS += dt;

                    // 何で動けないかを秒数で分けて残す（B7 を調べるため）。
                    // 止まる決まりは変えない。数えるだけ
                    if (blocked) _open.blockedS += dt;
                    if (!hasPath) _open.noPathS += dt;
                    if (!blocked && hasPath && _clearTimer < spec.resumeAfterClearSec)
                        _open.clearWaitS += dt;

                    if (UseMapPlanner)
                    {
                        // 依頼 v9 の 3 章：止まって 3 秒たっても前が空かなければ回り込む。
                        // 「10 秒ごとに引き直す」と escape は使わない
                        UpdateDetour(dt, blocked);
                    }
                    else
                    {
                        // 止まって10秒ごとに経路を引き直す
                        if (_stoppedTimer - _lastReplanAtStopped >= spec.replanAfterStoppedSec)
                        {
                            _lastReplanAtStopped = _stoppedTimer;
                            _replannedInStop = true;
                            _open.replans++;
                            SetDestination(_destination);
                        }
                    }

                    // 途中までの経路に縛られているとき（B7）。
                    // 「道が引けていなかった秒数」がしきい値を超えたら、
                    // 引き直しの間隔（repathRetrySec）を待たずに強く引き直す。
                    // それでも途中までしか引けなければ、いちど通路の入口を目指して出る。
                    if (!UseMapPlanner && !hasPath && !_escapeToEntrance &&
                        _open.noPathS - _lastEscapeAtNoPathS >= spec.noPathForceReplanSec)
                    {
                        EscapePartialPath();
                    }

                    // 60秒動けなかったら「動けなくなった」
                    if (_stoppedTimer >= spec.stuckAfterStoppedSec)
                    {
                        EnterStuck();
                        SyncAgent();
                        return;
                    }
                }
            }
            else
            {
                if (_inWait) { _inWait = false; EndStopRecord(_currentWaitS, StopResolve.AisleWait); }
                if (_inStop)
                {
                    _inStop = false;
                    StopCount++;
                    _stopDurations.Add(_currentStopS);
                    EndStopRecord(_currentStopS, ResolveOfStop());
                }
                _stoppedTimer = 0f;
                _lastReplanAtStopped = 0f;
                _blockedFrontS = 0f;
            }

            FaceAndMove(dt);
            SyncAgent();
            UpdatePathLine();
            CheckArrival();
        }

        // ---------------- 見せる（依頼 v9 の 6 章）----------------

        RobotPathLine _pathLine;

        /// <summary>
        /// いま引いている道を床の上に細い線で出す。
        /// 見た目だけで、動きには効かない。RobotSpec.showPathLine で切れる
        /// </summary>
        void UpdatePathLine()
        {
            if (spec == null || !spec.showPathLine)
            {
                if (_pathLine != null) _pathLine.Hide();
                return;
            }
            if (_pathLine == null)
            {
                _pathLine = RobotPathLine.Attach(transform, spec.pathLineWidthM);
                if (_pathLine == null) return;
            }

            int n = WayCount, i = WayIndex;
            if (n == 0 || i >= n) { _pathLine.Hide(); return; }

            _lineBuf.Clear();
            for (int k = i; k < n; k++) _lineBuf.Add(WayPoint(k));
            // 料理を載せているとき（卓へ向かっている）と空のときで色を変える
            _pathLine.Show(_lineBuf, 0, transform.position,
                           YieldRank == 0, spec.pathLineHeightM);
        }

        readonly List<Vector3> _lineBuf = new List<Vector3>();

        // ---------------- 台どうしの譲り合い（v5）----------------

        /// <summary>
        /// 2台がふさぎ合ったとき、決まった順で片方が譲る。
        ///
        /// 実際の配膳ロボットは台どうしで連絡を取り合い、片方が道を譲る。
        /// ここでは **番号の小さい台が先**。番号の大きい台が譲る。
        ///
        /// 2026-10-23：譲る側は **後ろへ下がるのではなく、相手の道から横へ
        /// yieldSideClearM（0.6 m）以上どいた点で待ち、相手が通り過ぎたら出る**。
        /// 前は来た道へ下がっていたので、相手も同じ道を来ると向かい合ったまま溜まっていた。
        /// どく点が無ければ、手前の分かれ道（道順の1つ前の点）の外で待つ。
        /// 待ちすぎたら（yieldSideWaitMaxSec）あきらめて進む。
        /// </summary>
        void UpdateRobotYield(float dt, FrontSensor.Reading reading)
        {
            if (_robotYield)
            {
                _robotYieldS += dt;

                bool passed = _robotYieldTo == null || Gap(_robotYieldTo) >= spec.robotYieldClearM;
                float limit = spec.yieldOrderEnabled && spec.yieldSideWaitMaxSec > 0f
                    ? spec.yieldSideWaitMaxSec : spec.robotYieldMaxSec;
                bool tooLong = limit > 0f && _robotYieldS >= limit;
                if (passed || tooLong)
                {
                    if (ops != null)
                        ops.AddYield(_robotYieldStartS, _robotYieldAt,
                                     "ロボット" + robotId + "号機", RobotSpec.RankRobot,
                                     _robotYieldTo != null ? "ロボット" + _robotYieldTo.robotId + "号機" : "",
                                     _robotYieldS, _robotYieldStepped, passed);

                    _robotYield = false;
                    _robotYieldTo = null;
                    _robotYieldS = 0f;
                    _blockedByRobotS = 0f;
                    ResumeLeg(_phase == Phase.ToPass ? _passPoint : _tablePoint);
                }
                return;
            }

            // ふさいでいるのがもう1台か
            ServingRobotController other = reading.blocked && reading.nearestIsRobot
                ? NearestOtherRobot() : null;
            if (other == null) { _blockedByRobotS = 0f; return; }

            _blockedByRobotS += dt;
            if (_blockedByRobotS < spec.robotYieldAfterSec) return;

            // 番号の小さい台が先。自分が小さければ譲らない（相手がどく）
            if (robotId <= other.robotId) return;

            _robotYield = true;
            _robotYieldS = 0f;
            _blockedByRobotS = 0f;
            _robotYieldTo = other;
            _robotYieldStartS = ops != null ? ops.NowS : 0f;
            _robotYieldAt = transform.position;

            Vector3 goal;
            _robotYieldStepped = TryYieldSidePoint(other, out goal);
            if (!_robotYieldStepped) goal = YieldBackPoint(other);
            SetDestination(goal, true);
        }

        /// <summary>
        /// 相手の道から横へ yieldSideClearM 以上どいた点を探す（2026-10-23）。
        /// 相手がいまたどっている線に直角の向きへ、近い順に試す。
        /// ロボットが通れる面に乗り、寄せたあとも横の距離が足りているものだけを使う。
        /// </summary>
        bool TryYieldSidePoint(ServingRobotController other, out Vector3 point)
        {
            point = transform.position;
            // 依頼 v9：横へどく点（yieldOrder）は SiteRoutes のときだけ働く。
            // MapPlanner では 4 章のよける場所（地図のマスから選ぶ）を使う
            if (UseMapPlanner) return false;
            if (spec == null || !spec.yieldOrderEnabled || other == null) return false;

            Vector3 a, b;
            if (!other.CurrentLegSegment(out a, out b)) return false;

            if (_sideCandidates == null) _sideCandidates = new List<Vector3>();
            YieldSide.Candidates(a, b, transform.position,
                                 spec.yieldSideClearM + spec.Radius,
                                 spec.yieldSideSearchM, spec.yieldSideStepM,
                                 _sideCandidates);

            NavMeshHit hit;
            for (int i = 0; i < _sideCandidates.Count; i++)
            {
                if (!NavMesh.SamplePosition(_sideCandidates[i], out hit, spec.yieldSideStepM, RobotFilter))
                    continue;
                if (!YieldSide.FarEnough(a, b, hit.position, spec.yieldSideClearM + spec.Radius))
                    continue;
                point = hit.position;
                return true;
            }
            return false;
        }

        List<Vector3> _sideCandidates;

        // ---------------- 打ち切りで置き直す先（依頼 v8b の A）----------------

        /// <summary>
        /// 打ち切りで置き直した先（trips.csv の reset_to）。
        /// pass / standby1 / standby2 …。打ち切りでない往復は空
        /// </summary>
        public string ResetTo { get; private set; }

        /// <summary>
        /// 置く先を選ぶ。ほかの台が近く（直径＋余裕）にいない最初の点。
        ///   受け渡し口の点 → 待機場所を順に
        /// どこも空いていなければ、ほかの台からいちばん遠い待機場所
        /// （待機場所が無ければ受け渡し口の点）。
        /// </summary>
        Vector3 PickResetPoint()
        {
            float near = spec != null ? spec.diameter + ResetClearMarginM : 0.8f;

            IList<Vector3> standby = ops != null && ops.store != null
                ? ops.store.RobotStandbyPoints : null;

            // 1. 受け渡し口の点
            if (NearestRobotDistance(_passPoint) > near) { ResetTo = "pass"; return _passPoint; }

            // 2. 待機場所を順に
            if (standby != null)
            {
                for (int i = 0; i < standby.Count; i++)
                {
                    if (NearestRobotDistance(standby[i]) <= near) continue;
                    ResetTo = "standby" + (i + 1);
                    return standby[i];
                }

                // 3. どこも空いていない。ほかの台からいちばん遠い待機場所
                int best = -1;
                float bestD = -1f;
                for (int i = 0; i < standby.Count; i++)
                {
                    float d = NearestRobotDistance(standby[i]);
                    if (d <= bestD) continue;
                    best = i; bestD = d;
                }
                if (best >= 0)
                {
                    ResetTo = "standby" + (best + 1);
                    return standby[best];
                }
            }

            ResetTo = "pass";
            return _passPoint;
        }

        /// <summary>置く先として見るときの、直径に足す余裕（m）</summary>
        const float ResetClearMarginM = 0.3f;

        /// <summary>その点から、いちばん近いほかの台までの距離（m）。ほかに台が無ければ大きい値</summary>
        float NearestRobotDistance(Vector3 p)
        {
            p.y = 0f;
            float best = float.MaxValue;
            for (int i = 0; i < _all.Count; i++)
            {
                ServingRobotController o = _all[i];
                if (o == null || o == this) continue;
                Vector3 q = o.transform.position; q.y = 0f;
                float d = Vector3.Distance(p, q);
                if (d < best) best = d;
            }
            return best;
        }

        /// <summary>相手の外周と自分の外周のあいだの空き（m）</summary>
        float Gap(ServingRobotController other)
        {
            Vector3 d = other.transform.position - transform.position;
            d.y = 0f;
            return d.magnitude - spec.Radius * 2f;
        }

        /// <summary>いちばん近いもう1台（譲る相手を決めるのに使う）</summary>
        ServingRobotController NearestOtherRobot()
        {
            ServingRobotController best = null;
            float bestD = float.MaxValue;
            for (int i = 0; i < _all.Count; i++)
            {
                ServingRobotController o = _all[i];
                if (o == null || o == this) continue;
                Vector3 d = o.transform.position - transform.position;
                d.y = 0f;
                float m = d.magnitude;
                if (m >= bestD) continue;
                best = o; bestD = m;
            }
            return best;
        }

        /// <summary>
        /// 横へどく点が無かったときの逃げ道。
        /// 手前の分かれ道（道順の1つ前の点）まで戻って、そこで待つ。
        /// それも無ければ、相手と反対の向きへ robotYieldBackM 下がる。
        /// </summary>
        Vector3 YieldBackPoint(ServingRobotController other)
        {
            NavMeshHit hit;

            if (_legPoints != null && _legIndex > 0 && _legIndex - 1 < _legPoints.Count)
            {
                Vector3 back = _legPoints[_legIndex - 1];
                if (NavMesh.SamplePosition(back, out hit, spec.Radius * 2f, RobotFilter))
                    return hit.position;
            }

            Vector3 away = transform.position - other.transform.position;
            away.y = 0f;
            if (away.sqrMagnitude < 0.0001f) away = -transform.forward;
            away.Normalize();
            Vector3 p = transform.position + away * spec.robotYieldBackM;
            if (NavMesh.SamplePosition(p, out hit, spec.Radius * 2f, RobotFilter)) return hit.position;
            return transform.position;
        }

        /// <summary>
        /// 細い通路に入る前の譲り合い。
        /// 行き先の通路にもう1台がいたら、太い通路側の入口で待つ。
        /// 戻り（ToPass）では譲らない（出ないと詰まるため）。
        /// </summary>
        bool UpdateAisleYielding()
        {
            if (_phase != Phase.ToTable) return false;
            if (!spec.aisleYielding || !_hasAisleZone || _enteredAisle) return false;

            if (ContainsXZ(_aisleZone, transform.position))
            {
                _enteredAisle = true;
                if (_yieldHold)
                {
                    _yieldHold = false;
                    ResumeLeg(_tablePoint);
                }
                return false;
            }

            // 待ちすぎたら譲らずに進む（人の robotYieldMaxSec と同じ考え方。v5）。
            // この区間のあいだは、もう譲らない
            if (_yieldHold)
            {
                _yieldHoldS += Time.deltaTime;
                if (spec.aisleWaitMaxSec > 0f && _yieldHoldS >= spec.aisleWaitMaxSec)
                {
                    _yieldGaveUp = true;
                    _yieldHold = false;
                    _yieldHoldS = 0f;
                    ResumeLeg(_tablePoint);
                    return false;
                }
            }

            if (!_yieldGaveUp && MustWaitForAisle())
            {
                if (!_yieldHold)
                {
                    _yieldHold = true;
                    _yieldHoldS = 0f;
                    // 入口の点そのものに立つと、中の台が出られなくなる。
                    // 通路の外へ下がり、出てくる道をふさがない所で待つ
                    _aisleWaitPoint = AisleWaitPoint();
                    SetDestination(_aisleWaitPoint, true);
                }
            }
            else if (_yieldHold)
            {
                _yieldHold = false;
                _yieldHoldS = 0f;
                ResumeLeg(_tablePoint);
            }

            if (!_yieldHold) return false;

            Vector3 a = transform.position; a.y = 0f;
            Vector3 b = _aisleWaitPoint; b.y = 0f;
            return Vector3.Distance(a, b) <= spec.arriveRadius;
        }

        /// <summary>
        /// 通路の入口で待つ点（v5）。入口の点から通路の外へ下がり、横へずれる。
        /// 中の台が出てくる道をふさがないため。焼いた面に乗らなければ、下がるだけ・入口そのものの順に落とす。
        /// </summary>
        Vector3 AisleWaitPoint()
        {
            Vector3 entrance = _aisleEntrance; entrance.y = 0f;
            if (!_hasAisleZone) return entrance;

            Vector3 center = _aisleZone.center; center.y = 0f;
            Vector3 outward = entrance - center;
            outward.y = 0f;
            if (outward.sqrMagnitude < 0.0001f) return entrance;
            outward.Normalize();
            Vector3 side = new Vector3(-outward.z, 0f, outward.x);

            Vector3 back = entrance + outward * spec.aisleWaitSetbackM;
            Vector3[] tries =
            {
                back + side * spec.aisleWaitSideM,
                back - side * spec.aisleWaitSideM,
                back
            };

            NavMeshHit hit;
            for (int i = 0; i < tries.Length; i++)
                if (NavMesh.SamplePosition(tries[i], out hit, spec.Radius, RobotFilter)) return hit.position;

            return entrance;
        }

        /// <summary>
        /// 途中までの経路に縛られた止まりから出る（B7）。
        ///   1. 引き直しの間隔（repathRetrySec）を待たずに強く引き直す
        ///   2. それでも最後までつながらなければ、いちど通路の入口を目指して出る
        /// どちらも「経路の引き直し」なので、この止まりは resolved が「経路を引き直した」になる。
        /// cause_detail には「途中までの経路」の印を足す。
        /// </summary>
        void EscapePartialPath()
        {
            _lastEscapeAtNoPathS = _open.noPathS;
            _replannedInStop = true;
            _open.replans++;
            MarkPartialPath();

            // 1. 間隔を無視して引き直す
            SetDestination(_destination, true);
            if (_pathValid && _cornerIndex < _corners.Length) return;

            // 2. それでも途中まで。通路の入口へ出る
            if (!spec.escapeToAisleEntranceOnPartialPath) return;

            Vector3 here = transform.position; here.y = 0f;
            Vector3 exit = _aisleEntrance; exit.y = 0f;
            if (Vector3.Distance(here, exit) <= spec.arriveRadius) return;

            _escapeToEntrance = true;
            SetDestination(_aisleEntrance, true);
        }

        /// <summary>通路の入口へ出ている途中。着いたら、もとの行き先をもう一度目指す</summary>
        void UpdateEscape()
        {
            if (!_escapeToEntrance) return;

            Vector3 here = transform.position; here.y = 0f;
            Vector3 exit = _aisleEntrance; exit.y = 0f;
            if (Vector3.Distance(here, exit) > spec.arriveRadius) return;

            _escapeToEntrance = false;
            ResumeLeg(_phase == Phase.ToPass ? _passPoint : _tablePoint);
        }

        /// <summary>この止まりに「途中までの経路」の印をつける（1回だけ）</summary>
        void MarkPartialPath()
        {
            if (!_recordingStop) return;
            string d = _open.causeDetail;
            if (!string.IsNullOrEmpty(d) && d.Contains(PartialPathMark)) return;
            _open.causeDetail = string.IsNullOrEmpty(d) ? PartialPathMark : d + "／" + PartialPathMark;
        }

        bool MustWaitForAisle()
        {
            for (int i = 0; i < _all.Count; i++)
            {
                ServingRobotController o = _all[i];
                if (o == null || o == this) continue;

                // その通路の中にいる台がいれば入らない
                if (ContainsXZ(_aisleZone, o.transform.position)) { _aisleBlockerName = o.name; return true; }

                // 同じ通路を目指していて、まだ入っていない台。番号の小さい方が先に入る
                if (o._phase == Phase.ToTable && !o._enteredAisle && o._hasAisleZone &&
                    SameZone(o._aisleZone, _aisleZone) && o.robotId < robotId)
                {
                    _aisleBlockerName = o.name;
                    return true;
                }
            }
            _aisleBlockerName = "";
            return false;
        }

        static bool ContainsXZ(Bounds b, Vector3 p)
        {
            return p.x >= b.min.x && p.x <= b.max.x && p.z >= b.min.z && p.z <= b.max.z;
        }

        static bool SameZone(Bounds a, Bounds b)
        {
            Vector3 d = a.center - b.center;
            d.y = 0f;
            return d.sqrMagnitude < 0.0025f;   // 5 cm 以内なら同じ通路
        }

        /// <summary>
        /// 動けなくなった。スタッフを呼んで、来るまで待つ（v5 3章）。
        /// Stuck は「この往復で1回でも動けなくなったか」の印なので、1往復に1回だけ立てる。
        /// 呼ぶ方は何度でも起こりうる（動かしてもらった先でまた動けなくなる）ので、
        /// 呼び出しは Stuck の印とは別に毎回出す。
        /// </summary>
        void EnterStuck()
        {
            if (_phase == Phase.StuckWait) return;   // すでに待っている
            Stuck = true;
            _mapYield = false;
            _mapYieldTo = null;
            _headOnS = 0f;
            if (_inStop) EndStopRecord(_currentStopS, StopResolve.Stuck);
            // 動けなくなった分は「自分で動き出した止まり」には数えない
            _inStop = false;
            _currentStopS = 0f;
            _stuckWaitTimer = 0f;
            _escapeToEntrance = false;
            _lastEscapeAtNoPathS = 0f;
            StuckCount++;
            _phaseBeforeStuck = _phase;
            _phase = Phase.StuckWait;
            if (ops != null) ops.PostRescue(this);
        }

        /// <summary>
        /// スタッフが来て rescue_s 動かし終わった（StaffAgent が呼ぶ。v5 3章）。
        /// 止まっていた所から、同じ道順の続きを走り直す。道順そのものは変えない。
        /// </summary>
        public void Rescued()
        {
            if (_phase != Phase.StuckWait) return;
            if (ops != null) ops.RescueFinished(_stuckWaitTimer);
            _stuckWaitTimer = 0f;
            _stoppedTimer = 0f;
            _lastReplanAtStopped = 0f;
            _clearTimer = 0f;
            bool toPass = _phaseBeforeStuck == Phase.ToPass;
            _phase = toPass ? Phase.ToPass : Phase.ToTable;
            ResumeLeg(toPass ? _passPoint : _tablePoint);
        }

        /// <summary>止まった原因を、前のセンサーが見たものから決める</summary>
        StopCause CauseOf(FrontSensor.Reading r)
        {
            if (r.nearestIsRobot) return StopCause.OtherRobot;
            if (r.nearestIsChair) return StopCause.Chair;
            if (r.nearestIsPerson)
            {
                if (r.nearestPersonStanding) return StopCause.PersonStanding;
                return r.nearestPersonKind == PersonKind.Staff ? StopCause.Staff : StopCause.Guest;
            }
            if (r.sawRobot) return StopCause.OtherRobot;
            if (r.sawChair) return StopCause.Chair;
            if (r.sawPerson) return StopCause.Guest;
            return StopCause.Unknown;
        }

        /// <summary>
        /// 行き先への道が引けないときの原因。
        /// ふさいでいるもの（引かれた椅子・もう1台のロボット）が見つかれば、その原因にする。
        /// 何も見つからないときだけ「通路の幅が足りない」にする。
        /// </summary>
        StopCause CauseOfNoPath(out string detail)
        {
            StopCause cause;
            string name;

            // 行き止まりの点 → 行き先 → 自分のまわり、の順に探す
            if (_hasPathEnd && FindBlocker(_pathEndPoint, out cause, out name))
            {
                detail = "経路なし：" + name;
                return cause;
            }
            if (FindBlocker(_destination, out cause, out name))
            {
                detail = "経路なし：" + name;
                return cause;
            }
            if (FindBlocker(transform.position, out cause, out name))
            {
                detail = "経路なし：" + name;
                return cause;
            }

            detail = "経路なし：ふさいでいるものが見つからない";
            return StopCause.NoPath;
        }

        const int BlockMax = 32;
        readonly Collider[] _blockHits = new Collider[BlockMax];

        /// <summary>
        /// その点のまわりで、道をふさぎうるもの（引かれた椅子・ほかのロボット）のうち
        /// いちばん近いものを探す。見つかれば true。
        /// </summary>
        bool FindBlocker(Vector3 around, out StopCause cause, out string name)
        {
            cause = StopCause.NoPath;
            name = "";

            Vector3 center = new Vector3(around.x, spec.clearanceProbeHeight, around.z);
            int n = Physics.OverlapSphereNonAlloc(center, spec.pathBlockSearchRadius, _blockHits,
                                                  ~0, QueryTriggerInteraction.Ignore);
            if (n > BlockMax) n = BlockMax;

            float best = float.MaxValue;
            for (int i = 0; i < n; i++)
            {
                Collider col = _blockHits[i];
                if (col == null) continue;

                Chair chair = col.GetComponentInParent<Chair>();
                if (chair != null)
                {
                    // 引かれていない椅子は NavMesh を削らないので、道をふさがない
                    if (!chair.IsPulled) continue;
                    float d = Vector3.Distance(center, chair.transform.position);
                    if (d < best)
                    {
                        best = d;
                        cause = StopCause.Chair;
                        name = chair.transform.parent != null
                            ? chair.transform.parent.name + "/" + chair.name : chair.name;
                    }
                    continue;
                }

                ServingRobotController other = col.GetComponentInParent<ServingRobotController>();
                if (other != null && other != this)
                {
                    float d = Vector3.Distance(center, other.transform.position);
                    if (d < best)
                    {
                        best = d;
                        cause = StopCause.OtherRobot;
                        name = other.name;
                    }
                }
            }
            return best < float.MaxValue;
        }

        // ---- 道を引いたときの中身（調べるためだけ。動きは変えない）----
        Vector3 _lastSolveFrom, _lastSolveTo;
        string _lastSolveStatus = "";
        int _lastSolveCorners;
        float _lastOffMeshM = -1f;

        /// <summary>
        /// いま開いている止まりの記録に、最後に道を引いたときの中身を写す。
        /// 止まっているあいだは 10 秒ごとに引き直すので、最後の1回ぶんが残る。
        /// 記録するだけで、止まる・動くの決まりには使わない。
        /// </summary>
        void RecordSolveIntoOpenStop()
        {
            if (!_recordingStop) return;
            _open.solveFrom = _lastSolveFrom;
            _open.solveTo = _lastSolveTo;
            _open.solveStatus = _lastSolveStatus;
            _open.solveCorners = _lastSolveCorners;
            _open.offMeshM = _lastOffMeshM;
        }

        /// <summary>
        /// この止まりの終わり方。
        /// 依頼 v9 の 3 章（回り込んで動き出した）・4 章（よける場所へ動いた）が働いたら、
        /// 「経路を引き直した」よりそちらを出す
        /// </summary>
        StopResolve ResolveOfStop()
        {
            if (_hasResolveOverride) return _resolveOverride;
            return _replannedInStop ? StopResolve.Replanned : StopResolve.SelfResume;
        }

        void BeginStopRecord(StopCause cause, string detail, bool pathBlocked)
        {
            _hasResolveOverride = false;
            _open = new StopEvent();
            _open.startTimeS = Time.time;
            _open.position = transform.position;
            _open.cause = cause;
            _open.causeDetail = detail == null ? "" : detail;
            _open.pathBlocked = pathBlocked;
            float left, right;
            MeasureSideClearance(out left, out right);
            _open.clearanceLeftM = left;
            _open.clearanceRightM = right;
            _recordingStop = true;
            RecordSolveIntoOpenStop();   // 止まり始めの時点の、直前に引いた中身
        }

        void EndStopRecord(float durationS, StopResolve resolve)
        {
            if (!_recordingStop) return;
            _recordingStop = false;
            _open.durationS = durationS;
            _open.resolve = resolve;
            _stopEvents.Add(_open);
        }

        /// <summary>
        /// 止まった位置での、左右の空き（ロボットの外周から壁・席・椅子まで）。
        /// 人とロボットは数えない（動くもののため）。
        /// </summary>
        void MeasureSideClearance(out float left, out float right)
        {
            left = ProbeSide(-transform.right);
            right = ProbeSide(transform.right);
        }

        const int ProbeMax = 16;
        readonly RaycastHit[] _probe = new RaycastHit[ProbeMax];

        float ProbeSide(Vector3 dir)
        {
            float range = spec.clearanceProbeRange;
            Vector3 origin = transform.position + Vector3.up * spec.clearanceProbeHeight;
            int n = Physics.RaycastNonAlloc(origin, dir, _probe, range, ~0, QueryTriggerInteraction.Ignore);
            if (n > ProbeMax) n = ProbeMax;

            float best = range;
            for (int i = 0; i < n; i++)
            {
                Collider col = _probe[i].collider;
                if (col == null) continue;
                if (col.GetComponentInParent<PersonBody>() != null) continue;
                if (col.GetComponentInParent<ServingRobotController>() != null) continue;
                if (_probe[i].distance < best) best = _probe[i].distance;
            }
            return Mathf.Max(0f, best - spec.Radius);
        }

        void CheckArrival()
        {
            if (_yieldHold) return;         // いまの行き先は通路の入口なので、着いても卓ではない
            if (_escapeToEntrance) return;  // 途中までの経路から出ている途中（B7）。行き先は通路の入口
            if (_mapYield) return;          // いまの行き先はよける場所（依頼 v9 の 4 章）

            Vector3 here = transform.position; here.y = 0f;
            Vector3 goal = _hasDestinationOnMesh ? _destinationOnMesh : _destination; goal.y = 0f;

            // v5b: 道順の途中の点は、少し手前でも「着いた」として次の点へ進む。
            // 最後の点（卓の止まる位置・受け渡し口）は今までどおり arriveRadius で見る
            bool midWaypoint = _legPoints != null && _legIndex + 1 < _legPoints.Count;
            float reach = midWaypoint
                ? Mathf.Max(spec.arriveRadius, spec.waypointReachDistance)
                : spec.arriveRadius;
            bool near = Vector3.Distance(here, goal) <= reach;

            // 最後までつながった経路の角を使い切ったら、着いたとみなす
            bool consumed = _pathValid && _corners.Length > 0 && _cornerIndex >= _corners.Length;
            if (!near && !consumed) return;

            if (AdvanceWaypoint()) return;     // まだ区間の途中

            if (_phase == Phase.ToTable)
            {
                _phase = Phase.Dwell;
                _dwellTimer = 0f;
                _speed = 0f;
                StopsCompleted = _stopIndex + 1;   // この卓には届いた

                // 卓に着いた時点を残して知らせる（依頼 v8 の 1）。
                // サイトの試算と同じで、ここで皿は「届いた」。
                // 受け渡しの 25 秒（tableDwellSec）はこのあと
                int at = _stopIndex;
                if (at >= 0 && at < _stopArrivedS.Count)
                    _stopArrivedS[at] = ops != null ? ops.NowS : 0f;
                if (StopArrived != null) StopArrived(this, at);
            }
            else if (_phase == Phase.ToPass)
            {
                Finish();
            }
        }

        void Finish()
        {
            _phase = Phase.Done;
            _speed = 0f;
            _legPoints = null;
            _mapYield = false;
            _mapYieldTo = null;
            // 往復が実際に終わった店の中の時刻（卓での25秒を含む。依頼 v9 の 5 章）
            TripEndS = ops != null ? ops.NowS : -1f;
            // まだ呼んだままのスタッフがいたら、呼び出しを下げる（v5 3章）
            if (ops != null) ops.CancelRescue(this);
            if (_inWait) { _inWait = false; EndStopRecord(_currentWaitS, StopResolve.AisleWait); }
            if (_inStop)
            {
                _inStop = false;
                StopCount++;
                _stopDurations.Add(_currentStopS);
                EndStopRecord(_currentStopS, ResolveOfStop());
            }
            if (float.IsInfinity(MinClearanceM)) MinClearanceM = -1f;   // 人がいなかった
            if (TripCompleted != null) TripCompleted(this);
        }

        // ---- サイトの道順（v5b）----
        RouteBook _routes;
        List<Vector3> _legPoints;         // いまの区間の曲がり角の点（最後が行き先そのもの）
        int _legIndex;
        bool _legIsNarrow;                // この区間が 0.8 m の道か（記録用）
        int _snapFallbacks;               // 止まる位置を 0.1 m で面に寄せられず、広げた回数

        /// <summary>いまの区間が 0.8 m の道か（記録用。v5b）</summary>
        public bool UsedNarrowAisle { get { return _legIsNarrow; } }

        /// <summary>
        /// この往復のどこかで 0.8 m の道を使ったか（trips.csv の via_narrow。v5 3章）。
        /// 1往復で何卓も回るので、区間ごとの印（UsedNarrowAisle）とは別に、
        /// 1回でも使ったら立てたままにする。
        /// </summary>
        public bool LegWasNarrow { get; private set; }

        /// <summary>道順の点を 0.1 m で面に寄せられなかった回数（報告用。v5b）</summary>
        public int RouteSnapFallbacks { get { return _snapFallbacks; } }

        /// <summary>
        /// 区間（受け渡し口→卓 / 卓→卓 / 卓→受け渡し口）を始める（v5b）。
        /// サイトの道順があればその点を順にたどり、無ければ行き先まで直接向かう。
        /// いまいる所に近い先頭の点は飛ばす（受け渡し口で始めるときなど）。
        /// </summary>
        void BeginLeg(List<Vector3> points, Vector3 fallbackTarget, bool narrow)
        {
            // サイトの道順をたどったときの長さを足す（走りには使わない。trips.csv の site_route_m）
            SiteRouteM += _lastRouteLenM;
            _lastRouteLenM = 0f;

            _legIsNarrow = narrow;
            if (narrow) LegWasNarrow = true;
            _legPoints = null;
            _legIndex = 0;
            _onDetour = false;
            _blockedFrontS = 0f;

            // 依頼 v9: 地図の上で、行き先まで自分で道を引く。サイトの routes の点はたどらない。
            // 「狭い道を使ったか」は、引いた道に 1 のマスが入ったかで決まる（narrow は使わない）
            if (UseMapPlanner)
            {
                _legIsNarrow = false;
                PlanOnMap(fallbackTarget, MarkMode.None, true, false);
                return;
            }

            if (spec != null && spec.followSiteRoutes && points != null && points.Count >= 2)
            {
                _legPoints = points;
                Vector3 here = transform.position; here.y = 0f;
                // 先頭の点は、いまいる所と同じことが多い（受け渡し口）。近ければ飛ばす
                while (_legIndex < _legPoints.Count - 1)
                {
                    Vector3 p = _legPoints[_legIndex]; p.y = 0f;
                    if (Vector3.Distance(here, p) > spec.waypointReachDistance) break;
                    _legIndex++;
                }
                SetDestination(_legPoints[_legIndex], true);
                return;
            }
            SetDestination(fallbackTarget, true);
        }

        /// <summary>いまの区間の、いま目指している点をもう一度行き先にする（v5b）</summary>
        void ResumeLeg(Vector3 fallbackTarget)
        {
            if (_legPoints != null && _legIndex < _legPoints.Count)
            {
                SetDestination(_legPoints[_legIndex], true);
                return;
            }
            SetDestination(fallbackTarget, true);
        }

        /// <summary>
        /// 道順の途中の点に着いたら、次の点へ進む（v5b）。
        /// 戻り値 true なら、まだ区間の途中（卓にも受け渡し口にも着いていない）。
        /// </summary>
        bool AdvanceWaypoint()
        {
            if (_legPoints == null || _legIndex + 1 >= _legPoints.Count) return false;
            _legIndex++;
            SetDestination(_legPoints[_legIndex], true);
            return true;
        }

        /// <summary>
        /// 直前に引いたサイトの道順の長さ（m）。BeginLeg が SiteRouteM に足す。
        /// MapPlanner でも「サイトの道順をたどったらこの長さ」として記録に残す（依頼 v9 の 5 章）
        /// </summary>
        float _lastRouteLenM;

        /// <summary>その卓へ行く道順（受け渡し口から / 直前の卓から）</summary>
        List<Vector3> RouteTo(int fromTableId, int toTableId, out bool narrow)
        {
            narrow = false;
            _lastRouteLenM = 0f;
            if (_routes == null) return null;
            RouteBook.Route used;
            List<Vector3> pts;
            if (fromTableId <= 0)
            {
                pts = _routes.PointsFromPass(toTableId, out used);
            }
            else
            {
                bool reversed;
                pts = _routes.PointsBetween(fromTableId, toTableId, out used, out reversed);
            }
            if (used != null) { narrow = used.IsNarrow; _lastRouteLenM = used.length; }
            return pts;
        }

        /// <summary>卓 → 受け渡し口の道順</summary>
        List<Vector3> RouteToPass(int fromTableId, out bool narrow)
        {
            narrow = false;
            _lastRouteLenM = 0f;
            if (_routes == null) return null;
            RouteBook.Route used;
            List<Vector3> pts = _routes.PointsToPass(fromTableId, out used);
            if (used != null) { narrow = used.IsNarrow; _lastRouteLenM = used.length; }
            return pts;
        }

        void SetDestination(Vector3 point) { SetDestination(point, false); }

        void SetDestination(Vector3 point, bool force)
        {
            // 依頼 v9: 地図の上で引く（NavMesh はロボットの道探しには使わない）
            if (UseMapPlanner)
            {
                PlanOnMap(point, MarkMode.None, force);
                return;
            }

            // 引き直しの間隔を空ける。いまの経路は消さない
            if (_repathCooldown > 0f && !force)
            {
                _destination = point;
                return;
            }
            _repathCooldown = spec.repathRetrySec;
            if (_path == null) _path = new NavMeshPath();

            _destination = point;
            _pathValid = false;
            _corners = new Vector3[0];
            _cornerIndex = 0;

            // v5b: 道順の最後の点（止まる位置そのもの）だけは寄せない（0.1 m 以下）。
            // 途中の点・通路の入口・道順を使わないときは、今までどおり広めに寄せる。
            bool lastRoutePoint =
                _legPoints != null && _legIndex == _legPoints.Count - 1 &&
                Vector3.Distance(point, _legPoints[_legIndex]) < 0.001f;
            float snap = lastRoutePoint
                ? Mathf.Min(spec.routePointSnapM, spec.Radius * 4f)
                : spec.Radius * 4f;
            SolvePath(point, RobotFilter, snap);
        }

        /// <summary>
        /// 行き先まで道を引き、_path・_corners に入れる。
        /// snap は、点を焼いた面へ寄せるときに探す半径（m）。
        /// 止まる位置がその半径で面に乗らなければ、広げて引き直し、回数を数える（v5b）。
        /// </summary>
        void SolvePath(Vector3 point, NavMeshQueryFilter filter, float snap)
        {
            if (_path == null) _path = new NavMeshPath();

            NavMeshHit hit;
            Vector3 from = transform.position;
            // いまの位置が焼いた面からどれだけ離れているか（調べるためだけ。動きは変えない）
            _lastOffMeshM = NavMesh.SamplePosition(transform.position, out hit, spec.Radius * 4f, filter)
                ? Vector3.Distance(transform.position, hit.position) : -1f;
            if (_lastOffMeshM >= 0f) from = hit.position;

            Vector3 to = point;
            if (NavMesh.SamplePosition(to, out hit, snap, filter)) to = hit.position;
            else if (snap < spec.Radius * 4f)
            {
                _snapFallbacks++;
                if (NavMesh.SamplePosition(to, out hit, spec.Radius * 4f, filter)) to = hit.position;
            }

            _lastSolveFrom = from;
            _lastSolveTo = to;

            if (!NavMesh.CalculatePath(from, to, filter, _path) || _path.corners.Length == 0)
            {
                _lastSolveStatus = "引けない";
                _lastSolveCorners = _path != null ? _path.corners.Length : 0;
                RecordSolveIntoOpenStop();
                return;
            }

            bool complete = _path.status == NavMeshPathStatus.PathComplete;
            _lastSolveStatus = _path.status.ToString();
            _lastSolveCorners = _path.corners.Length;
            RecordSolveIntoOpenStop();

            _destinationOnMesh = to;
            _hasDestinationOnMesh = true;
            _corners = _path.corners;
            _cornerIndex = 0;
            _pathValid = complete;
            if (!complete && _corners.Length > 0)
            {
                _pathEndPoint = _corners[_corners.Length - 1];
                _hasPathEnd = true;
            }
            else _hasPathEnd = false;

            // 足もとの角は飛ばす
            Vector3 here = transform.position; here.y = 0f;
            while (_cornerIndex < _corners.Length)
            {
                Vector3 c = _corners[_cornerIndex]; c.y = 0f;
                if (Vector3.Distance(here, c) > spec.cornerReachDistance) break;
                _cornerIndex++;
            }
        }

        // ================= 依頼 v9：地図の上で自分で道を引く =================

        /// <summary>道を引くときに差し込むもの</summary>
        enum MarkMode
        {
            None,     // そのまま引く
            Detour,   // 近くの人・ロボットを通れないマスとして置く（3 章）
            Avoid     // 優先が高い台の先の道を重くする（4 章）
        }

        /// <summary>
        /// 地図の上で、行き先まで道を引く（依頼 v9 の 2 章）。
        /// 引き直しは 1 台につき replanMinIntervalSec（0.5 秒）に 1 回まで（force で無視できる）。
        /// </summary>
        bool PlanOnMap(Vector3 point, MarkMode marks, bool force)
        {
            return PlanOnMap(point, marks, force, true);
        }

        /// <summary>
        /// isReplan が false のときは「引き直し」に数えない
        /// （区間を始めるときの1回目は引き直しではない）
        /// </summary>
        bool PlanOnMap(Vector3 point, MarkMode marks, bool force, bool isReplan)
        {
            if (_repathCooldown > 0f && !force)
            {
                _destination = point;
                return false;
            }
            _repathCooldown = Mathf.Max(0.01f, spec.replanMinIntervalSec);

            _destination = point;
            _pathValid = false;
            _hasPathEnd = false;
            _corners = NoCorners;
            _cornerIndex = 0;

            _planner.ClearMarks();
            if (marks == MarkMode.Detour) AddDetourMarks();
            else
            {
                // ふつうに引き直したら、もう回り込みの道ではない（モニターの出し分けに使う）
                _onDetour = false;
                if (marks == MarkMode.Avoid) AddAvoidMarks();
            }

            GridPlanner.Status s = _planner.Plan(transform.position, point, spec.PlannerSettings());
            return AfterMapPlan(s, isReplan);
        }

        /// <summary>引いた結果を受け取る。引けたら true</summary>
        bool AfterMapPlan(GridPlanner.Status status, bool isReplan)
        {
            if (isReplan)
            {
                Replans++;
                if (_recordingStop) _open.replans++;
            }
            if (_planner.LastPlanMs > PlanMsMax) PlanMsMax = _planner.LastPlanMs;
            if (_planner.LastStartWasBlocked) MapSnapStarts++;

            // stops.csv に出す中身（調べるためだけ。動きは変えない）
            _lastSolveFrom = transform.position;
            _lastSolveTo = _destination;
            _lastSolveStatus = _planner.StatusText;
            _lastSolveCorners = _planner.Points.Count;
            _lastOffMeshM = _planner.LastStartSnapM;
            RecordSolveIntoOpenStop();

            if (status != GridPlanner.Status.Ok || _planner.Points.Count == 0)
            {
                PlanFails++;
                return false;
            }

            _corners = new Vector3[_planner.Points.Count];
            for (int i = 0; i < _corners.Length; i++) _corners[i] = _planner.Points[i];
            _cornerIndex = 0;
            _pathValid = true;
            _hasDestinationOnMesh = true;
            _destinationOnMesh = _corners[_corners.Length - 1];

            if (_planner.UsedNarrow) { _legIsNarrow = true; LegWasNarrow = true; }

            // 足もとの角は飛ばす（最後の1つは残す。着いたと見るのに使う）
            Vector3 here = transform.position; here.y = 0f;
            while (_cornerIndex < _corners.Length - 1)
            {
                Vector3 c = _corners[_cornerIndex]; c.y = 0f;
                if (Vector3.Distance(here, c) > spec.cornerReachDistance) break;
                _cornerIndex++;
            }
            return true;
        }

        /// <summary>
        /// 回り込むために、近くの人・ほかのロボットをその 1 回だけ通れないマスとして置く（3 章）。
        /// 人は 人の半径＋ロボットの半径＋detourPersonMarginM、
        /// ロボットは 2 台の半径の和＋detourPersonMarginM。
        /// </summary>
        void AddDetourMarks()
        {
            Vector3 me = transform.position; me.y = 0f;
            float near = spec.detourNearM;
            float margin = spec.detourPersonMarginM;

            IReadOnlyList<PersonBody> people = PersonBody.All;
            for (int i = 0; i < people.Count; i++)
            {
                PersonBody p = people[i];
                if (p == null || p.kind == PersonKind.Seated) continue;
                Vector3 q = p.transform.position; q.y = 0f;
                if (Vector3.Distance(me, q) > near) continue;
                _planner.BlockCircle(q, p.radius + spec.Radius + margin);
            }

            for (int i = 0; i < _all.Count; i++)
            {
                ServingRobotController o = _all[i];
                if (o == null || o == this) continue;
                Vector3 q = o.transform.position; q.y = 0f;
                if (Vector3.Distance(me, q) > near) continue;
                _planner.BlockCircle(q, spec.Radius * 2f + margin);
            }
        }

        /// <summary>
        /// 優先が高い台の先の道を重くして、相手の道とかぶらない道を選ばせる（4 章）。
        /// 相手の体は通れないマスとして置く。
        /// </summary>
        void AddAvoidMarks()
        {
            ServingRobotController other = _mapYieldTo;
            if (other == null) return;

            other.PathAhead(spec.robotPathAheadM, _aheadOther);
            float costR = spec.Radius * 2f + spec.robotPathCostMarginM;
            for (int i = 1; i < _aheadOther.Count; i++)
                _planner.CostLine(_aheadOther[i - 1], _aheadOther[i], costR, spec.robotPathCostAdd);

            Vector3 q = other.transform.position; q.y = 0f;
            _planner.BlockCircle(q, spec.Radius * 2f);
        }

        /// <summary>いまの道の、ここから行き先までの残りの長さ（m）</summary>
        float RemainingPathM()
        {
            if (_corners.Length == 0 || _cornerIndex >= _corners.Length)
                return Vector3.Distance(Flat(transform.position), Flat(_destination));

            float sum = 0f;
            Vector3 from = Flat(transform.position);
            for (int i = _cornerIndex; i < _corners.Length; i++)
            {
                Vector3 to = Flat(_corners[i]);
                sum += Vector3.Distance(from, to);
                from = to;
            }
            return sum;
        }

        static Vector3 Flat(Vector3 v) { v.y = 0f; return v; }

        /// <summary>
        /// 前がふさがって止まっているとき（3 章）。
        /// detourAfterSec（3 秒）たっても空かなければ、回り込む道を探す。
        /// 引けなければ待ち、3 秒ごとに探し直す。
        /// 止まる位置そのものに人が立っているときは、回り込まずに待つ。
        /// </summary>
        void UpdateDetour(float dt, bool blocked)
        {
            if (!blocked)
            {
                _blockedFrontS = 0f;
                return;
            }
            _blockedFrontS += dt;
            if (_blockedFrontS < spec.detourAfterSec) return;
            _blockedFrontS = 0f;                       // 次の 3 秒を数え直す
            if (PersonOnGoal()) return;
            TryDetour();
        }

        /// <summary>行き先（止まる位置）そのものに人が立っているか</summary>
        bool PersonOnGoal()
        {
            Vector3 goal = Flat(_destination);
            IReadOnlyList<PersonBody> people = PersonBody.All;
            for (int i = 0; i < people.Count; i++)
            {
                PersonBody p = people[i];
                if (p == null || p.kind == PersonKind.Seated) continue;
                Vector3 q = p.transform.position; q.y = 0f;
                if (Vector3.Distance(goal, q) <= p.radius + spec.Radius) return true;
            }
            return false;
        }

        /// <summary>
        /// 回り込む道を引いてみる。
        /// 延びた距離が大きすぎる道（もとの残りの道の detourMaxFactor 倍＋detourExtraAddM より長い）は
        /// 取らずに待つ。引けなければ、もとの道に戻す。
        /// </summary>
        bool TryDetour()
        {
            float remain = RemainingPathM();
            float limit = remain * spec.detourMaxFactor + spec.detourExtraAddM;

            Vector3[] prevCorners = _corners;
            int prevIndex = _cornerIndex;
            bool prevValid = _pathValid;
            Vector3 prevOnMesh = _destinationOnMesh;
            bool prevHasOnMesh = _hasDestinationOnMesh;

            if (PlanOnMap(_destination, MarkMode.Detour, true) && _planner.LastLengthM <= limit)
            {
                Detours++;
                _onDetour = true;
                SetResolveOverride(StopResolve.Detoured);
                return true;
            }

            // 取らない。もとの道に戻す
            _corners = prevCorners;
            _cornerIndex = prevIndex;
            _pathValid = prevValid;
            _destinationOnMesh = prevOnMesh;
            _hasDestinationOnMesh = prevHasOnMesh;
            return false;
        }

        /// <summary>この止まりの終わり方を上書きする（回り込み・よけ）</summary>
        void SetResolveOverride(StopResolve r)
        {
            _hasResolveOverride = true;
            _resolveOverride = r;
            _replannedInStop = true;
        }

        // ---------------- ロボットどうし（4 章）----------------

        /// <summary>
        /// 優先の順で、片方が道をゆずる（依頼 v9 の 4 章・エイイチの決定）。
        ///   1. 料理を載せているロボットが先
        ///   2. 空で戻るロボットは後
        ///   3. 同じ状態なら 1 号機が先
        ///
        /// 優先が低い側は、相手が robotReplanNearM（3 m）以内に来て先の道がかぶったら、
        /// 相手の道を重くして引き直す。かぶらない道が引けないときは、
        /// 近く（robotYieldSpotSearchM）でよける場所を探してそこで待つ（最大 robotYieldMaxSec）。
        /// 向かい合って headOnStuckSec（10 秒）止まったら、引き直しを待たずによける場所へ動く。
        ///
        /// 優先が高い側はここで何もしない（相手を気にせず道を引く）。
        /// どちらも止まり続けないよう、前のセンサーの止まりからは 3 章の回り込みで抜ける。
        /// </summary>
        void UpdateRobotYieldMap(float dt, FrontSensor.Reading reading)
        {
            if (_mapYield)
            {
                _mapYieldS += dt;
                bool passed = _mapYieldTo == null || Gap(_mapYieldTo) >= spec.robotYieldClearM;
                bool tooLong = spec.robotYieldMaxSec > 0f && _mapYieldS >= spec.robotYieldMaxSec;
                if (!passed && !tooLong) return;

                if (ops != null)
                    ops.AddYield(_mapYieldStartS, _mapYieldAt,
                                 "ロボット" + robotId + "号機", RobotSpec.RankRobot,
                                 _mapYieldTo != null ? "ロボット" + _mapYieldTo.robotId + "号機" : "",
                                 _mapYieldS, _mapYieldStepped, passed);

                _mapYield = false;
                _mapYieldTo = null;
                _mapYieldS = 0f;
                _headOnS = 0f;
                ResumeLeg(GoalOfPhase());
                return;
            }

            ServingRobotController other = HigherPriorityNear();
            if (other == null)
            {
                _headOnS = 0f;
                _avoidTarget = null;       // この出会いは終わった
                return;
            }

            // 向かい合って止まっているか
            bool stopped = _speed <= spec.stoppedSpeedEpsilon;
            bool headOn = stopped && IsHeadOn(other);
            if (headOn) _headOnS += dt; else _headOnS = 0f;

            // ここから先は道を引く。引き直しは 1 台につき replanMinIntervalSec（0.5 秒）に 1 回まで。
            // 毎コマ引くと、1 回 10〜80 ms の道探しが回を押しつぶす
            if (_repathCooldown > 0f) return;

            // 2台が向かい合って headOnStuckSec（10 秒）止まったら、よける場所へ動く
            if (spec.headOnStuckSec > 0f && _headOnS >= spec.headOnStuckSec)
            {
                StartMapYield(other);
                return;
            }

            if (!PathsOverlap(other)) return;

            // 相手の道とかぶらない道があれば、そちらを選ぶ
            _mapYieldTo = other;
            if (TryAvoidPlan(other)) return;
            _mapYieldTo = null;

            // かぶらない道が引けない。止まっているならよける場所へ動く
            if (stopped) StartMapYield(other);
        }

        Vector3 GoalOfPhase()
        {
            return _phase == Phase.ToPass ? _passPoint : _tablePoint;
        }

        /// <summary>
        /// 近く（robotReplanNearM）にいて、自分より先に通る台。いなければ null。
        /// いちばん近い 1 台だけを見る
        /// </summary>
        ServingRobotController HigherPriorityNear()
        {
            ServingRobotController best = null;
            float bestD = spec.robotReplanNearM;
            Vector3 me = Flat(transform.position);
            for (int i = 0; i < _all.Count; i++)
            {
                ServingRobotController o = _all[i];
                if (o == null || o == this) continue;
                if (!YieldsTo(o)) continue;
                float d = Vector3.Distance(me, Flat(o.transform.position));
                if (d > bestD) continue;
                best = o; bestD = d;
            }
            return best;
        }

        /// <summary>2 台が向かい合っているか（向きの内積が headOnDotMax 以下）</summary>
        bool IsHeadOn(ServingRobotController other)
        {
            Vector3 a = transform.forward; a.y = 0f;
            Vector3 b = other.transform.forward; b.y = 0f;
            if (a.sqrMagnitude < 0.0001f || b.sqrMagnitude < 0.0001f) return false;
            return Vector3.Dot(a.normalized, b.normalized) <= spec.headOnDotMax;
        }

        /// <summary>自分の先の道と、相手の先の道がかぶっているか</summary>
        bool PathsOverlap(ServingRobotController other)
        {
            PathAhead(spec.robotPathAheadM, _aheadMine);
            other.PathAhead(spec.robotPathAheadM, _aheadOther);
            float limit = spec.Radius * 2f + spec.robotPathCostMarginM;
            return PolylineGap(_aheadMine, _aheadOther) < limit;
        }

        /// <summary>
        /// 相手の道を重くして引き直す。かぶらない道になったら使う。
        /// かぶったままなら、もとの道に戻して false
        /// </summary>
        bool TryAvoidPlan(ServingRobotController other)
        {
            Vector3[] prevCorners = _corners;
            int prevIndex = _cornerIndex;
            bool prevValid = _pathValid;
            Vector3 prevOnMesh = _destinationOnMesh;
            bool prevHasOnMesh = _hasDestinationOnMesh;

            if (PlanOnMap(_destination, MarkMode.Avoid, true) && !PathsOverlap(other))
            {
                // 同じ相手との 1 回の出会いは 1 回だけ数える
                // （近くにいるあいだ 0.5 秒ごとに引き直すので、毎回数えると膨らむ）
                if (_avoidTarget != other) { RobotYields++; _avoidTarget = other; }
                SetResolveOverride(StopResolve.YieldedToRobot);
                return true;
            }

            _corners = prevCorners;
            _cornerIndex = prevIndex;
            _pathValid = prevValid;
            _destinationOnMesh = prevOnMesh;
            _hasDestinationOnMesh = prevHasOnMesh;
            return false;
        }

        /// <summary>
        /// よける場所へ動いて、相手が通り過ぎるまで待つ。
        /// 場所が無い・そこへの道が引けないときは何もしない（待って、次のコマでまた見る）
        /// </summary>
        void StartMapYield(ServingRobotController other)
        {
            FindYieldSpots(other, _yieldSpots, 3);
            if (_yieldSpots.Count == 0) return;

            // 近い順に試す。道が引けた所へ動く
            Vector3[] prevCorners = _corners;
            int prevIndex = _cornerIndex;
            bool prevValid = _pathValid;
            Vector3 prevOnMesh = _destinationOnMesh;
            bool prevHasOnMesh = _hasDestinationOnMesh;
            Vector3 prevGoal = _destination;

            for (int i = 0; i < _yieldSpots.Count; i++)
            {
                if (!PlanOnMap(_yieldSpots[i], MarkMode.None, true)) continue;

                _mapYield = true;
                _mapYieldS = 0f;
                _mapYieldTo = other;
                _mapYieldStepped = true;
                _mapYieldStartS = ops != null ? ops.NowS : 0f;
                _mapYieldAt = transform.position;
                _headOnS = 0f;
                RobotYields++;
                SetResolveOverride(StopResolve.YieldedToRobot);
                return;
            }

            // どこへも引けない。もとの道に戻す
            _corners = prevCorners;
            _cornerIndex = prevIndex;
            _pathValid = prevValid;
            _destinationOnMesh = prevOnMesh;
            _hasDestinationOnMesh = prevHasOnMesh;
            _destination = prevGoal;
        }

        readonly List<Vector3> _yieldSpots = new List<Vector3>();

        /// <summary>
        /// よける場所を探す（4 章）。
        /// 近く（robotYieldSpotSearchM）で、相手の先の道から
        /// 2 台の半径の和＋robotYieldSeparationM 以上離れ、自分が入れるマス（free_class 1 以上）。
        /// 近い順に、互いに YieldSpotApartM 以上離れたものを max 個まで返す
        /// （となりのマスばかり並べても、どれも同じ所なので意味がない）。
        /// 同じ距離ならマスの番号の小さい方（乱数を使わない）。
        /// </summary>
        void FindYieldSpots(ServingRobotController other, List<Vector3> into, int max)
        {
            into.Clear();
            if (_map == null || other == null) return;

            other.PathAhead(spec.robotPathAheadM, _aheadOther);
            if (_aheadOther.Count < 2) return;

            float need = spec.Radius * 2f + spec.robotYieldSeparationM;
            float search = spec.robotYieldSpotSearchM;
            Vector3 me = Flat(transform.position);

            // 自分のまわりを輪の形に、内側から外側へ見ていく（並べ替えをしないで近い順にする）
            float res = _map.res;
            int cx = Mathf.FloorToInt(me.x / res);
            int cy = Mathf.FloorToInt(me.z / res);
            int maxR = Mathf.Max(1, Mathf.CeilToInt(search / res));
            float search2 = search * search;
            float apart2 = YieldSpotApartM * YieldSpotApartM;

            for (int r = 0; r <= maxR && into.Count < max; r++)
            {
                for (int iy = cy - r; iy <= cy + r && into.Count < max; iy++)
                {
                    if (iy < 0 || iy >= _map.ny) continue;
                    for (int ix = cx - r; ix <= cx + r; ix++)
                    {
                        if (ix < 0 || ix >= _map.nx) continue;
                        // 輪の上だけ見る（中は前の回で見た）
                        if (r > 0 && Mathf.Abs(ix - cx) != r && Mathf.Abs(iy - cy) != r) continue;

                        int i = _map.Index(ix, iy);
                        if (_map.ClassOf(i) < 1) continue;
                        Vector3 c = _map.Center(i);
                        float dx = c.x - me.x, dz = c.z - me.z;
                        if (dx * dx + dz * dz > search2) continue;
                        if (PointToPolyline(c, _aheadOther) < need) continue;

                        bool tooClose = false;
                        for (int k = 0; k < into.Count; k++)
                            if ((into[k] - c).sqrMagnitude < apart2) { tooClose = true; break; }
                        if (tooClose) continue;

                        into.Add(c);
                        if (into.Count >= max) break;
                    }
                }
            }
        }

        /// <summary>よける場所の候補どうしを、これだけ離してから並べる（m）</summary>
        const float YieldSpotApartM = 0.5f;

        // ---------------- 線と線の離れ ----------------

        /// <summary>2 本の折れ線のいちばん近いところの距離（m）</summary>
        static float PolylineGap(List<Vector3> a, List<Vector3> b)
        {
            if (a == null || b == null || a.Count < 2 || b.Count < 2) return float.MaxValue;
            float best = float.MaxValue;
            for (int i = 1; i < a.Count; i++)
            {
                for (int k = 1; k < b.Count; k++)
                {
                    float d = SegmentGap(a[i - 1], a[i], b[k - 1], b[k]);
                    if (d < best) best = d;
                }
            }
            return best;
        }

        /// <summary>点から折れ線までの距離（m）</summary>
        static float PointToPolyline(Vector3 p, List<Vector3> line)
        {
            float best = float.MaxValue;
            for (int i = 1; i < line.Count; i++)
            {
                float d = PointToSegment(p, line[i - 1], line[i]);
                if (d < best) best = d;
            }
            return best;
        }

        static float PointToSegment(Vector3 p, Vector3 a, Vector3 b)
        {
            Vector3 v = b - a; v.y = 0f;
            Vector3 w = p - a; w.y = 0f;
            float l2 = v.sqrMagnitude;
            float t = l2 > 1e-12f ? Mathf.Clamp01(Vector3.Dot(w, v) / l2) : 0f;
            Vector3 q = a + v * t;
            q.y = 0f;
            Vector3 pf = p; pf.y = 0f;
            return Vector3.Distance(pf, q);
        }

        /// <summary>
        /// 線分と線分のいちばん近いところの距離（m）。
        /// 交わっているときは 0（端点だけ見ると、交わっていても離れていることになってしまう）
        /// </summary>
        static float SegmentGap(Vector3 a1, Vector3 a2, Vector3 b1, Vector3 b2)
        {
            if (SegmentsCross(a1, a2, b1, b2)) return 0f;
            float d = PointToSegment(a1, b1, b2);
            d = Mathf.Min(d, PointToSegment(a2, b1, b2));
            d = Mathf.Min(d, PointToSegment(b1, a1, a2));
            d = Mathf.Min(d, PointToSegment(b2, a1, a2));
            return d;
        }

        static bool SegmentsCross(Vector3 a1, Vector3 a2, Vector3 b1, Vector3 b2)
        {
            float d1 = Cross(b1, b2, a1), d2 = Cross(b1, b2, a2);
            float d3 = Cross(a1, a2, b1), d4 = Cross(a1, a2, b2);
            return ((d1 > 0f && d2 < 0f) || (d1 < 0f && d2 > 0f)) &&
                   ((d3 > 0f && d4 < 0f) || (d3 < 0f && d4 > 0f));
        }

        static float Cross(Vector3 a, Vector3 b, Vector3 p)
        {
            return (b.x - a.x) * (p.z - a.z) - (b.z - a.z) * (p.x - a.x);
        }

        // ---------------- すれ違い（依頼 v13 の 依頼1）----------------

        ServingRobotController _passByOther;
        bool _passByNarrow;
        float _passByShiftM;

        /// <summary>寄る線の始まり。すれ違いに入った所を覚えて、そこから右へ寄った線を走る</summary>
        Vector3 _passByLaneFrom;
        bool _passByHasLane;

        /// <summary>すれ違い中に止まっていた秒数と、今の決まりに渡したか</summary>
        float _passByStallS;
        bool _passByGaveUp;

        /// <summary>
        /// いま「渡す前に離れている」最中か（2026-10-26。依頼 v13 の直し②-c）。
        /// 鼻先 0.06 m のまま今までの決まりに渡すと、譲る場所も回り込む道も
        /// 見つからず固まる（545 秒）。前のセンサーが届く passByBackOffM（0.6 m）まで
        /// 下がってから渡す
        /// </summary>
        bool _passByBackingOff;

        /// <summary>いま離れている最中か（FaceAndMove が読む）</summary>
        public bool PassByBackingOff { get { return _passByBackingOff; } }

        /// <summary>寄った先が通れる床かを見るときの、焼いた面を探す半径（m）</summary>
        const float PassBySampleM = 0.05f;

        /// <summary>寄った線の上で、どれだけ先を狙うか（m）</summary>
        const float PassByLookaheadM = 1.0f;

        /// <summary>
        /// 向かい合ってくる台とすれ違えるかを見る（依頼 v13 の 依頼1）。
        ///
        /// 入ったら、2台とも速さを passBySpeedMps まで落とし、止まらずに
        /// 進む向きの右へ PassByShiftM（体の幅＋間 の半分）寄って通る。
        /// 譲り合いの順位は付けない。
        ///
        /// 通れる幅が足りないときは _passByNarrow にして、
        /// 今までの決まり（譲る順番・回り込み）に任せる。
        ///
        /// 2026-10-26 の直し（依頼 v13 の確かめのあと）:
        ///   ① 要る幅に **人が横を通る分** を入れた（CanPassByHere）。
        ///      前は NavMesh だけで見ていたので 1.2 m の通路でも寄ってしまい、
        ///      壁際 0.05 m しか残らず人がまったく通れなかった
        ///   ② 間が passBySeparationM を割ったら、3 秒を待たずに離れに入る。
        ///      渡す前に passByBackOffM（0.6 m）まで下がる（鼻先 0.06 m で
        ///      渡すと立て直せず 545 秒固まった）
        /// どちらも passByEnabled が入のときだけ働く。切のときの動きは cs:9 と同じ
        /// </summary>
        void UpdatePassBy(float dt)
        {
            _passByNarrow = false;
            _passByShiftM = 0f;

            if (spec == null || !spec.passByEnabled ||
                _mapYield || _robotYield || _yieldHold ||
                _phase == Phase.Dwell || _phase == Phase.StuckWait)
            {
                _passByOther = null;
                _passByHasLane = false;
                _passByStallS = 0f;
                _passByGaveUp = false;
                _passByBackingOff = false;
                return;
            }

            // いまの相手とのすれ違いが続いているか。続いていなければ新しく探す
            if (_passByOther != null &&
                (_passByOther.YieldingToRobot ||
                 !RobotPassBy.ContinuesMoving(this, _passByOther, spec)))
            {
                _passByOther = null;
                _passByHasLane = false;
                _passByStallS = 0f;
                _passByGaveUp = false;
            }
            if (_passByOther == null)
            {
                _passByOther = NearestPassByStart();
                _passByHasLane = false;
                _passByStallS = 0f;
                _passByGaveUp = false;
            }
            if (_passByOther == null) return;

            // 寄って通ろうとしても止まってしまったら、detourAfterSec（3 秒）で
            // 今の決まりに渡す（依頼 v13 の「3秒空かなければ回り込む道を探す」）。
            // いちど渡したら、この出会いのあいだは戻さない（行ったり来たりしない）
            // 2026-10-26（直し②-b）：間が passBySeparationM を割ったら、
            // 3 秒を待たずに離れに入る（割ったままだと2台とも止まり続けるため）
            float gapNow = Gap(_passByOther);
            bool tooClose = gapNow < spec.passBySeparationM;

            if (!_passByGaveUp)
            {
                if (_speed <= spec.stoppedSpeedEpsilon) _passByStallS += dt;
                else _passByStallS = 0f;
                if (tooClose || _passByStallS >= spec.detourAfterSec) _passByGaveUp = true;
            }
            if (_passByGaveUp)
            {
                // 直し②-c：前のセンサーが届く所まで離れてから、今までの決まりに渡す。
                // 離れ終わるか、下がれなくなったら渡す
                _passByBackingOff = spec.passByBackOffM > 0f &&
                                    gapNow < spec.passByBackOffM &&
                                    CanBackOff();
                _passByNarrow = true;
                _passByHasLane = false;
                _passByShiftM = 0f;
                return;
            }
            _passByBackingOff = false;

            float want = spec.PassByShiftM;
            // 2026-10-26（直し①）：人が横を通れる幅があるかも見る
            if (!CanPassByHere(this, want) || !CanPassByHere(_passByOther, want))
            {
                _passByNarrow = true;
                _passByHasLane = false;
                return;
            }

            _passByShiftM = want;
            if (!_passByHasLane)
            {
                _passByLaneFrom = Flat(transform.position);
                _passByHasLane = true;
            }
        }

        /// <summary>
        /// これからすれ違う台のうち、いちばん近い1台。
        /// 同じ距離なら号機の小さい方（乱数を使わない）
        /// </summary>
        ServingRobotController NearestPassByStart()
        {
            ServingRobotController best = null;
            float bestD = float.MaxValue;
            for (int i = 0; i < _all.Count; i++)
            {
                ServingRobotController o = _all[i];
                if (o == null || o == this) continue;
                if (o.YieldingToRobot) continue;          // よけて待っている台とはすれ違わない
                if (!RobotPassBy.Starts(this, o, spec)) continue;
                float d = RobotPassBy.CenterDistance(this, o);
                if (d > bestD || (d == bestD && best != null && o.robotId > best.robotId)) continue;
                best = o; bestD = d;
            }
            return best;
        }

        /// <summary>
        /// そこで すれ違ってよいか（2026-10-26。依頼 v13 の直し①）。
        ///
        /// 前は「寄った先がロボット用の焼いた面に乗るか」だけを見ていた。
        /// ロボット用の面は体の半径 0.25 m で削ってあるので、1.2 m の通路でも
        /// 中心を ±0.35 m まで置けてしまい、0.3 m の寄せが通っていた。
        /// その結果、2台が寄ると壁際に 0.05 m しか残らず **人がまったく通れない**。
        ///
        /// いまは地図の clear_mm（そのマスから障害物までの実距離）で
        /// 通路の幅そのものを見る。
        ///   要る幅 = 体の幅×2 ＋ 2台の間 ＋ 人が横を通る分（RobotSpec.PassByNeedWidthM）
        ///   中心線から壁まで = clear_mm なので、その半分あるかで見る
        /// 足りなければ すれ違わず、今までの決まり（料理を載せた方が先・
        /// 3秒で回り込む）に渡す。
        ///
        /// 地図が無いとき（SiteRoutes で走るとき）は すれ違わない
        /// （幅を確かめられないまま人をふさぐ方が悪いため）
        /// </summary>
        /// <summary>
        /// 後ろへ1歩さがれるか（2026-10-26。依頼 v13 の直し②-c）。
        /// 下がった先がロボットの通れる床でなければ、下がらずに渡す
        /// </summary>
        bool CanBackOff()
        {
            if (spec == null) return false;
            Vector3 back = Flat(transform.position) - Flat(transform.forward).normalized * 0.2f;
            NavMeshHit hit;
            return NavMesh.SamplePosition(back, out hit, spec.Radius, RobotFilter);
        }

        bool CanPassByHere(ServingRobotController r, float want)
        {
            if (r == null || spec == null) return false;
            if (_map == null) return false;

            float needHalf = spec.PassByNeedWidthM * 0.5f;

            // 2台それぞれの足元と、寄った先・その少し先を見る
            Vector3 right = RobotPassBy.RightOf(r.transform.forward);
            Vector3 fwd = Flat(r.transform.forward);
            if (fwd.sqrMagnitude > 0.0001f) fwd = fwd.normalized;
            Vector3 here = Flat(r.transform.position);

            for (int i = 0; i <= 2; i++)
            {
                Vector3 p = here + fwd * (0.5f * i);
                int cell = _map.CellAt(p);
                if (cell < 0) return false;
                if (_map.ClearM(cell) < needHalf) return false;
            }

            // 寄った先がロボットの通れる床であることも、これまでどおり見る
            NavMeshHit hit;
            return NavMesh.SamplePosition(r.transform.position + right * want,
                                          out hit, PassBySampleM, RobotFilter);
        }

        /// <summary>
        /// すれ違い中に狙う点。すれ違いに入った所から今の角へ引いた線の、
        /// 右 _passByShiftM の線の上を走る（寄りすぎて壁へ向かわないよう、
        /// 乗らなければ寄せを減らす）
        /// </summary>
        Vector3 PassByTarget(Vector3 here, Vector3 corner)
        {
            Vector3 a = _passByHasLane ? _passByLaneFrom : Flat(here);
            Vector3 b = Flat(corner);
            Vector3 ab = b - a;
            float len = ab.magnitude;
            if (len < 0.05f) return corner;

            Vector3 dir = ab / len;
            Vector3 right = RobotPassBy.RightOf(dir);
            Vector3 foot = YieldSide.ProjectOnSegment(a, b, here);
            float ahead = Mathf.Min(PassByLookaheadM, Vector3.Distance(foot, b));

            for (int i = 0; i < 3; i++)
            {
                float f = 1f - 0.33f * i;
                Vector3 p = foot + dir * ahead + right * (_passByShiftM * f);
                NavMeshHit hit;
                if (NavMesh.SamplePosition(p, out hit, PassBySampleM, RobotFilter)) return p;
            }
            return corner;
        }

        void FaceAndMove(float dt)
        {
            if (_repathCooldown > 0f) _repathCooldown -= dt;

            // 2026-10-26（直し②-c）：今までの決まりに渡す前に、前のセンサーが届く所まで
            // 後ろへ下がる。下がっている間は道を追わない（向きも変えない）。
            // 記録の後始末（止まりの開き・閉じ）は Tick 側に残るので飛ばさない
            if (_passByBackingOff) { BackOffStep(dt); return; }

            if (_cornerIndex >= _corners.Length)
            {
                if (_repathCooldown <= 0f) SetDestination(_destination);
                return;
            }

            Vector3 here = transform.position;
            Vector3 corner = _corners[_cornerIndex];
            Vector3 to = corner - here; to.y = 0f;
            float d = to.magnitude;

            while (d <= spec.cornerReachDistance && _cornerIndex < _corners.Length - 1)
            {
                _cornerIndex++;
                corner = _corners[_cornerIndex];
                to = corner - here; to.y = 0f;
                d = to.magnitude;
            }

            if (d > 0.0001f)
            {
                Vector3 dir = to / d;

                // すれ違い中だけ、狙う点を進む向きの右へ寄せる（依頼 v13）。
                // 角の進み方・1コマで進む距離の決め方は変えない。
                // 切のときはこの if に入らないので、1行も計算が変わらない
                if (PassByMoving)
                {
                    Vector3 aim = PassByTarget(here, corner) - here;
                    aim.y = 0f;
                    if (aim.sqrMagnitude > 0.0001f) dir = aim.normalized;
                }

                // 向きを変える
                Quaternion want = Quaternion.LookRotation(dir, Vector3.up);
                transform.rotation = Quaternion.RotateTowards(transform.rotation, want, spec.turnSpeedDegPerSec * dt);

                float step = Mathf.Min(_speed * dt, d);
                if (step > 0f)
                {
                    transform.position = here + dir * step;
                    DistanceM += step;
                    LastStepM = step;
                }
            }

            if (d <= spec.cornerReachDistance && _cornerIndex >= _corners.Length - 1) _cornerIndex = _corners.Length;
        }

        /// <summary>
        /// 1コマぶん後ろへ下がる（2026-10-26。依頼 v13 の直し②-c）。
        /// 下がった先がロボットの通れる床でなければ、その場で離れをやめる
        /// （次のコマで今までの決まりに渡る）
        /// </summary>
        void BackOffStep(float dt)
        {
            if (spec == null) { _passByBackingOff = false; return; }

            Vector3 fwd = Flat(transform.forward);
            if (fwd.sqrMagnitude < 0.0001f) { _passByBackingOff = false; return; }
            fwd = fwd.normalized;

            float step = Mathf.Max(0f, spec.passByBackSpeedMps) * dt;
            if (step <= 0f) { _passByBackingOff = false; return; }

            Vector3 to = Flat(transform.position) - fwd * step;
            NavMeshHit hit;
            if (!NavMesh.SamplePosition(to, out hit, spec.Radius, RobotFilter))
            {
                _passByBackingOff = false;      // もう下がれない。このまま渡す
                return;
            }

            Vector3 next = hit.position;
            next.y = transform.position.y;
            float moved = Vector3.Distance(Flat(transform.position), Flat(next));
            transform.position = next;
            DistanceM += moved;
            LastStepM = moved;
            _speed = 0f;                        // 下がりは「走っている」に数えない
        }

        void SyncAgent()
        {
            if (_agent == null || !_agent.enabled) return;
            if (!_agent.isOnNavMesh)
            {
                _agent.Warp(transform.position);
                return;
            }
            _agent.nextPosition = transform.position;
        }

        void UpdateClearance()
        {
            float myR = spec.Radius;
            Vector3 me = transform.position; me.y = 0f;
            IReadOnlyList<PersonBody> all = PersonBody.All;
            for (int i = 0; i < all.Count; i++)
            {
                PersonBody p = all[i];
                if (p == null || !p.walksInAisle) continue;
                Vector3 q = p.transform.position; q.y = 0f;
                float gap = Vector3.Distance(me, q) - myR - p.radius;
                if (gap < MinClearanceM) MinClearanceM = gap;
            }
        }

        /// <summary>経路が最後までつながっているか（引き直しに失敗しているときは false）</summary>
        public bool PathComplete { get { return _pathValid; } }

        void OnDrawGizmosSelected()
        {
            if (!_hasAisleZone) return;
            Gizmos.color = _yieldHold ? new Color(1f, 0.8f, 0.1f, 0.6f) : new Color(0.2f, 0.9f, 0.5f, 0.35f);
            Gizmos.DrawWireCube(new Vector3(_aisleZone.center.x, 0.05f, _aisleZone.center.z),
                                new Vector3(_aisleZone.size.x, 0.1f, _aisleZone.size.z));
            Gizmos.DrawWireSphere(_aisleEntrance + Vector3.up * 0.05f, 0.25f);
        }
    }
}
```

### Assets/ServiceSim/Service/ServiceSpec.cs

```csharp
using UnityEngine;

namespace ServiceSim
{
    /// <summary>
    /// 店の回し方の数字。試算（haizen_sim.py の COMMON と STORES["family"]）と同じ値にそろえる。
    /// 店の運営に関する数字はすべてこの .asset に置く。
    ///
    /// 三角分布は（いちばん短い, いちばん多い, いちばん長い）の分（min）で持つ。
    /// </summary>
    [CreateAssetMenu(menuName = "ServiceSim/Service Spec", fileName = "ServiceSpec")]
    public class ServiceSpec : ScriptableObject
    {
        [Header("来客（STORES[\"family\"][\"arrivals\"]）")]
        [Tooltip("混み具合ごとの来客のペース（組/時）。[空いている, 普通, 混雑]")]
        public int[] arrivalsPerHourByCrowd = { 25, 35, 45 };

        [Tooltip("ピークの長さ（分）。1回の実行の長さ（STORES[\"family\"][\"peak_min\"]）")]
        public float peakMinutes = 120f;

        [Header("サイトの試算から読む値（v5 3章。SimParams が回すときだけ入れる）")]
        [Tooltip("その条件の来客のペース（組/時）。unity_conditions の arrivals_per_h。" +
                 "0 なら、これまでどおり arrivalsPerHourByCrowd（混み具合）を使う")]
        public int arrivalsPerHour;

        [Tooltip("動けなくなったロボットを、スタッフが動かす時間（秒）（common.rescue_s）")]
        public float rescueSec = 15f;

        [Tooltip("来客が終わったあと、全員が帰るのを待つ上限（分）（common.max_extra_min）")]
        public float maxExtraMin = 600f;

        [Header("組（Unity だけの値。試算に人数はない）")]
        [Tooltip("組の人数を選ぶ重み。[1人, 2人, 3人, 4人]")]
        public float[] groupSizeWeights = { 0.20f, 0.35f, 0.20f, 0.25f };

        [Header("案内・会計・呼び出し（COMMON）")]
        [Tooltip("案内にかかる時間（秒）。歩く時間は別（guide_s）")]
        public float guideSec = 45f;

        [Tooltip("会計の時間（秒）（pay_s）")]
        public float paySec = 90f;

        [Tooltip("呼び出し対応の時間（秒）。歩く時間は別（call_s）")]
        public float callSec = 30f;

        [Tooltip("レジの前の人数を数える半径（m）。記録だけに使う")]
        public float registerCrowdRadiusM = 2.0f;

        [Tooltip("会計はレジに立つ組の1人だけで済ませる（ほかの人は席で待つ）。" +
                 "外すと前のとおり、誰もレジに行かずスタッフだけで会計する（前後を比べるときに使う）")]
        public bool payAtRegisterByOne = true;

        [Tooltip("会計のとき、スタッフが立つ点からこれ以上離れていたら数える（m）。" +
                 "店員側に入れていないしるしなので、0 回になるはず")]
        public float registerPayOffSpotM = 0.1f;

        [Tooltip("1組あたりの呼び出しの起きやすさ（call_rate）。試算は2回ぶん 0.6/2 で判定")]
        public float callRate = 0.6f;

        [Header("料理（first_dishes・extra・fragile）")]
        [Tooltip("最初の注文：1皿めができるまで（分, 最短/最頻/最長）")]
        public Vector3 firstDish1CookMin = new Vector3(5f, 8f, 12f);

        [Tooltip("最初の注文：2皿めができるまで（分, 最短/最頻/最長）")]
        public Vector3 firstDish2CookMin = new Vector3(8f, 12f, 18f);

        [Tooltip("食事中のデザート・ドリンクの数の平均（fragile）")]
        public float fragilePerGroup = 0.5f;

        [Tooltip("デザート・ドリンクができるまで（分, 最短/最頻/最長）（fragile_cook）")]
        public Vector3 fragileCookMin = new Vector3(2f, 4f, 7f);

        [Tooltip("デザート・ドリンクをロボットも運ぶか。false なら人だけ（fragile_by）")]
        public bool fragileByRobot = true;

        [Tooltip("これを超えて届いた皿を「待たせた皿」とする（分）（cold_min）")]
        public float coldMin = 3f;

        [Header("運ぶ（COMMON）")]
        [Tooltip("ロボットが1走りで載せる料理の数（trays）。1 か 3")]
        public int trays = 3;

        [Tooltip("客がロボットから皿を取る時間（秒）（robot_handoff_s）")]
        public float robotHandoffSec = 25f;

        [Tooltip("スタッフが皿を置く時間（秒）（staff_place_s）")]
        public float staffPlaceSec = 10f;

        [Tooltip("料理ができてから、ロボットだけを待つ秒数（robot_first_wait_s）。" +
                 "このあいだスタッフは料理を持たず、案内・片付け・会計を続ける。" +
                 "過ぎたら、ロボットとスタッフのうち先に空いた方が運ぶ。" +
                 "0 にすると、はじめから先に空いた方が運ぶ（v2.7 より前と同じ）")]
        public float robotFirstWaitSec = 120f;

        [Tooltip("同じ皿が1往復の打ち切り（maxTripSec 超え）で戻された回数が " +
                 "これ以上になったら、その皿はロボットに渡さずスタッフの仕事にする。" +
                 "0 で無制限（前と同じ動き）")]
        public int maxRobotTripTimeoutsPerDish = 2;

        [Header("食事（eat_min）")]
        [Tooltip("食事の長さ（分, 最短/最頻/最長）")]
        public Vector3 eatMin = new Vector3(20f, 30f, 45f);

        [Tooltip("1人あたりドリンクバーへ行く平均の回数（Unity だけの値）")]
        public float drinkTripsPerPerson = 1.5f;

        [Tooltip("ドリンクバーで過ごす時間（秒）（Unity だけの値）")]
        public float drinkBarSec = 20f;

        [Tooltip("1組あたりトイレへ行く平均の回数（Unity だけの値）")]
        public float toiletTripsPerGroup = 0.3f;

        [Tooltip("トイレの中で過ごす時間（秒）（Unity だけの値）")]
        public float toiletSec = 180f;

        [Header("片付け（COMMON・bus_trips）")]
        [Tooltip("卓の皿をまとめる時間（秒）（clear_s）")]
        public float clearSec = 40f;

        [Tooltip("卓を拭いてセットする時間（秒）（wipe_s）")]
        public float wipeSec = 40f;

        [Tooltip("下げ膳の往復回数（皿の量）（bus_trips）")]
        public int busTrips = 2;

        [Header("待合")]
        [Tooltip("待合に立たせる組の上限。あふれた組は店の外で待つ扱い（人として置かない）")]
        public int waitingVisibleMax = 8;

        [Header("あきらめて、やり直す（B1）")]
        [Tooltip("会計・片付けで止まって卓が空かない状態がこの秒数続いたら、卓を空ける")]
        public float tableStuckFreeSec = 600f;

        // ---- 読み出し ----

        public int ArrivalsPerHour(CrowdLevel crowd)
        {
            // サイトの条件で回すときは、混み具合を使わずその値をそのまま使う（v5 3章）
            if (arrivalsPerHour > 0) return arrivalsPerHour;
            if (arrivalsPerHourByCrowd == null || arrivalsPerHourByCrowd.Length == 0) return 35;
            int i = Mathf.Clamp((int)crowd, 0, arrivalsPerHourByCrowd.Length - 1);
            return arrivalsPerHourByCrowd[i];
        }

        /// <summary>三角分布（分）から秒を引く。試算の tri() と同じ</summary>
        public static float TriangularSec(System.Random rng, Vector3 lowModeHigh)
        {
            float lo = lowModeHigh.x, mode = lowModeHigh.y, hi = lowModeHigh.z;
            if (hi <= lo) return lo * 60f;
            mode = Mathf.Clamp(mode, lo, hi);
            double u = rng.NextDouble();
            double c = (mode - lo) / (double)(hi - lo);
            double v = u < c
                ? lo + System.Math.Sqrt(u * (hi - lo) * (mode - lo))
                : hi - System.Math.Sqrt((1.0 - u) * (hi - lo) * (hi - mode));
            return (float)v * 60f;
        }

        /// <summary>指数分布（秒）。試算の expovariate と同じ</summary>
        public static float ExponentialSec(System.Random rng, float ratePerSec)
        {
            if (ratePerSec <= 0f) return float.MaxValue;
            double u = rng.NextDouble();
            if (u <= 1e-12) u = 1e-12;
            return (float)(-System.Math.Log(u) / ratePerSec);
        }

        /// <summary>組の人数を重みで選ぶ</summary>
        public int PickGroupSize(System.Random rng)
        {
            float total = 0f;
            int n = groupSizeWeights != null ? groupSizeWeights.Length : 0;
            for (int i = 0; i < n; i++) total += Mathf.Max(0f, groupSizeWeights[i]);
            if (total <= 0f) return 2;

            float r = (float)rng.NextDouble() * total;
            for (int i = 0; i < n; i++)
            {
                r -= Mathf.Max(0f, groupSizeWeights[i]);
                if (r <= 0f) return i + 1;
            }
            return n;
        }

        /// <summary>平均 mean 回を、整数回に落とす（試算の fragile と同じやり方）</summary>
        public static int PickCount(System.Random rng, float mean)
        {
            int n = (int)mean;
            if (rng.NextDouble() < mean - n) n++;
            return n;
        }
    }
}
```

### Assets/ServiceSim/Service/ServiceOps.cs

```csharp
using System.Collections.Generic;
using UnityEngine;

namespace ServiceSim
{
    /// <summary>料理1皿</summary>
    public class DishOrder
    {
        public int id;
        public int groupId;
        public int tableIndex;      // StoreBuilder.Tables の番号
        public int tableId;         // 卓の番号（報告用）
        public float readyS = -1f;  // 受け渡し口に出た時刻（店の中, 秒）
        public float arrivedS = -1f;// 卓に届いた時刻

        /// <summary>はじめて誰かが持ち上げた時刻（受け渡し口で待った時間を出すため）</summary>
        public float firstTakenS = -1f;

        /// <summary>最後に持ち上げられた時刻（運んでいた時間を出すため）</summary>
        public float lastTakenS = -1f;
        public bool fragile;        // デザート・ドリンク
        public bool taken;          // 誰かが持っている
        public bool byRobot;        // ロボットが運んだ
        public int requeued;        // 受け渡し口に戻された回数

        /// <summary>
        /// 同じ通路に2台を集めないために、後に回した皿か（v5・A）。
        /// 数を1皿につき1回だけ数えるための印（毎コマ数えないため）
        /// </summary>
        public bool deferredForAisle;

        /// <summary>
        /// スタッフも運べるようになった時刻（店の中, 秒）。まだなら -1（サイトの試算 v2.7）。
        ///
        /// 料理ができてから robot_first_wait_s のあいだは、ロボットだけが運ぶ。
        /// その時間を過ぎたら、ここに時刻が入り、スタッフの仕事板にも出る
        /// （そこからはロボットとスタッフの先に空いた方が運ぶ）。
        /// </summary>
        public float staffEligibleS = -1f;

        /// <summary>
        /// はじめからスタッフも運べる皿か（サイトの試算 v2.7）。
        /// 割れ物を人が運ぶ設定（fragile_by=staff）の倒れやすい料理と、ロボット0台のとき。
        /// この皿は「ロボットを待っていた皿」として数えない
        /// （待つ決まりがはじめから当てはまらないため）
        /// </summary>
        public bool staffFromStart;

        /// <summary>
        /// ロボットだけを待つ時間を過ぎたか（サイトの試算 v2.7）。
        /// staffFromStart の皿では立たない
        /// </summary>
        public bool robotWaitExpired;

        /// <summary>
        /// この皿が「1往復の打ち切り」で戻された回数（2026-10-27・依頼の直し）。
        /// ロボットが取る → 600 秒で打ち切り → 戻す → すぐ同じロボットが取る、を
        /// いつまでもくり返して客が帰れなくなった回があったので数える
        /// （10/27 の夜の s0_passon：同じ2皿を 57・58 回くり返した）
        /// </summary>
        public int robotTimeouts;

        /// <summary>
        /// ロボットには渡さない皿（スタッフだけが運ぶ）。
        /// 上の robotTimeouts が ServiceSpec.maxRobotTripTimeoutsPerDish に
        /// 届いたときに立てる。立つと PickBatch が拾わなくなる
        /// </summary>
        public bool staffOnly;
    }

    /// <summary>スタッフの仕事。優先は試算の staff_job と同じ（1=案内・配膳・呼び出し・会計、2=片付け）</summary>
    /// <summary>
    /// スタッフの仕事の種類。
    /// Rescue（v5 3章）は、動けなくなったロボットのところへ歩いて行って rescue_s 動かす仕事。
    /// サイトの決まりで「ほかの仕事より先」なので、優先度は 0（ほかは 1 と 2）。
    /// </summary>
    public enum StaffJobKind { Guide, Serve, Call, Pay, Clear, Rescue }

    public class StaffJob
    {
        public StaffJobKind kind;
        public int priority;
        public int groupId;
        public int tableIndex;
        public int tableId;
        public DishOrder dish;      // Serve のときだけ
        public ServingRobotController robot;  // Rescue のときだけ（動けなくなった台）
        public bool taken;
        public float postedS;

        /// <summary>スタッフが取った時刻（店の中, 秒）。まだなら -1。入店待ちの内訳に使う</summary>
        public float takenS = -1f;

        /// <summary>
        /// この仕事を何回あきらめたか（v5 3章）。**仕事ごと**に数える。
        /// 人ごとに数えていたときは、仕事が人から人へ回ると誰も上限（staffGiveUpMaxTries）に
        /// 届かず、「3回で移して済ませる」逃がし弁が働かないまま回り続けた。
        /// </summary>
        public int tries;
        /// <summary>仕事が終わったときに呼ばれる（出した側が入れる）</summary>
        public System.Action onDone;
    }

    /// <summary>
    /// あきらめた・移した・卓を空けた、の1回ぶん（give_ups.csv に1行）。
    /// 「移した」「卓を空けた」が多いときは、人が通れない店になっているしるし。
    /// </summary>
    public struct GiveUpRow
    {
        public float tS;         // 店の中の時刻（秒）
        public string who;       // 客 / スタッフ / 組
        public string job;       // 何をしていたか
        public int tableId;      // 関わる卓（無ければ 0）
        public float toX, toZ;   // 向かっていた先
        public float x, z;       // そのときの位置
        public string aisle;     // そのときいた通路
        public float waitedS;    // 着けないまま待った秒数
        public string action;    // あきらめた / 移した / 卓を空けた
    }

    /// <summary>
    /// 人がロボットに近づきすぎた1回ぶん（contacts.csv に1行）。
    /// ロボットの外周から PeopleSpec.robotContactGapM 以内に入ったとき。
    /// </summary>
    public struct ContactRow
    {
        public float tS;         // 店の中の時刻（秒）
        public float x, z;       // そのときの位置
        public string aisle;     // そのときいた通路
        public string place;     // そのときいた場所（レジ・待合・ドリンクバーなど。無ければ空）
        public string who;       // 誰が（種類:名前）
        public bool careless;    // よけ方を守らない人だったか
        public int robotId;      // 相手の台
        public float gapM;       // ロボットの外周からの空き（m。負なら重なった）
    }

    /// <summary>
    /// 届いた料理1皿ぶん（dishes.csv に1行）。サイトの試算 v2.7 の割り当ての決まりを確かめるため。
    /// 「誰が運んだか」「受け渡し口で待った秒数」「2分を過ぎて人が拾ったか」を残す。
    /// </summary>
    public struct DishRow
    {
        public int dishId;
        public int tableId;
        public int groupId;
        public bool fragile;

        /// <summary>料理ができた時刻（店の中, 秒）</summary>
        public float readyS;

        /// <summary>はじめて誰かが持ち上げた時刻</summary>
        public float firstTakenS;

        /// <summary>卓に届いた時刻</summary>
        public float arrivedS;

        /// <summary>誰が運んだか（ロボット / スタッフ）</summary>
        public bool byRobot;

        /// <summary>受け渡し口で待った秒数（できてから、はじめて持ち上げられるまで）</summary>
        public float passWaitS;

        /// <summary>運んでいた秒数（最後に持ち上げられてから、卓に届くまで）</summary>
        public float carryS;

        /// <summary>途中で戻されて失った秒数</summary>
        public float requeueS;

        /// <summary>受け渡し口に戻された回数</summary>
        public int requeued;

        /// <summary>はじめからスタッフも運べる皿だったか（fragile_by=staff・ロボット0台）</summary>
        public bool staffFromStart;

        /// <summary>ロボットだけを待つ時間（robot_first_wait_s）を過ぎたか</summary>
        public bool robotWaitExpired;

        /// <summary>
        /// 2分（robot_first_wait_s）を過ぎて人が拾った皿か。
        /// robotWaitExpired かつスタッフが運んだもの（staffFromStart の皿は数えない）
        /// </summary>
        public bool lateStaffPickup;

        /// <summary>同じ通路に2台を集めない決まりで後に回された皿か（v5・A）</summary>
        public bool deferredForAisle;
    }

    /// <summary>
    /// 誰が誰に譲った1回ぶん（yields.csv に1行。2026-10-23）。
    /// 譲る順番は RobotSpec（ロボット＞スタッフ＞客）。
    /// 譲り終わったとき（相手が通り過ぎた・待ちすぎてやめた）に1行足す。
    /// </summary>
    public struct YieldRow
    {
        /// <summary>店の中の時刻（秒）。譲り**始めた**時刻</summary>
        public float tS;

        /// <summary>譲った側（ロボット1号機 / スタッフ:名前 / 客:名前）</summary>
        public string who;

        /// <summary>譲った側の順番（0=ロボット・1=スタッフ・2=客）</summary>
        public int rank;

        /// <summary>相手（ロボット2号機 など）</summary>
        public string toWhom;

        /// <summary>譲り始めた位置</summary>
        public float x, z;

        /// <summary>そのときいた通路（無ければ空）</summary>
        public string aisle;

        /// <summary>そのときいた場所（レジ・待合など。無ければ空）</summary>
        public string place;

        /// <summary>受け渡し口と客席をつなぐ角の中だったか</summary>
        public bool inCorner;

        /// <summary>譲って待った秒数</summary>
        public float waitedS;

        /// <summary>相手の道から横へどけたか（false なら来た道へ戻って待った）</summary>
        public bool steppedAside;

        /// <summary>相手が通り過ぎて終わったか（false なら待ちすぎてやめた）</summary>
        public bool passed;
    }

    /// <summary>届けられずに受け渡し口へ戻した1回ぶん（requeues.csv に1行）</summary>
    public struct RequeueRow
    {
        public float tS;
        public int dishId;
        public int tableId;
        public int robotId;
        public bool fragile;
        public float readyS;      // 料理ができた時刻
        public int requeuedSoFar; // この皿が戻された回数（この回を含む）

        /// <summary>
        /// この積み直しで「もうロボットには渡さない（スタッフだけ）」に変えたか
        /// （2026-10-27）。requeues.csv の to_staff
        /// </summary>
        public bool toStaff;
        public string why;        // 動けなくなった / 打ち切り / 取りやめ / 着かなかった
    }

    /// <summary>卓1つの状態</summary>
    public class TableSlot
    {
        public int index;
        public int tableId;
        public Vector3 deliveryPoint;
        public Vector3 aisleEntrance;
        public Bounds aisleZone;

        /// <summary>この卓へ行くときに通る細い通路の番号（店の形の aisles）。-1 なら通らない</summary>
        public int aisleIndex = -1;
        public Vector3 seatPoint;

        /// <summary>何人かけの卓か（2人席なら2、4人席なら4）</summary>
        public int seatCount = 4;

        public int groupId = -1;    // -1 なら空き
        public bool clearing;       // 片付け中（まだ使えない）
        public bool Free { get { return groupId < 0 && !clearing; } }
    }

    /// <summary>
    /// 店の回し方の中心。卓の空き、受け渡し口の料理の待ち行列、スタッフの仕事板を持ち、
    /// 空いたロボットに料理をまとめて渡す（試算の deliver / _robot_run と同じ決まり）。
    ///
    /// この script は店の運営だけを扱う。人の歩き方はそれぞれの script、
    /// ロボットの止まる決まりは ServingRobotController に任せる。
    /// 数字は ServiceSpec の .asset に置く。
    /// </summary>
    public class ServiceOps : MonoBehaviour
    {
        [Header("設定")]
        public ServiceSpec spec;
        public RobotSpec robotSpec;

        [Tooltip("卓と通路を読む先")]
        public StoreBuilder store;

        [Header("いまの様子（読むだけ）")]
        [SerializeField] int _freeTables;
        [SerializeField] int _dishesWaiting;

        /// <summary>受け渡し口でロボットを待っている皿（サイトの試算 v2.7）</summary>
        [SerializeField] int _dishesWaitingForRobot;
        [SerializeField] int _jobsWaiting;

        readonly List<TableSlot> _tables = new List<TableSlot>();
        readonly List<DishOrder> _pass = new List<DishOrder>();
        readonly List<StaffJob> _jobs = new List<StaffJob>();
        readonly List<ServingRobotController> _robots = new List<ServingRobotController>();
        readonly Dictionary<ServingRobotController, List<DishOrder>> _carrying =
            new Dictionary<ServingRobotController, List<DishOrder>>();
        readonly Dictionary<ServingRobotController, List<DishOrder>> _lastBatch =
            new Dictionary<ServingRobotController, List<DishOrder>>();

        /// <summary>
        /// 止まる所の番号 → その所で降ろす皿（依頼 v8 の 1）。
        ///
        /// StartRun に渡す stops を作るループは SlotOf が null の皿を飛ばすので、
        /// 「止まる所の番号＝batch の番号」とは限らない。だからこの対応表で引く。
        /// </summary>
        readonly Dictionary<ServingRobotController, List<DishOrder>> _stopDishes =
            new Dictionary<ServingRobotController, List<DishOrder>>();

        int _nextDishId = 1;

        // ---- 記録（TripLogger と ExperimentRunner が読む）----
        public readonly List<float> dishWaitMin = new List<float>();   // 料理ができてから届くまで（分）
        public readonly List<float> seatWaitMin = new List<float>();   // 入店から案内が始まるまで（分）
        public readonly List<float> turnMin = new List<float>();       // 退店から卓が空くまで（分）

        // ---- 入店待ちの内訳（v5 3章・サイトの試算（haizen_sim.py）と同じ分け方）----
        //   seat_wait   … 店に来てから案内が始まるまで（seatWaitMin）
        //   blocked_wait… そのうち、卓は空いたのに片付けが終わらず待った分
        //   full_wait   … seat_wait − blocked_wait（ほんとうに満席で待った分）
        //   guide_wait  … 案内の仕事が板に出てから、スタッフが取るまで
        //   staff_wait  … blocked_wait ＋ guide_wait（店側が追いつかなかった分）
        //   entry_wait  … 店に来てから卓に着くまで（案内の歩きと guide_s を含む）
        /// <summary>entry_wait_mean：店に来てから案内されて卓に着くまで（分）</summary>
        public readonly List<float> entryWaitMin = new List<float>();

        /// <summary>full_wait_mean：席待ち − blocked_wait（分）</summary>
        public readonly List<float> fullWaitMin = new List<float>();

        /// <summary>blocked_wait_mean：卓は空いたが片付けが終わらず待った分（分）</summary>
        public readonly List<float> blockedWaitMin = new List<float>();

        /// <summary>guide_wait_mean：案内の仕事が板に出てから取られるまで（分）</summary>
        public readonly List<float> guideWaitMin = new List<float>();

        /// <summary>staff_wait_mean：blocked_wait ＋ guide_wait（分）</summary>
        public readonly List<float> staffWaitMin = new List<float>();
        public int robotDishes { get; private set; }
        public int staffDishes { get; private set; }
        public int requeuedDishes { get; private set; }

        /// <summary>
        /// 往復の打ち切りが続いて「スタッフだけが運ぶ」に回した皿の数（2026-10-27）。
        /// 0 なら、その回では上限に届いた皿が1枚も無かったということ
        /// </summary>
        public int dishesToStaffAfterTimeout { get; private set; }

        // ---- あきらめた記録（B1）----
        readonly List<GiveUpRow> _giveUps = new List<GiveUpRow>();
        public IReadOnlyList<GiveUpRow> GiveUps { get { return _giveUps; } }

        // ---- 人がロボットをよけた記録 ----
        readonly List<ContactRow> _contacts = new List<ContactRow>();

        /// <summary>人がロボットに近づきすぎた1回ずつ（contacts.csv）</summary>
        public IReadOnlyList<ContactRow> Contacts { get { return _contacts; } }

        // ---- 譲り合いの記録（yields.csv・2026-10-23）----
        readonly List<YieldRow> _yields = new List<YieldRow>();

        /// <summary>誰が誰に譲った1回ずつ（yields.csv）</summary>
        public IReadOnlyList<YieldRow> Yields { get { return _yields; } }

        /// <summary>
        /// 譲り1回ぶんを残す。譲り**終わった**ときに呼ぶ（待った秒数が決まってから）。
        /// startS は譲り始めた時刻、at は譲り始めた位置。
        /// </summary>
        public void AddYield(float startS, Vector3 at, string who, int rank, string toWhom,
                             float waitedS, bool steppedAside, bool passed)
        {
            YieldRow r = new YieldRow();
            r.tS = startS;
            r.who = who;
            r.rank = rank;
            r.toWhom = toWhom;
            r.x = at.x;
            r.z = at.z;
            r.aisle = AisleNameAt(at);
            r.place = store != null ? store.PlaceNameAt(at) : "";
            // 依頼 v9：角の箱は SiteRoutes のときだけ働く
            r.inCorner = robotSpec != null && !robotSpec.UseMapPlanner && robotSpec.InCorner(at);
            r.waitedS = Mathf.Max(0f, waitedS);
            r.steppedAside = steppedAside;
            r.passed = passed;
            _yields.Add(r);
        }

        /// <summary>人がロボットを待った回数（入口で待つ＋通路の中で下がる）</summary>
        public int robotYieldWaits { get; private set; }

        /// <summary>人がロボットを待っていた秒数の合計</summary>
        public float robotYieldWaitSeconds { get; private set; }

        /// <summary>人が回り道した回数</summary>
        public int robotYieldDetours { get; private set; }

        // ---- レジの前の混み具合（調べるための記録）----
        /// <summary>レジの前にいた人数のいちばん多いとき</summary>
        public int registerCrowdMax { get; private set; }

        float _registerCrowdSum;
        int _registerCrowdSamples;

        // ---- 会計のときスタッフが店員側（レジの内側）にいたか ----
        /// <summary>会計をした回数</summary>
        public int registerPayCount { get; private set; }

        /// <summary>そのうち、店員側にいなかった回数（0 になるはず）</summary>
        public int registerPayNotAtBackCount { get; private set; }

        /// <summary>立つ点から 0.1 m より離れて会計した回数（0 になるはず）</summary>
        public int registerPayOffSpotCount { get; private set; }

        /// <summary>立つ点と実際に立った位置の距離のいちばん大きいもの（m）</summary>
        public float registerPayMaxOffsetM { get; private set; }

        /// <summary>立つ点からの距離の合計（平均を出すため, m）</summary>
        float _registerPayOffsetSum;

        /// <summary>
        /// いま店員側（レジの内側）で会計しているスタッフがいるか。
        /// 店員側は 0.8 × 1.2 m で1人ぶんなので、会計の仕事は同時に1つだけ取れる。
        /// </summary>
        public bool RegisterBackBusy { get; private set; }

        /// <summary>店員側に、会計でないスタッフがいた秒数（0 になるはず）</summary>
        public float registerBackIntruderSeconds { get; private set; }

        /// <summary>店員側に、会計でないスタッフがいた回数（0 になるはず）</summary>
        public int registerBackIntruderCount { get; private set; }

        /// <summary>会計でないスタッフが店員側にいたことを残す（StaffAgent が毎コマ呼ぶ）</summary>
        public void AddRegisterBackIntruder(float dt, bool firstFrame)
        {
            registerBackIntruderSeconds += dt;
            if (firstFrame) registerBackIntruderCount++;
        }

        /// <summary>立つ点と実際に立った位置の距離の平均（m）</summary>
        public float RegisterPayMeanOffsetM
        {
            get { return registerPayCount > 0 ? _registerPayOffsetSum / registerPayCount : 0f; }
        }

        /// <summary>
        /// 会計1回ぶんを数える。
        /// atBack が false なら店員側にいなかった。offsetM は立つ点からの距離。
        /// どちらも 0 になるはず（ならなければ、スタッフが店員側に入れていない）。
        /// </summary>
        public void AddRegisterPay(bool atBack, float offsetM)
        {
            registerPayCount++;
            if (!atBack) registerPayNotAtBackCount++;

            _registerPayOffsetSum += offsetM;
            if (offsetM > registerPayMaxOffsetM) registerPayMaxOffsetM = offsetM;
            float limit = spec != null ? spec.registerPayOffSpotM : 0.1f;
            if (offsetM > limit) registerPayOffSpotCount++;
        }

        /// <summary>レジの前にいた人数の平均</summary>
        public float RegisterCrowdMean
        {
            get { return _registerCrowdSamples > 0 ? _registerCrowdSum / _registerCrowdSamples : 0f; }
        }

        // ---- スタッフの時間の使い方（仕事ごとの秒数）----
        readonly Dictionary<string, float> _staffTime = new Dictionary<string, float>();

        /// <summary>スタッフが何にどれだけ時間を使ったか（仕事の名前 → 秒）</summary>
        public IDictionary<string, float> StaffTime { get { return _staffTime; } }

        // ---- 料理が届くまでの内訳（運んだのがロボットか人かで分ける）----
        /// <summary>届いた料理1皿ぶんの内訳（秒）</summary>
        public struct DishParts
        {
            public bool byRobot;
            public float passWaitS;   // 受け渡し口で待った（できてから、はじめて持ち上げられるまで）
            public float carryS;      // 運んでいた（最後に持ち上げられてから、卓に届くまで）
            public float requeueS;    // 途中で戻されて失った（残り）
            public int requeued;      // 戻された回数
        }

        readonly List<DishParts> _dishParts = new List<DishParts>();

        /// <summary>届いた料理の、届くまでの内訳（summary.json に出す）</summary>
        public IReadOnlyList<DishParts> DishPartsList { get { return _dishParts; } }

        // ---- 皿ごとの記録（dishes.csv・サイトの試算 v2.7）----
        readonly List<DishRow> _dishRows = new List<DishRow>();

        /// <summary>届いた料理1皿ずつの記録（dishes.csv）</summary>
        public IReadOnlyList<DishRow> DishRows { get { return _dishRows; } }

        /// <summary>
        /// 2分（robot_first_wait_s）を過ぎて人が拾った皿の数（summary.json に出す）。
        /// ロボットだけを待つ時間を過ぎてから、スタッフが運んだ皿
        /// </summary>
        public int lateStaffDishes { get; private set; }

        /// <summary>
        /// いま受け渡し口でロボットを待っている皿の数（キッチンのモニターに出す）。
        /// ロボットだけを待つ時間（robot_first_wait_s）の中にいて、まだ誰も持っていない皿。
        /// はじめからスタッフも運べる皿（fragile_by=staff・ロボット0台）は数えない
        /// </summary>
        public int DishesWaitingForRobot { get { return _dishesWaitingForRobot; } }

        // ---- 仕事が板で待った秒数（仕事ごと）----
        readonly Dictionary<string, float> _jobWaitSum = new Dictionary<string, float>();
        readonly Dictionary<string, int> _jobWaitCount = new Dictionary<string, int>();
        readonly Dictionary<string, float> _jobWaitMax = new Dictionary<string, float>();
        readonly Dictionary<string, int> _jobWaitOverCount = new Dictionary<string, int>();

        /// <summary>仕事が板に出てから取られるまでの秒数の合計（仕事の名前 → 秒）</summary>
        public IDictionary<string, float> JobWaitSum { get { return _jobWaitSum; } }

        /// <summary>仕事が取られた回数（仕事の名前 → 回）</summary>
        public IDictionary<string, int> JobWaitCount { get { return _jobWaitCount; } }

        /// <summary>
        /// 仕事が板に出てから取られるまでの、いちばん長かった秒数（仕事の名前 → 秒。依頼 v10 の 2）。
        /// 逃がし弁の数え方を直す前は、片付け・会計の長い待ちが「卓を空けた」で消えていた
        /// </summary>
        public IDictionary<string, float> JobWaitMax { get { return _jobWaitMax; } }

        /// <summary>
        /// そのうち JobWaitOverSec（600 秒）を超えた数（仕事の名前 → 回。依頼 v10 の 2）
        /// </summary>
        public IDictionary<string, int> JobWaitOverCount { get { return _jobWaitOverCount; } }

        /// <summary>長びいたと数える境目（秒）。ServiceSpec.tableStuckFreeSec と同じ</summary>
        public float JobWaitOverSec
        {
            get { return spec != null && spec.tableStuckFreeSec > 0f ? spec.tableStuckFreeSec : 600f; }
        }

        void RecordJobWait(StaffJob j)
        {
            if (j == null) return;
            string k = StaffAgent.JobKindName(j.kind);
            float waited = Mathf.Max(0f, NowS - j.postedS);
            float v; int n;
            _jobWaitSum.TryGetValue(k, out v);
            _jobWaitCount.TryGetValue(k, out n);
            _jobWaitSum[k] = v + waited;
            _jobWaitCount[k] = n + 1;

            // いちばん長かった待ちと、長びいた数（依頼 v10 の 2）
            float max;
            _jobWaitMax.TryGetValue(k, out max);
            if (waited > max) _jobWaitMax[k] = waited;
            if (waited >= JobWaitOverSec)
            {
                int over;
                _jobWaitOverCount.TryGetValue(k, out over);
                _jobWaitOverCount[k] = over + 1;
            }
        }

        /// <summary>スタッフの時間を足す（StaffAgent が毎コマ呼ぶ）</summary>
        public void AddStaffTime(string what, float dt)
        {
            if (string.IsNullOrEmpty(what)) return;
            float v;
            _staffTime.TryGetValue(what, out v);
            _staffTime[what] = v + dt;
        }

        // ---- レジの前に並ぶ点（会計に立つ人が1人ずつ）----
        readonly List<Vector3> _regQueues = new List<Vector3>();
        readonly List<bool> _regQueueUsed = new List<bool>();

        /// <summary>レジの並ぶ点を1つ取る。無ければ -1（会計に立つ人も席で待つ）</summary>
        public int TakeRegisterQueueSpot(out Vector3 spot)
        {
            for (int i = 0; i < _regQueueUsed.Count; i++)
            {
                if (_regQueueUsed[i]) continue;
                _regQueueUsed[i] = true;
                spot = _regQueues[i];
                return i;
            }
            spot = RegisterPointOrPass();
            return -1;
        }

        /// <summary>取ったレジの並ぶ点を空ける</summary>
        public void ReleaseRegisterQueueSpot(int index)
        {
            if (index < 0 || index >= _regQueueUsed.Count) return;
            _regQueueUsed[index] = false;
        }

        Vector3 RegisterPointOrPass()
        {
            return store != null ? store.RegisterPoint : Vector3.zero;
        }
        public int gaveUpCount { get; private set; }      // あきらめた
        public int warpedCount { get; private set; }      // 移した
        public int tableFreedCount { get; private set; }  // 卓を空けた

        // ---- 立ち往生でスタッフを呼んだ回数（v5 3章・meta.metrics の rescues）----
        /// <summary>
        /// 同じ通路に2台を集めないために、料理を後に回した回数（v5・A）。
        /// 多いときは、2台が同じ通路の卓ばかりを受け持とうとしているしるし
        /// </summary>
        public int aisleDeferrals { get; private set; }

        /// <summary>動けなくなって、スタッフを呼んだ回数</summary>
        public int rescueCalls { get; private set; }

        /// <summary>そのうち、スタッフが着いて動かした回数</summary>
        public int rescueDone { get; private set; }

        /// <summary>スタッフが着くまでロボットが待った秒数の合計</summary>
        public float rescueWaitSeconds { get; private set; }

        // ---- ロボットのふさがっている割合（robot_util）----
        float _robotBusyS, _robotAliveS;

        /// <summary>robot_util：ロボットが空いていなかった時間の割合</summary>
        public float RobotUtil { get { return _robotAliveS > 0.01f ? _robotBusyS / _robotAliveS : 0f; } }

        // ---- スタッフが歩いた距離（walk_km_per_staff）----
        float _staffWalkM;
        int _staffCount;

        /// <summary>スタッフ1人が歩いた距離（km）</summary>
        public float WalkKmPerStaff
        {
            get { return _staffCount > 0 ? _staffWalkM / 1000f / _staffCount : 0f; }
        }

        /// <summary>スタッフの人数を伝える（walk_km_per_staff を出すため）</summary>
        public void SetStaffCount(int n) { _staffCount = n; }

        /// <summary>スタッフが1コマで歩いた距離を足す（StaffAgent が呼ぶ）</summary>
        public void AddStaffWalk(float m)
        {
            if (m > 0f && m < 1f) _staffWalkM += m;   // 1 m を超える跳びは「移した」ぶんなので数えない
        }

        // ---- 戻した皿の記録（B8）----
        readonly List<RequeueRow> _requeues = new List<RequeueRow>();
        public IReadOnlyList<RequeueRow> Requeues { get { return _requeues; } }

        // ---- 椅子を引いた回数（B6）----
        readonly Dictionary<string, int> _chairPullsByReason = new Dictionary<string, int>();
        readonly Dictionary<int, int> _chairPullsByTable = new Dictionary<int, int>();
        public int chairPulls { get; private set; }
        public IDictionary<string, int> ChairPullsByReason { get { return _chairPullsByReason; } }
        public IDictionary<int, int> ChairPullsByTable { get { return _chairPullsByTable; } }

        // ---- ドリンクバーの場所取り（B4）----
        readonly List<Vector3> _dbStands = new List<Vector3>();
        readonly List<Vector3> _dbQueues = new List<Vector3>();
        readonly List<bool> _dbStandUsed = new List<bool>();
        readonly List<bool> _dbQueueUsed = new List<bool>();

        /// <summary>並ぶ点に回された回数</summary>
        public int drinkBarQueued { get; private set; }

        /// <summary>立つ点も並ぶ点も空いていなくて、あきらめた回数</summary>
        public int drinkBarGaveUp { get; private set; }

        /// <summary>並んで待った秒数（分で記録）</summary>
        public readonly List<float> drinkBarWaitMin = new List<float>();

        public IList<ServingRobotController> Robots { get { return _robots; } }
        public IList<TableSlot> Tables { get { return _tables; } }
        public int FreeTableCount { get { return _freeTables; } }
        public int DishesWaiting { get { return _dishesWaiting; } }

        /// <summary>
        /// 仕事板に載っている仕事（読むだけ。依頼 v10b の 2）。
        /// 時間帯ごとの記録（TimelineLogger）が、取られていない仕事の数と
        /// 片付けの仕事が取られているかを数えるのに使う。書き換えないこと
        /// </summary>
        public IList<StaffJob> Jobs { get { return _jobs; } }

        /// <summary>店の中の時刻（秒）。ExperimentRunner が進める</summary>
        public float NowS { get; private set; }

        // ---------------- 用意 ----------------

        public void Setup()
        {
            _tables.Clear();
            _pass.Clear();
            _jobs.Clear();
            _carrying.Clear();
            _stopDishes.Clear();        // 止まる所と皿の対応表（依頼 v8 の 1）
            _lastBatch.Clear();
            dishWaitMin.Clear();
            seatWaitMin.Clear();
            turnMin.Clear();
            entryWaitMin.Clear();
            fullWaitMin.Clear();
            blockedWaitMin.Clear();
            guideWaitMin.Clear();
            staffWaitMin.Clear();
            aisleDeferrals = 0;
            rescueCalls = 0;
            rescueDone = 0;
            rescueWaitSeconds = 0f;
            _robotBusyS = 0f;
            _robotAliveS = 0f;
            _staffWalkM = 0f;
            robotDishes = 0;
            staffDishes = 0;
            requeuedDishes = 0;
            dishesToStaffAfterTimeout = 0;
            _giveUps.Clear();
            _requeues.Clear();
            _contacts.Clear();
            _yields.Clear();
            _staffTime.Clear();
            _dishParts.Clear();
            _dishRows.Clear();
            lateStaffDishes = 0;
            _dishesWaitingForRobot = 0;
            _jobWaitSum.Clear();
            _jobWaitCount.Clear();
            _jobWaitMax.Clear();
            _jobWaitOverCount.Clear();
            registerCrowdMax = 0;
            registerPayCount = 0;
            registerPayNotAtBackCount = 0;
            registerPayOffSpotCount = 0;
            registerPayMaxOffsetM = 0f;
            _registerPayOffsetSum = 0f;
            RegisterBackBusy = false;
            registerBackIntruderSeconds = 0f;
            registerBackIntruderCount = 0;
            _registerCrowdSum = 0f;
            _registerCrowdSamples = 0;
            _regQueues.Clear();
            _regQueueUsed.Clear();
            robotYieldWaits = 0;
            robotYieldWaitSeconds = 0f;
            robotYieldDetours = 0;
            robotStuckDetours = 0;
            _chairPullsByReason.Clear();
            _chairPullsByTable.Clear();
            chairPulls = 0;
            gaveUpCount = 0;
            warpedCount = 0;
            tableFreedCount = 0;
            _nextDishId = 1;
            NowS = 0f;

            drinkBarQueued = 0;
            drinkBarGaveUp = 0;
            drinkBarWaitMin.Clear();
            _dbStands.Clear();
            _dbQueues.Clear();
            _dbStandUsed.Clear();
            _dbQueueUsed.Clear();

            if (store == null) return;
            IList<StoreBuilder.TableInfo> t = store.Tables;
            for (int i = 0; i < t.Count; i++)
            {
                TableSlot s = new TableSlot();
                s.index = i;
                s.tableId = t[i].tableId;
                s.deliveryPoint = t[i].deliveryPoint;
                s.aisleEntrance = t[i].aisleEntrance;
                s.aisleZone = t[i].aisleZone;
                s.aisleIndex = t[i].aisleIndex;
                s.seatPoint = t[i].center;
                s.seatCount = t[i].seatCount;
                _tables.Add(s);
            }
            // ドリンクバーの立つ点・並ぶ点を読む（B4）
            SetupDrinkBar(store.PointsOf(PlaceKind.DrinkBarStand),
                          store.PointsOf(PlaceKind.DrinkBarQueue));

            // レジの前に並ぶ点を読む
            IList<Vector3> rq = store.RegisterQueuePoints;
            for (int i = 0; i < rq.Count; i++) { _regQueues.Add(rq[i]); _regQueueUsed.Add(false); }

            Recount();
        }

        // ---------------- ドリンクバーの場所取り（B4）----------------

        void SetupDrinkBar(IList<Vector3> stands, IList<Vector3> queues)
        {
            for (int i = 0; i < stands.Count; i++) { _dbStands.Add(stands[i]); _dbStandUsed.Add(false); }
            for (int i = 0; i < queues.Count; i++) { _dbQueues.Add(queues[i]); _dbQueueUsed.Add(false); }
        }

        /// <summary>
        /// ドリンクバーの場所を1つ取る（B4）。
        ///   ・空いている立つ点があれば、そこへ
        ///   ・埋まっていれば、端の並ぶ点へ
        ///   ・それも埋まっていれば取れない（客はあきらめて席に戻る）
        /// spot は向かう点、queued は並ぶ点だったか。
        /// 返した番号を ReleaseDrinkBarSpot に渡して空ける。
        /// </summary>
        public int TakeDrinkBarSpot(out Vector3 spot, out bool queued)
        {
            for (int i = 0; i < _dbStandUsed.Count; i++)
            {
                if (_dbStandUsed[i]) continue;
                _dbStandUsed[i] = true;
                spot = _dbStands[i];
                queued = false;
                return i;
            }

            for (int i = 0; i < _dbQueueUsed.Count; i++)
            {
                if (_dbQueueUsed[i]) continue;
                _dbQueueUsed[i] = true;
                spot = _dbQueues[i];
                queued = true;
                drinkBarQueued++;
                return 1000 + i;      // 1000 以上は並ぶ点
            }

            spot = _dbStands.Count > 0 ? _dbStands[0] : Vector3.zero;
            queued = false;
            drinkBarGaveUp++;
            return -1;
        }

        /// <summary>取った場所を空ける。waitedS は並んで待った秒数</summary>
        public void ReleaseDrinkBarSpot(int token, float waitedS)
        {
            if (token < 0) return;
            if (token >= 1000)
            {
                int i = token - 1000;
                if (i < _dbQueueUsed.Count) _dbQueueUsed[i] = false;
                drinkBarWaitMin.Add(Mathf.Max(0f, waitedS / 60f));
                return;
            }
            if (token < _dbStandUsed.Count) _dbStandUsed[token] = false;
        }

        /// <summary>ドリンクバーに立つ点があるか（無ければ今までどおり1点へ行く）</summary>
        public bool HasDrinkBarSpots { get { return _dbStands.Count > 0; } }

        public void RegisterRobot(ServingRobotController r)
        {
            if (r == null || _robots.Contains(r)) return;
            _robots.Add(r);
            _carrying[r] = new List<DishOrder>();
            r.ops = this;               // 立ち往生したときスタッフを呼ぶ先（v5 3章）
            // 待機場所を先に教える（1往復めの戻りから、戻りの線を使えるように。v5）
            if (store != null)
            {
                IList<Vector3> spots = store.RobotStandbyPoints;
                if (spots.Count > 0) r.SetStandbyPoint(spots[(_robots.Count - 1) % spots.Count]);
            }
            r.TripCompleted += OnRobotRunDone;
            r.StopArrived += OnRobotStopArrived;     // 卓に着いた時点で届いたにする（依頼 v8 の 1）
        }

        public void ClearRobots()
        {
            for (int i = 0; i < _robots.Count; i++)
            {
                if (_robots[i] == null) continue;
                _robots[i].TripCompleted -= OnRobotRunDone;
                _robots[i].StopArrived -= OnRobotStopArrived;
            }
            _robots.Clear();
            _carrying.Clear();
            _stopDishes.Clear();
            _lastBatch.Clear();
        }

        /// <summary>店の中の時刻を進める（ExperimentRunner が毎コマ呼ぶ）</summary>
        public void Advance(float dt)
        {
            NowS += dt;
            AssignRobots();
            ReleaseDishesToStaff();
            ParkIdleRobots();
            SampleRegisterCrowd();
            SampleRobotUtil(dt);
            Recount();
        }

        /// <summary>
        /// ロボットがふさがっていた時間を数える（robot_util）。
        /// 待機場所へ下がっているあいだ・受け渡し口で空のまま待っているあいだは Idle / Done なので
        /// 「空いている」。動けなくなって人を待っているあいだ（StuckWait）は「ふさがっている」。
        /// </summary>
        void SampleRobotUtil(float dt)
        {
            for (int i = 0; i < _robots.Count; i++)
            {
                ServingRobotController r = _robots[i];
                if (r == null) continue;
                _robotAliveS += dt;
                ServingRobotController.Phase p = r.CurrentPhase;
                if (p != ServingRobotController.Phase.Idle &&
                    p != ServingRobotController.Phase.Done) _robotBusyS += dt;
            }
        }

        /// <summary>
        /// レジの前にいた人数を数える（調べるための記録）。
        /// 待合に立つ客も、通路を歩く人も、スタッフも数える。
        /// </summary>
        void SampleRegisterCrowd()
        {
            if (store == null || spec == null) return;
            if (!store.HasPoint(PlaceKind.Register)) return;

            Vector3 reg = store.RegisterPoint;
            float r2 = spec.registerCrowdRadiusM * spec.registerCrowdRadiusM;

            int n = 0;
            IReadOnlyList<PersonBody> all = PersonBody.All;
            for (int i = 0; i < all.Count; i++)
            {
                PersonBody b = all[i];
                if (b == null || b.kind == PersonKind.Seated) continue;
                Vector3 d = b.transform.position - reg;
                d.y = 0f;
                if (d.sqrMagnitude <= r2) n++;
            }

            if (n > registerCrowdMax) registerCrowdMax = n;
            _registerCrowdSum += n;
            _registerCrowdSamples++;
        }

        /// <summary>
        /// 仕事のないロボットを待機場所へ下げる（B2）。
        /// 受け渡し口の前の点にはスタッフも料理を取りに来るので、空の台を置かない。
        /// 料理を載せるときは AssignRobots が受け渡し口から走り出させる。
        ///
        /// v5：料理が待っていても下げる。
        /// 前は「料理が待っているなら受け渡し口で待つ」にしていたが、そのせいで
        /// 出発する台と戻ってきた台が受け渡し口の前の1点に重なり、ふさぎ合っていた
        /// （20261023 0913 の回で、受け渡し口の前の止まりが「もう1台のロボット」23 回）。
        /// 待機場所は1台に1つなので、重ならない。
        /// 料理の割り当て（AssignRobots）はこの前に走るので、載せる台はそのまま走り出す。
        /// </summary>
        void ParkIdleRobots()
        {
            if (store == null) return;
            IList<Vector3> spots = store.RobotStandbyPoints;
            if (spots.Count == 0) return;

            for (int i = 0; i < _robots.Count; i++)
            {
                ServingRobotController r = _robots[i];
                if (r == null) continue;
                if (r.CurrentPhase != ServingRobotController.Phase.Idle &&
                    r.CurrentPhase != ServingRobotController.Phase.Done) continue;

                Vector3 spot = spots[i % spots.Count];
                r.SetStandbyPoint(spot);   // 戻りの線の終わりに使う（v5）
                r.GoStandby(spot);
            }
        }

        void Recount()
        {
            int free = 0;
            for (int i = 0; i < _tables.Count; i++) if (_tables[i].Free) free++;
            _freeTables = free;

            int d = 0;
            for (int i = 0; i < _pass.Count; i++) if (!_pass[i].taken) d++;
            _dishesWaiting = d;

            int j = 0;
            for (int i = 0; i < _jobs.Count; i++) if (!_jobs[i].taken) j++;
            _jobsWaiting = j;
        }

        // ---------------- 卓 ----------------

        /// <summary>
        /// ほかの台が向かっている、または中にいる細い通路の番号（v5・A）。
        ///   ・まだ届けていない卓の通路（往復の途中のもの）
        ///   ・いま体がその通路の区画の中にある台の通路（戻り道で通っているときも押さえる）
        /// </summary>
        HashSet<int> AislesTakenByOthers(ServingRobotController self)
        {
            HashSet<int> busy = new HashSet<int>();

            for (int i = 0; i < _robots.Count; i++)
            {
                ServingRobotController o = _robots[i];
                if (o == null || o == self) continue;
                if (o.CurrentPhase == ServingRobotController.Phase.Idle ||
                    o.CurrentPhase == ServingRobotController.Phase.Done) continue;

                IReadOnlyList<int> ids = o.TableIds;
                for (int k = o.StopsCompleted; k < ids.Count; k++)
                {
                    TableSlot s = SlotOfTableId(ids[k]);
                    if (s != null && s.aisleIndex >= 0) busy.Add(s.aisleIndex);
                }

                if (store == null) continue;
                IList<StoreBuilder.AisleInfo> aisles = store.Aisles;
                Vector3 p = o.transform.position;
                for (int a = 0; a < aisles.Count; a++)
                {
                    Bounds b = aisles[a].zone;
                    if (p.x < b.min.x || p.x > b.max.x || p.z < b.min.z || p.z > b.max.z) continue;
                    busy.Add(a);
                }
            }
            return busy;
        }

        TableSlot SlotOfTableId(int tableId)
        {
            for (int i = 0; i < _tables.Count; i++) if (_tables[i].tableId == tableId) return _tables[i];
            return null;
        }

        /// <summary>
        /// その人数が座れる空き卓を1つ取る。無ければ null。
        /// 「入るうちでいちばん小さい卓」を選ぶので、
        ///   ・1〜2人の組は空いている2人席を先に使い、無ければ4人席
        ///   ・3〜4人の組は4人席だけ
        /// になる（2人席に3人以上は座らせない）。
        /// </summary>
        public TableSlot TakeFreeTable(int groupId, int size)
        {
            TableSlot slot = FindFreeTable(size);
            if (slot == null) return null;

            slot.groupId = groupId;
            Recount();
            return slot;
        }

        /// <summary>その人数が入るうちで、いちばん席数の少ない空き卓。無ければ null</summary>
        TableSlot FindFreeTable(int size)
        {
            int want = Mathf.Max(1, size);
            TableSlot best = null;
            int bestSeats = int.MaxValue;

            for (int i = 0; i < _tables.Count; i++)
            {
                if (!_tables[i].Free) continue;
                int seats = Mathf.Max(1, _tables[i].seatCount);
                if (seats < want) continue;
                if (seats >= bestSeats) continue;
                best = _tables[i];
                bestSeats = seats;
                if (bestSeats == want) break;      // ちょうどの大きさが見つかった
            }
            return best;
        }

        /// <summary>その人数が座れる空き卓があるか</summary>
        public bool HasFreeTableFor(int size)
        {
            return FindFreeTable(size) != null;
        }

        /// <summary>慣らしのために、卓を使っている状態で始める</summary>
        public TableSlot TakeTableAt(int index, int groupId)
        {
            if (index < 0 || index >= _tables.Count) return null;
            if (!_tables[index].Free) return null;
            _tables[index].groupId = groupId;
            Recount();
            return _tables[index];
        }

        /// <summary>片付けが終わって卓が空く</summary>
        public void ReleaseTable(TableSlot slot, float leftS)
        {
            if (slot == null) return;
            slot.groupId = -1;
            slot.clearing = false;
            if (leftS >= 0f) turnMin.Add((NowS - leftS) / 60f);
            Recount();
        }

        public void RecordSeatWait(float arriveS)
        {
            seatWaitMin.Add(Mathf.Max(0f, (NowS - arriveS) / 60f));
        }

        /// <summary>
        /// 入店待ちの内訳を1組ぶん残す（v5 3章）。卓に着いた時に GroupManager が呼ぶ。
        /// 分け方はサイトの試算（haizen_sim.py）と同じ。
        ///   entryWaitS   … 店に来てから卓に着くまで（案内に歩いた時間と guide_s を含む）
        ///   seatWaitS    … 店に来てから案内が始まるまで
        ///   blockedWaitS … そのうち、卓は空いたのに片付けが終わらず待った分
        ///   guideWaitS   … 案内の仕事が板に出てから、スタッフが取るまで
        /// full_wait は seat_wait − blocked_wait、staff_wait は blocked_wait ＋ guide_wait。
        /// </summary>
        public void RecordEntryWait(float entryWaitS, float seatWaitS,
                                    float blockedWaitS, float guideWaitS)
        {
            float blocked = Mathf.Clamp(blockedWaitS, 0f, Mathf.Max(0f, seatWaitS));
            float full = Mathf.Max(0f, seatWaitS - blocked);
            float guide = Mathf.Max(0f, guideWaitS);

            entryWaitMin.Add(Mathf.Max(0f, entryWaitS / 60f));
            fullWaitMin.Add(full / 60f);
            blockedWaitMin.Add(blocked / 60f);
            guideWaitMin.Add(guide / 60f);
            staffWaitMin.Add((blocked + guide) / 60f);
        }

        /// <summary>
        /// 片付け待ちの卓の数（v5 3章・サイトの _acc と同じ blocked_wait を数えるのに使う）。
        /// 客が帰ってから、拭き終わって使えるようになるまでの卓
        /// </summary>
        public int ClearingTableCount
        {
            get
            {
                int n = 0;
                for (int i = 0; i < _tables.Count; i++) if (_tables[i].clearing) n++;
                return n;
            }
        }

        // ---------------- 立ち往生でスタッフを呼ぶ（v5 3章）----------------

        /// <summary>
        /// 動けなくなったロボットのところへスタッフを呼ぶ。
        /// サイトの決まりで「ほかの仕事より先」なので、優先度 0 で板の先頭に出す。
        /// 受け渡し口へ移す（warp）のは、もうしない。
        /// </summary>
        public StaffJob PostRescue(ServingRobotController robot)
        {
            if (robot == null) return null;
            StaffJob j = NewJob(StaffJobKind.Rescue, 0, null, null, 0);
            j.robot = robot;
            _jobs.Insert(0, j);
            rescueCalls++;
            Recount();
            return j;
        }

        /// <summary>スタッフが着いて rescue_s 動かし終わった（ロボットが呼ぶ）</summary>
        public void RescueFinished(float waitedS)
        {
            rescueDone++;
            rescueWaitSeconds += Mathf.Max(0f, waitedS);
        }

        /// <summary>
        /// そのロボットに出ている Rescue の仕事を板から外す（往復を打ち切ったときなど）。
        /// まだ誰も取っていないものは板から消す。
        /// もう誰かが向かっている途中のものは消さず、相手の台だけ外す
        /// （歩き出したスタッフはそのまま着いて終える。消すとその人の仕事が宙に浮く）。
        /// </summary>
        public void CancelRescue(ServingRobotController robot)
        {
            for (int i = _jobs.Count - 1; i >= 0; i--)
            {
                StaffJob j = _jobs[i];
                if (j.kind != StaffJobKind.Rescue || j.robot != robot) continue;
                if (j.taken) j.robot = null;
                else _jobs.RemoveAt(i);
            }
            Recount();
        }

        // ---------------- あきらめた記録（B1）----------------

        /// <summary>あきらめた・移した・卓を空けたことを1行残す</summary>
        public void AddGiveUp(string who, string job, int tableId, Vector3 to, Vector3 at,
                              float waitedS, string action)
        {
            GiveUpRow r = new GiveUpRow();
            r.tS = NowS;
            r.who = who;
            r.job = job;
            r.tableId = tableId;
            r.toX = to.x;
            r.toZ = to.z;
            r.x = at.x;
            r.z = at.z;
            r.aisle = AisleNameAt(at);
            r.waitedS = waitedS;
            r.action = action;
            _giveUps.Add(r);

            if (action == "移した") warpedCount++;
            else if (action == "卓を空けた") tableFreedCount++;
            else gaveUpCount++;
        }

        // ---------------- 人がロボットをよけた記録 ----------------

        /// <summary>人がロボットの外周の近くまで来た1回を残す（接触に近い）</summary>
        public void AddRobotContact(Vector3 at, string who, bool careless, int robotId, float gapM)
        {
            ContactRow r = new ContactRow();
            r.tS = NowS;
            r.x = at.x;
            r.z = at.z;
            r.aisle = AisleNameAt(at);
            r.place = store != null ? store.PlaceNameAt(at) : "";
            r.who = who;
            r.careless = careless;
            r.robotId = robotId;
            r.gapM = gapM;
            _contacts.Add(r);
        }

        /// <summary>人がロボットを待ち始めた（入口で待つ・通路の中で下がる）</summary>
        public void AddRobotYieldWait() { robotYieldWaits++; }

        /// <summary>人がロボットを待っていた秒数を足す</summary>
        public void AddRobotYieldWaitSeconds(float dt) { robotYieldWaitSeconds += dt; }

        /// <summary>人が回り道した</summary>
        public void AddRobotYieldDetour() { robotYieldDetours++; }

        /// <summary>
        /// 店員優先の版の逃げ道：相手のロボットが別の理由で止まったまま
        /// staffFirstRobotStuckSec（60 秒）たって、店員が回り道した回数（v5）
        /// </summary>
        public int robotStuckDetours { get; private set; }

        public void AddRobotStuckDetour() { robotStuckDetours++; }

        // ---------------- 椅子を引いた回数（B6）----------------

        /// <summary>椅子を1脚引いた。きっかけごと・卓ごとに数える</summary>
        public void AddChairPull(string reason, int tableId)
        {
            chairPulls++;

            string key = string.IsNullOrEmpty(reason) ? "その他" : reason;
            int n;
            _chairPullsByReason[key] = _chairPullsByReason.TryGetValue(key, out n) ? n + 1 : 1;
            _chairPullsByTable[tableId] = _chairPullsByTable.TryGetValue(tableId, out n) ? n + 1 : 1;
        }

        /// <summary>その点がどの通路の上かを名前で返す。細い通路を先に見る</summary>
        public string AisleNameAt(Vector3 p)
        {
            if (store == null) return "";
            string wide = "";
            IList<StoreBuilder.AisleInfo> aisles = store.Aisles;
            for (int i = 0; i < aisles.Count; i++)
            {
                Bounds b = aisles[i].zone;
                if (p.x < b.min.x || p.x > b.max.x || p.z < b.min.z || p.z > b.max.z) continue;
                if (aisles[i].narrow) return aisles[i].name;
                if (wide.Length == 0) wide = aisles[i].name;
            }
            return wide;
        }

        // ---------------- 料理 ----------------

        /// <summary>
        /// 料理が1皿できて、受け渡し口に出る。
        ///
        /// 割り当ての決まりはサイトの試算 v2.7 にそろえる:
        ///   ・まずロボットを待つ。そのあいだスタッフは料理を持たず、案内・片付け・会計を続ける
        ///     （＝スタッフの仕事板に Serve を出さない。ロボットは _pass を直に見るので受け持てる）
        ///   ・robot_first_wait_s（common.robot_first_wait_s）を過ぎてもロボットが受け持たなければ、
        ///     そこからはロボットとスタッフのうち先に空いた方が運ぶ（ReleaseDishesToStaff が出す）
        ///   ・割れ物を人が運ぶ設定（fragile_by=staff）の倒れやすい料理と、ロボット0台のときは、
        ///     今までどおり、はじめからスタッフが運ぶ
        /// </summary>
        public DishOrder PutDish(int groupId, TableSlot slot, bool fragile)
        {
            DishOrder d = new DishOrder();
            d.id = _nextDishId++;
            d.groupId = groupId;
            d.tableIndex = slot.index;
            d.tableId = slot.tableId;
            d.readyS = NowS;
            d.fragile = fragile;
            d.staffFromStart = StaffCarriesFromStart(d);
            _pass.Add(d);

            if (d.staffFromStart)
            {
                // ロボットが受け持てない皿。待たせる意味がないので、はじめから仕事板に出す
                d.staffEligibleS = NowS;
                PostJob(StaffJobKind.Serve, groupId, slot, d, 1);
            }
            Recount();
            return d;
        }

        /// <summary>
        /// はじめからスタッフも運べる皿か（サイトの試算 v2.7）。
        ///   ・割れ物を人が運ぶ設定（fragile_by=staff）の、倒れやすい料理
        ///   ・ロボットが1台もいないとき
        ///   ・robot_first_wait_s が 0 以下のとき（v2.7 より前と同じ回し方に戻せる逃がし口）
        /// </summary>
        bool StaffCarriesFromStart(DishOrder d)
        {
            if (spec == null) return true;
            if (spec.robotFirstWaitSec <= 0f) return true;
            if (!spec.fragileByRobot && d.fragile) return true;
            return RobotCount == 0;
        }

        /// <summary>動いているロボットの数（null の枠は数えない）</summary>
        public int RobotCount
        {
            get
            {
                int n = 0;
                for (int i = 0; i < _robots.Count; i++) if (_robots[i] != null) n++;
                return n;
            }
        }

        /// <summary>
        /// ロボットだけを待つ時間を過ぎた皿を、スタッフの仕事板にも出す（サイトの試算 v2.7）。
        ///
        /// 待った時間は **料理ができた時（readyS）から**数える。
        /// 同じ通路に2台を集めない決まり（oneRobotPerAisle）で後に回した皿も、
        /// 受け渡し口に戻された皿も、この数え方は変えない
        /// （後に回された分だけ待ちが延びるのを、そのまま出すため）。
        /// </summary>
        void ReleaseDishesToStaff()
        {
            if (spec == null) return;
            float wait = spec.robotFirstWaitSec;

            int waitingForRobot = 0;
            for (int i = 0; i < _pass.Count; i++)
            {
                DishOrder d = _pass[i];
                if (d.arrivedS >= 0f) continue;

                // もうスタッフも運べる皿（はじめから運べる皿も含む）
                if (d.staffEligibleS >= 0f) continue;

                if (NowS - d.readyS < wait)
                {
                    // まだロボットだけを待つ時間の中。誰も持っていなければ「ロボットを待っている皿」
                    if (!d.taken) waitingForRobot++;
                    continue;
                }

                d.staffEligibleS = NowS;
                d.robotWaitExpired = true;

                // 誰かが持っている皿（ロボットが載せた・運んでいる途中）は、板に出さない。
                // 戻されたときに RequeueDish が出し直す
                if (d.taken) continue;

                TableSlot slot = SlotOf(d.tableIndex);
                if (slot != null) PostJob(StaffJobKind.Serve, d.groupId, slot, d, 1);
            }
            _dishesWaitingForRobot = waitingForRobot;
        }

        /// <summary>
        /// 慣らしのために、もう届いている皿として1枚作る（B3）。
        /// 受け渡し口にも仕事板にも出さず、待ち時間の記録にも入れない
        /// （慣らしの皿を 0 分として数えると dish_wait が小さく出てしまう）。
        /// </summary>
        public DishOrder SeedArrivedDish(int groupId, TableSlot slot)
        {
            DishOrder d = new DishOrder();
            d.id = _nextDishId++;
            d.groupId = groupId;
            d.tableIndex = slot != null ? slot.index : -1;
            d.tableId = slot != null ? slot.tableId : 0;
            d.readyS = NowS;
            d.arrivedS = NowS;
            d.fragile = false;
            return d;
        }

        /// <summary>届いた（ロボットでもスタッフでも）</summary>
        /// <summary>料理を持ち上げた。受け渡し口で待った時間・運んでいた時間を出すために時刻を残す</summary>
        void MarkTaken(DishOrder d)
        {
            if (d == null) return;
            d.taken = true;
            if (d.firstTakenS < 0f) d.firstTakenS = NowS;
            d.lastTakenS = NowS;
        }

        public void DishArrived(DishOrder d, bool byRobot)
        {
            if (d == null || d.arrivedS >= 0f) return;
            d.arrivedS = NowS;
            d.byRobot = byRobot;
            dishWaitMin.Add(Mathf.Max(0f, (d.arrivedS - d.readyS) / 60f));
            if (byRobot) robotDishes++; else staffDishes++;
            AddDishParts(d);
            _pass.Remove(d);
            RemoveServeJob(d);
            Recount();
        }

        /// <summary>
        /// 届けられなかった料理を、受け渡し口の待ち行列の先頭に戻す。
        /// 次に空いたロボットかスタッフが運ぶ。
        /// </summary>
        /// <summary>
        /// 届いた1皿の、届くまでの内訳を足す。
        ///   受け渡し口で待った ＋ 運んでいた ＋ 途中で戻されて失った ＝ できてから届くまで
        /// </summary>
        void AddDishParts(DishOrder d)
        {
            if (d == null || d.arrivedS < 0f || d.readyS < 0f) return;

            float total = Mathf.Max(0f, d.arrivedS - d.readyS);
            float passWait = d.firstTakenS >= 0f ? Mathf.Max(0f, d.firstTakenS - d.readyS) : total;
            float carry = d.lastTakenS >= 0f ? Mathf.Max(0f, d.arrivedS - d.lastTakenS) : 0f;

            DishParts p = new DishParts();
            p.byRobot = d.byRobot;
            p.passWaitS = passWait;
            p.carryS = carry;
            p.requeueS = Mathf.Max(0f, total - passWait - carry);
            p.requeued = d.requeued;
            _dishParts.Add(p);

            // ---- 皿ごとの記録（dishes.csv・サイトの試算 v2.7）----
            // 「2分を過ぎて人が拾った皿」は、ロボットだけを待つ時間を過ぎてから
            // スタッフが運んだもの。はじめからスタッフが運ぶ皿
            // （fragile_by=staff・ロボット0台）は、待つ決まりが当てはまらないので数えない
            bool late = d.robotWaitExpired && !d.staffFromStart && !d.byRobot;

            DishRow row = new DishRow();
            row.dishId = d.id;
            row.tableId = d.tableId;
            row.groupId = d.groupId;
            row.fragile = d.fragile;
            row.readyS = d.readyS;
            row.firstTakenS = d.firstTakenS;
            row.arrivedS = d.arrivedS;
            row.byRobot = d.byRobot;
            row.passWaitS = passWait;
            row.carryS = carry;
            row.requeueS = p.requeueS;
            row.requeued = d.requeued;
            row.staffFromStart = d.staffFromStart;
            row.robotWaitExpired = d.robotWaitExpired;
            row.lateStaffPickup = late;
            row.deferredForAisle = d.deferredForAisle;
            _dishRows.Add(row);

            if (late) lateStaffDishes++;
        }

        public void RequeueDish(DishOrder d)
        {
            RequeueDish(d, 0, "");
        }

        /// <summary>戻した理由も残す（B8）。robotId 0 はロボット以外</summary>
        public void RequeueDish(DishOrder d, int robotId, string why)
        {
            if (d == null || d.arrivedS >= 0f) return;
            d.taken = false;
            d.requeued++;
            requeuedDishes++;

            // 1往復の打ち切りで戻された皿は数える。
            // 上限に届いたら、その皿は **ロボットに渡さずスタッフの仕事** にする
            // （2026-10-27。10/27 の夜の s0_passon で、同じ2皿を 57・58 回
            //   「取る→600 秒で打ち切り→戻す→すぐ取る」とくり返し、
            //   その皿を待っていた2組が 720 分まで帰れなかった）。
            // 皿は _pass に残すが PickBatch が拾わなくなるので、
            // 下の PostJobFront で板に出たものをスタッフが運ぶ
            bool flipped = false;
            if (why == "打ち切り")
            {
                d.robotTimeouts++;
                int limit = spec != null ? spec.maxRobotTripTimeoutsPerDish : 0;
                if (limit > 0 && !d.staffOnly && d.robotTimeouts >= limit)
                {
                    d.staffOnly = true;
                    flipped = true;
                    dishesToStaffAfterTimeout++;
                    // まだロボットだけを待つ時間の中でも、ここからはスタッフが運べる
                    if (d.staffEligibleS < 0f) d.staffEligibleS = NowS;
                    Debug.LogWarning("[ServiceSim] 皿 " + d.id + "（卓 " + d.tableId +
                                     "）は往復の打ち切りが " + d.robotTimeouts +
                                     " 回になったので、ロボットには渡さずスタッフの仕事にします");
                }
            }

            RequeueRow r = new RequeueRow();
            r.tS = NowS;
            r.dishId = d.id;
            r.tableId = d.tableId;
            r.robotId = robotId;
            r.fragile = d.fragile;
            r.readyS = d.readyS;
            r.requeuedSoFar = d.requeued;
            r.why = string.IsNullOrEmpty(why) ? "着かなかった" : why;
            r.toStaff = flipped;
            _requeues.Add(r);

            _pass.Remove(d);
            _pass.Insert(0, d);

            TableSlot slot = SlotOf(d.tableIndex);
            RemoveServeJob(d);

            // サイトの試算 v2.7：まだロボットだけを待つ時間の中なら、板に出さずロボットを待つ。
            // 待った時間は料理ができた時から数えるので、戻されても数え直さない
            // （過ぎていれば ReleaseDishesToStaff が staffEligibleS を入れている）
            if (slot != null && d.staffEligibleS >= 0f)
                PostJobFront(StaffJobKind.Serve, d.groupId, slot, d, 1);
            Recount();
        }

        TableSlot SlotOf(int index)
        {
            return index >= 0 && index < _tables.Count ? _tables[index] : null;
        }

        // ---------------- スタッフの仕事板 ----------------

        public StaffJob PostJob(StaffJobKind kind, int groupId, TableSlot slot, DishOrder dish, int priority)
        {
            StaffJob j = NewJob(kind, groupId, slot, dish, priority);
            _jobs.Add(j);
            Recount();
            return j;
        }

        StaffJob PostJobFront(StaffJobKind kind, int groupId, TableSlot slot, DishOrder dish, int priority)
        {
            StaffJob j = NewJob(kind, groupId, slot, dish, priority);
            _jobs.Insert(0, j);
            Recount();
            return j;
        }

        StaffJob NewJob(StaffJobKind kind, int groupId, TableSlot slot, DishOrder dish, int priority)
        {
            StaffJob j = new StaffJob();
            j.kind = kind;
            j.priority = priority;
            j.groupId = groupId;
            j.tableIndex = slot != null ? slot.index : -1;
            j.tableId = slot != null ? slot.tableId : 0;
            j.dish = dish;
            j.postedS = NowS;
            return j;
        }

        /// <summary>
        /// 次にやる仕事を1つ取る。優先度が小さいものが先、同じなら出た順。
        /// 配膳の仕事は、料理がまだ誰も持っていないものだけ。
        /// 立ち往生（Rescue）は優先度 0 なので、ほかの仕事より先に取られる。
        /// </summary>
        public StaffJob TakeNextJob()
        {
            StaffJob best = null;
            for (int i = 0; i < _jobs.Count; i++)
            {
                StaffJob j = _jobs[i];
                if (j.taken) continue;
                if (j.kind == StaffJobKind.Serve && (j.dish == null || j.dish.taken || j.dish.arrivedS >= 0f)) continue;
                // 店員側（レジの内側）は1人ぶんなので、会計はひとつずつ
                if (j.kind == StaffJobKind.Pay && RegisterBackBusy) continue;
                if (best == null || j.priority < best.priority) best = j;
                if (best != null && best.priority <= 0) break;   // 優先0（立ち往生）が見つかれば、それが先
            }
            if (best == null) return null;
            best.taken = true;
            best.takenS = NowS;
            if (best.kind == StaffJobKind.Pay) RegisterBackBusy = true;
            RecordJobWait(best);
            if (best.kind == StaffJobKind.Serve && best.dish != null) MarkTaken(best.dish);
            Recount();
            return best;
        }

        /// <summary>仕事が終わった</summary>
        public void FinishJob(StaffJob job)
        {
            if (job == null) return;
            if (job.kind == StaffJobKind.Pay) RegisterBackBusy = false;   // 店員側を空ける
            _jobs.Remove(job);
            Recount();
            if (job.onDone != null) job.onDone();
        }

        /// <summary>
        /// 仕事を板から外すだけ（終わったときの呼び出しはしない）。
        /// 卓を強引に空けたときなど、出した側がもう待っていないときに使う。
        /// </summary>
        public void FinishJobSilently(StaffJob job)
        {
            if (job == null) return;
            if (job.kind == StaffJobKind.Pay) RegisterBackBusy = false;   // 店員側を空ける
            job.onDone = null;
            _jobs.Remove(job);
            if (job.kind == StaffJobKind.Serve && job.dish != null) job.dish.taken = false;
            Recount();
        }

        /// <summary>仕事を手放す（届けられなかったときなど）</summary>
        public void AbandonJob(StaffJob job)
        {
            if (job == null) return;
            job.taken = false;
            if (job.kind == StaffJobKind.Pay) RegisterBackBusy = false;   // 店員側を空ける
            if (job.kind == StaffJobKind.Serve && job.dish != null) job.dish.taken = false;
            Recount();
        }

        void RemoveServeJob(DishOrder d)
        {
            for (int i = _jobs.Count - 1; i >= 0; i--)
                if (_jobs[i].kind == StaffJobKind.Serve && _jobs[i].dish == d) _jobs.RemoveAt(i);
        }

        // ---------------- ロボットに渡す ----------------

        /// <summary>
        /// 空いているロボットに、受け渡し口の料理を段の数までまとめて渡す。
        /// 近い卓から回る（試算の _robot_run と同じ）。
        /// </summary>
        void AssignRobots()
        {
            if (spec == null || store == null) return;

            for (int i = 0; i < _robots.Count; i++)
            {
                ServingRobotController r = _robots[i];
                if (r == null) continue;
                if (r.CurrentPhase != ServingRobotController.Phase.Idle &&
                    r.CurrentPhase != ServingRobotController.Phase.Done) continue;

                List<DishOrder> batch = PickBatch(r);
                if (batch.Count == 0) continue;

                // 止まる所と皿を、同じ並びで組む（依頼 v8 の 1）。
                // SlotOf が null の皿は止まる所を作れないので、どちらからも外す
                List<ServingRobotController.RunStop> stops = new List<ServingRobotController.RunStop>();
                List<DishOrder> stopDishes = new List<DishOrder>();
                for (int k = 0; k < batch.Count; k++)
                {
                    TableSlot s = SlotOf(batch[k].tableIndex);
                    if (s == null) continue;
                    ServingRobotController.RunStop stop = new ServingRobotController.RunStop();
                    stop.tableId = s.tableId;
                    stop.point = s.deliveryPoint;
                    stop.aisleEntrance = s.aisleEntrance;
                    stop.aisleZone = s.aisleZone;
                    stops.Add(stop);
                    stopDishes.Add(batch[k]);
                }
                if (stops.Count == 0) continue;

                _carrying[r] = batch;
                _stopDishes[r] = stopDishes;
                r.StartRun(stops, store.PassPoint, NowS);
            }
        }

        /// <summary>段の数までまとめる。近い卓から順に並べる</summary>
        List<DishOrder> PickBatch(ServingRobotController self)
        {
            List<DishOrder> batch = new List<DishOrder>();
            Vector3 pass = store.PassPoint;
            int trays = Mathf.Max(1, spec.trays);

            // 同じ通路に2台を集めない（v5・A）。
            // ほかの台が向かっている・中にいる通路の卓は、この回では受け持たない。
            // 1往復で3卓まとめるときも入れない（この for で毎回見る）
            bool oneEach = robotSpec != null && robotSpec.oneRobotPerAisle;
            HashSet<int> busy = oneEach ? AislesTakenByOthers(self) : null;

            for (int i = 0; i < _pass.Count && batch.Count < trays; i++)
            {
                DishOrder d = _pass[i];
                if (d.taken || d.arrivedS >= 0f) continue;
                if (!spec.fragileByRobot && d.fragile) continue;   // 人だけが運ぶ料理
                if (d.staffOnly) continue;                         // 打ち切りが続いてスタッフに回した皿（2026-10-27）
                if (busy != null)
                {
                    TableSlot slot = SlotOf(d.tableIndex);
                    if (slot != null && slot.aisleIndex >= 0 && busy.Contains(slot.aisleIndex))
                    {
                        // 数えるのは1皿につき1回だけ（毎コマ数えると意味のない大きさになる）
                        if (!d.deferredForAisle) { d.deferredForAisle = true; aisleDeferrals++; }
                        continue;
                    }
                }
                batch.Add(d);
            }

            batch.Sort(delegate (DishOrder a, DishOrder b)
            {
                TableSlot sa = SlotOf(a.tableIndex), sb = SlotOf(b.tableIndex);
                float da = sa != null ? Flat(sa.deliveryPoint - pass) : 0f;
                float db = sb != null ? Flat(sb.deliveryPoint - pass) : 0f;
                return da.CompareTo(db);
            });

            for (int i = 0; i < batch.Count; i++)
            {
                MarkTaken(batch[i]);
                RemoveServeJob(batch[i]);
            }
            return batch;
        }

        static float Flat(Vector3 d) { d.y = 0f; return d.magnitude; }

        /// <summary>
        /// ロボットが止まる所（卓）に着いた（依頼 v8 の 1）。
        ///
        /// サイトの試算と同じで、**卓に着いた時点でその皿は届いた**。
        /// 受け渡しの 25 秒（RobotSpec.tableDwellSec）はこのあとなので、
        /// 客は受け渡し口へ戻るのを待たずに食べ始められる
        /// （前は往復の最後にまとめて届いたにしていて、料理の待ちが
        ///  1皿あたり 1.0〜1.3 分長く出ていた）。
        /// </summary>
        void OnRobotStopArrived(ServingRobotController r, int stopIndex)
        {
            List<DishOrder> stopDishes;
            if (!_stopDishes.TryGetValue(r, out stopDishes) || stopDishes == null) return;
            if (stopIndex < 0 || stopIndex >= stopDishes.Count) return;

            DishArrived(stopDishes[stopIndex], true);
        }

        /// <summary>
        /// ロボットの1走りぶんの片づけが終わった（依頼 v8 の 2）。
        ///
        /// ExperimentRunner は trips.csv を書くときに LastBatchOf を読むので、
        /// ServingRobotController.TripCompleted を直に受けると、呼ばれる順番によって
        /// **1つ前の往復の皿**を読んでしまっていた。
        /// ここで片づけを終えてから出すこのイベントを受ければ、順番に頼らずに済む。
        /// </summary>
        public event System.Action<ServingRobotController> RunRecorded;

        /// <summary>
        /// ロボットの1走りが終わった。
        /// 卓に着いた皿はもう届いているので何もしない。
        /// まだ届いていない皿（arrivedS &lt; 0）だけを受け渡し口の先頭へ戻す。
        /// </summary>
        void OnRobotRunDone(ServingRobotController r)
        {
            List<DishOrder> batch;
            if (!_carrying.TryGetValue(r, out batch) || batch == null) return;

            _lastBatch[r] = batch;      // trips.csv を書くために残す
            _carrying[r] = new List<DishOrder>();
            _stopDishes[r] = new List<DishOrder>();

            // なぜ届けられなかったか。ロボットが持っている結果から決める。
            // v5 3章：動けなくなってもスタッフが動かせば走りは続くので、
            // 「動けなくなった」は打ち切り・取りやめより後ろに置く（往復が終わった理由が先）
            string why = r.TimedOut ? "打ち切り"
                       : r.Aborted ? "取りやめ"
                       : r.Stuck ? "動けなくなった"
                       : "着かなかった";

            for (int i = 0; i < batch.Count; i++)
            {
                // 卓に着いた時点で DishArrived してあるので、arrivedS で見分ける
                // （StopsCompleted の数で数えない。止まる所と皿の並びは対応表で持っている）
                if (batch[i].arrivedS >= 0f) continue;
                RequeueDish(batch[i], r.robotId, why);
            }

            // 片づけが終わってから知らせる（ここで LastBatchOf を読んでよい）
            if (RunRecorded != null) RunRecorded(r);
        }

        /// <summary>そのロボットがいま載せている料理</summary>
        public IList<DishOrder> CarriedBy(ServingRobotController r)
        {
            List<DishOrder> batch;
            if (r != null && _carrying.TryGetValue(r, out batch)) return batch;
            return new List<DishOrder>();
        }

        /// <summary>直前の1走りで載せていた料理（trips.csv を書くのに使う）</summary>
        public IList<DishOrder> LastBatchOf(ServingRobotController r)
        {
            List<DishOrder> batch;
            if (r != null && _lastBatch.TryGetValue(r, out batch) && batch != null) return batch;
            return new List<DishOrder>();
        }
    }
}
```

### Builds/headless/build_info.json（3回目の夜のあとに作り直した実行ファイル）

```json
{
  "schema": "servicesim-build-info/0.1",
  "build_id": "cs27_20261027_1141",
  "unity_cs": "ServiceSim/cs:27",
  "pending_changes": "",
  "built_at": "2026-10-27 11:41:41",
  "editor_version": "2022.3.62f3",
  "target": "StandaloneOSX",
  "architecture": "ARM64（Apple Silicon）",
  "scene": "Assets/ServiceSim/Scenes/ServiceSim_Family.unity",
  "app": "Builds/headless/ServiceSim.app",
  "exe": "ServiceSim",
  "ledger_schema_version": 2,
  "total_size_bytes": 59966690,
  "build_seconds": 2.8
}
```


## 夜の実行係（ServiceSim の tools/・cs:27）

### tools/make_queue.py

```python
#!/usr/bin/env python3
"""make_queue.py — 夜に回す「条件 × 種」を queue.json に並べる（依頼 v12 の 4 章）

条件は Unity と同じ所から引く（手で写さない）:
  Assets/ServiceSim/Experiment/serving-robot-params.json の unity_conditions

最初の夜：6条件（③ fam_p1・⑦ fam_p5 × ロボット 0・1・2 台）× 種 0〜9 ＝ 60 回
          ＋ ③2台・すれ違い「入」× 種 0〜9 ＝ 10 回  … 合わせて 70 回

並べ方は **種ごと**（種0 の全7条件 → 種1 の全7条件 → …）。
同時に N 本走らせると最後の 70 mod N 本は同時本数が減るので、
条件ごとに並べるとその回がすれ違い「入」だけに偏ってしまう。
同時本数は stuck_count・robot_share などを動かすと分かっている
（busycheck・2026-10-26）ので、1つの条件に偏らせない。
スタッフ4人・42組/時・上げ膳（mode=serve）・1往復3卓・レジ1つは、
その JSON の条件がそのまま持っている値なので、ここでは選び直さない。

すれ違い（依頼 v13 の 依頼1）は条件ごとに持たせる（pass_by: on / off）。
標準は on（2026-10-27 エイイチの決定）。--pass-by off で切の回も作れる。
--pass-by-extra で「その条件だけ on の回」を足す（--pass-by off のときだけ使える。
--pass-by on と一緒だと同じ回が2回並び出力フォルダ名がぶつかるので、誤りとして止める）。
実行ファイルには -ssPassBy on|off で渡す（依頼 v12 の 2 章に足す引数）。

使い方（プロジェクトの中で）
  # 60回（すれ違い入。2026-10-27 から標準）
  python3 tools/make_queue.py --out queue.json
  # 最初の夜と同じ 70回（切の60回 ＋ ③2台の入10回）
  python3 tools/make_queue.py --out queue.json --pass-by off \
      --pass-by-extra fam_p1_s4_r2_a42
  python3 tools/make_queue.py --conditions fam_p1_s4_r1_a42 --seeds 1 --out /tmp/q.json

標準ライブラリだけで動く。Unity も台帳も読み書きしない。
"""
import argparse
import json
import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
PROJECT = os.path.dirname(HERE)
PARAMS = os.path.join(PROJECT, 'Assets', 'ServiceSim', 'Experiment',
                      'serving-robot-params.json')

# 条件の名前の付け方は Unity の SimParams.Condition.Id と同じにする。
#   {店}_s{スタッフ}_r{ロボット}_a{来客/時}   例: fam_p1_s4_r1_a42
# 実行ファイルに -ssCondition で渡す字（依頼 v12 の 2 章）
def condition_id(c):
    return '%s_s%d_r%d_a%d' % (c['store'], int(c['staff']), int(c['robots']),
                               int(c['arrivals_per_h']))


def parse_seeds(text):
    """'0-9' / '0,1,2' / '3' を種の一覧にする"""
    out = []
    for part in str(text).split(','):
        part = part.strip()
        if not part:
            continue
        if '-' in part[1:]:                      # 先頭の - は負の数のため避ける
            a, b = part.split('-', 1)
            lo, hi = int(a), int(b)
            if hi < lo:
                lo, hi = hi, lo
            out.extend(range(lo, hi + 1))
        else:
            out.append(int(part))
    seen, uniq = set(), []
    for s in out:
        if s not in seen:
            seen.add(s)
            uniq.append(s)
    return uniq


def load_conditions(path):
    with open(path, encoding='utf-8') as f:
        d = json.load(f)
    meta = d.get('meta') or {}
    conds = []
    for item in d.get('unity_conditions') or []:
        c = item.get('condition') or {}
        missing = [k for k in ('store', 'staff', 'robots', 'arrivals_per_h', 'mode')
                   if k not in c]
        if missing:
            sys.exit('unity_conditions に %s がありません: %r' % (missing, c))
        conds.append(c)
    return meta, conds


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--params', default=PARAMS, help='serving-robot-params.json')
    ap.add_argument('--seeds', default='0-9', help='種（例 0-9 / 0,1,2）')
    ap.add_argument('--conditions', default='',
                    help='条件の名前をカンマ区切りで絞る（既定は6条件ぜんぶ）')
    ap.add_argument('--label', default='night', help='出力フォルダに入る名札')
    ap.add_argument('--pass-by-extra', default='',
                    help='すれ違い「入」でも回す条件（カンマ区切り）。'
                         'その条件だけ、同じ種で on の回を足す（依頼 v13）')
    ap.add_argument('--pass-by', default='on', choices=('off', 'on'),
                    help='ふだんの回のすれ違い（既定 on。2026-10-27 エイイチの決定）')
    ap.add_argument('--out', default=os.path.join(PROJECT, 'queue.json'))
    a = ap.parse_args()

    if not os.path.exists(a.params):
        sys.exit('試算の値が見つかりません: %s' % a.params)

    meta, conds = load_conditions(a.params)
    seeds = parse_seeds(a.seeds)
    if not seeds:
        sys.exit('--seeds が空です')

    want = [s.strip() for s in a.conditions.split(',') if s.strip()]
    if want:
        known = {condition_id(c) for c in conds}
        bad = [w for w in want if w not in known]
        if bad:
            sys.exit('知らない条件です: %s\n  使える条件: %s'
                     % (', '.join(bad), ', '.join(sorted(known))))
        conds = [c for c in conds if condition_id(c) in want]

    extra = [s.strip() for s in a.pass_by_extra.split(',') if s.strip()]
    if extra and a.pass_by == 'on':
        sys.exit('--pass-by-extra は --pass-by off のときだけ使えます。\n'
                 '  --pass-by on（既定）では、ふだんの回がもう「入」なので、'
                 '同じ条件・同じ種の「入」が2回並び、出力フォルダ名がぶつかります。\n'
                 '  切の回に「入」を足すなら --pass-by off を付けてください。')
    known = {condition_id(c) for c in conds}
    bad = [w for w in extra if w not in known]
    if bad:
        sys.exit('--pass-by-extra に知らない条件があります: %s\n  使える条件: %s'
                 % (', '.join(bad), ', '.join(sorted(known))))

    def make(c, seed, pass_by):
        return {
            'condition': condition_id(c),
            'seed': seed,
            'pass_by': pass_by,          # 'on' / 'off'（実行ファイルへ -ssPassBy で渡す）
            'store': c['store'],
            'staff': int(c['staff']),
            'robots': int(c['robots']),
            'arrivals_per_h': int(c['arrivals_per_h']),
            'mode': c.get('mode', 'serve'),
            'trays': int(c.get('trays', 1)),
            'fragile_by': c.get('fragile_by', 'any'),
            'registers': int(c.get('registers', 1)),
        }

    # 1種ぶんの「条件の並び」を先に作る。
    #   ふだんの6条件（すれ違いは --pass-by。既定 on）
    #   ＋ --pass-by-extra に挙げた条件の「入」の回
    # 最初の夜は 6 ＋ 1 = 7 条件になる
    units = [(c, a.pass_by) for c in conds]
    for c in conds:
        if condition_id(c) not in extra:
            continue
        if int(c['robots']) < 2:
            print('  ※ %s はロボットが %d 台なので、すれ違いは起きません（足しますが参考）'
                  % (condition_id(c), int(c['robots'])))
        units.append((c, 'on'))

    # ★ 並べ方は **種ごと**（2026-10-26 エイイチの指示）。
    #   種0 の全条件 → 種1 の全条件 → … の順。
    #
    #   なぜ：同時に N 本走らせると、並びの最後の 70 mod N 本は
    #   同時本数が減った状態で回る。条件ごとに並べると、その「薄い」回が
    #   すれ違い「入」だけに集まってしまう。
    #   busycheck（2026-10-26）で、同時本数は
    #   stuck_count・rescues・robot_share・late_staff_dishes を動かすと
    #   分かっているので、1つの条件に偏らせない。
    #   種ごとに並べれば、薄くなるのは「最後の種の後ろの数条件」だけになり、
    #   どの条件も同じ扱いになる。
    runs = []
    for s in seeds:
        for c, pb in units:
            runs.append(make(c, s, pb))

    q = {
        'schema': 'servicesim-night-queue/0.1',
        'label': a.label,
        'params_model': meta.get('model', ''),
        'layout_generation': meta.get('layout_generation', ''),
        'params_source': os.path.relpath(a.params, PROJECT),
        'seeds': seeds,
        'conditions': [condition_id(c) for c in conds],
        'pass_by': a.pass_by,
        'pass_by_extra': extra,
        'order': 'by_seed',          # 種ごとに並べた（2026-10-26）
        'units_per_seed': len(units),
        'count': len(runs),
        'runs': runs,
    }

    out_dir = os.path.dirname(os.path.abspath(a.out))
    if out_dir and not os.path.isdir(out_dir):
        os.makedirs(out_dir)
    with open(a.out, 'w', encoding='utf-8') as f:
        json.dump(q, f, ensure_ascii=False, indent=1)
        f.write('\n')

    print('並べました: %s' % a.out)
    print('  並べ方: 種ごと（種%s の全%d条件 → 種%s の全%d条件 → …）'
          % (seeds[0], len(units), seeds[1] if len(seeds) > 1 else seeds[0], len(units)))
    print('  試算の版 %s / 店の形の版 %s' % (q['params_model'], q['layout_generation']))
    n_on = sum(1 for r in runs if r['pass_by'] == 'on')
    print('  条件 %d / 種 %s / 合わせて %d 回（すれ違い 切 %d 回・入 %d 回）'
          % (len(conds), ','.join(str(s) for s in seeds), len(runs), len(runs) - n_on, n_on))
    for cid in q['conditions']:
        mark = '（＋すれ違い入も %d 回）' % len(seeds) if cid in extra else ''
        print('    %s%s' % (cid, mark))


if __name__ == '__main__':
    main()
```

### tools/night_runner.py

```python
#!/usr/bin/env python3
"""night_runner.py — 夜のあいだ、並べた回を全部回す（依頼 v12 の 4 章）

  python3 tools/night_runner.py --queue queue.json --parallel 2 --night 20261026

やること
  ・始める前に、KIOXIA がつながっているか・実行ファイルがあるかを確かめる（無ければ始めない）
  ・caffeinate で Mac をスリープさせない（終わったら必ず解く）
  ・N 本まで同時に実行ファイルを起動する。1本終わったら次を起動する
  ・台帳に completed の行がある回は飛ばす（途中で止めても続きから回せる）
  ・1回の上限（既定 90 分）を超えたら止め、status=stopped の行を台帳に足す
  ・落ちた回は status=failed の行を足す
  ・止まった・落ちた回は、その夜のうちに1度だけやり直す（出力フォルダに _try2）
  ・途中で KIOXIA が外れたら、新しい回を始めずに待ち、走っている回が終わったら止まる
  ・時刻では止めない（並べた回が全部終わるまで回す）
  ・記録は service_sim/unity/night/{night}/ に3つ
      runner_log.txt … いつ・どの回を・何本目で始めて、どう終わったか（人が読む用）
      runs.csv       … 1回ごとの開始・終了の時刻（あとで重なりを計算する用）
      queue.json     … その夜に並べた回の写し（何をどの順で並べたか）
  ・全部終わったら judge_night.py を呼ぶ

実行ファイルの呼び方（依頼 v12 の 2 章）
  {実行ファイル} -batchmode -nographics -logFile {その回のログ}
      -ssCondition fam_p1_s4_r1_a42 -ssSeed 3 -ssLabel night -ssNight 20261026
      -ssParallel N  -ssRunning M   （N は決めた本数・M はそのとき実際に走る本数）
  終了コード：0 = 最後まで回った／2 = 途中で止まった／1 = 失敗

出力フォルダの名前（依頼 v12 の 2 章）
  {night}_{ラベル}_{cs}_{店}_r{台数}_s{種}        例 20261026_night_cs3_fam_p1_r1_s3
  ★ この形は **実行ファイル（Assets 側・依頼 v12 の 1〜3 章）と合わせる決まり**。
    いまの Unity 側（ServiceSimStageB.FolderName）は "cs{N}" なので、それにそろえてある。
    形は cs{N} に固定（Python だけの設定にすると C# と食い違うため。
    2026-10-26・codex レビュー A7）。

標準ライブラリだけで動く。台帳は tools/ledger.py を通してしか触らない。
"""
import argparse
import csv
import datetime as dt
import json
import os
import re
import shutil
import signal
import subprocess
import sys
import time

import ledger as L

HERE = os.path.dirname(os.path.abspath(__file__))
PROJECT = os.path.dirname(HERE)

DEFAULT_VOLUME = '/Volumes/KIOXIA'
DEFAULT_UNITY_DIR = '/Volumes/KIOXIA/r2r2r-data/service_sim/unity'
DEFAULT_BUILD = os.path.join(PROJECT, 'Builds', 'headless', 'ServiceSim.app',
                             'Contents', 'MacOS', 'ServiceSim')
DEFAULT_LIMIT_MIN = 90.0

COND_RE = re.compile(r'^(?P<store>[a-z0-9_]+?)_s(?P<staff>\d+)_r(?P<robots>\d+)_a(?P<arr>\d+)$')


# ---------------- 小さな道具 ----------------

class Log(object):
    def __init__(self, path):
        self.path = path
        d = os.path.dirname(path)
        if d and not os.path.isdir(d):
            os.makedirs(d)
        self._f = open(path, 'a', encoding='utf-8')

    def __call__(self, msg):
        line = '%s  %s' % (dt.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), msg)
        print(line, flush=True)
        self._f.write(line + '\n')
        self._f.flush()

    def close(self):
        try:
            self._f.close()
        except Exception:
            pass


def parse_condition(cid):
    m = COND_RE.match(cid)
    if not m:
        return None
    return {'store': m.group('store'), 'staff': int(m.group('staff')),
            'robots': int(m.group('robots')), 'arrivals_per_h': int(m.group('arr'))}


def build_info_path(build_path):
    """
    build_info.json の場所。**実行ファイルの .app と同じ所** だけを見る
    （依頼 v14 の 5）。

    実行ファイルは …/headless/ServiceSim.app/Contents/MacOS/ServiceSim なので、
    .app から1つ上（…/headless/）に build_info.json がある。
    2026-10-26：前は上に何階層も探したうえ、見つからなければ cm status に
    頼っていた。そのせいで「実行ファイルの版」ではなく
    「いまの作業ツリーの版」を台帳に書く道があった（取り違えの元）
    """
    p = os.path.abspath(build_path)
    # …/X.app/Contents/MacOS/exe → …/X.app
    app = p
    for _ in range(3):
        app = os.path.dirname(app)
    if app.endswith('.app'):
        return os.path.join(os.path.dirname(app), 'build_info.json')
    # .app の形でないとき（素の実行ファイル）は、そのそば
    return os.path.join(os.path.dirname(p), 'build_info.json')


def read_build_info(build_path):
    """
    build_info.json を読む。無い・読めないときは None
    （依頼 v14 の 5：cm status には頼らない）
    """
    p = build_info_path(build_path)
    if not os.path.exists(p):
        return None, p
    try:
        with open(p, encoding='utf-8') as f:
            return json.load(f), p
    except Exception:
        return None, p


def cs_token(unity_cs):
    """
    ServiceSim/cs:3 → cs3（出力フォルダの名前に入る字）

    2026-10-26（codex レビュー A7）：前は --cs-token で形を変えられたが、
    その設定は Python だけが持っていて実行ファイルには渡らないので、
    変えると **Python と C# でフォルダ名が食い違う**（実行係が
    「もう回した」と誤って飛ばす／同じ回を2度回す）。
    形は cs{N} に固定した。C# 側は HeadlessRun.CsToken が同じ式を持つ
    """
    m = re.search(r'cs:(\d+)', unity_cs or '')
    n = m.group(1) if m else 'unknown'
    return 'cs' + n


def norm_pass_by(v):
    """
    すれ違いの入切を on / off のどちらかにそろえる（2026-10-26・codex レビュー A7）。
    C# 側は ToLowerInvariant して "on" だけを入れ、それ以外は off にする。
    Python が大小をそのまま使うと、"On" で C# は off・Python は on 扱いになり
    フォルダ名が食い違う
    """
    return 'on' if str(v).strip().lower() == 'on' else 'off'


def folder_name(night, label, token, cond, seed, pass_by='off', tryno=1):
    """
    出力フォルダの名前。
    すれ違い「入」の回だけ _passon を付ける（2026-10-26。依頼 v13）。
    同じ条件・同じ種で 入 と 切 を両方回すので、付けないと名前がぶつかる。
    「切」の回の名前は付ける前と同じまま（基準データと並べるため）
    """
    name = '%s_%s_%s_%s_r%d_s%d' % (night, label, token, cond['store'], cond['robots'], seed)
    if norm_pass_by(pass_by) == 'on':
        name += '_passon'
    return name if tryno <= 1 else '%s_try%d' % (name, tryno)


def volume_ok(volume, unity_dir):
    return os.path.ismount(volume) and os.path.isdir(unity_dir)


# ---------------- 実行係 ----------------

class Runner(object):
    def __init__(self, a, log):
        self.a = a
        self.log = log
        self.unity_dir = a.unity_dir
        self.night_dir = os.path.join(a.unity_dir, 'night', a.night)
        self.limit_s = a.limit_min * 60.0
        self.build_info, self.build_info_at = read_build_info(a.build)
        self.unity_cs = ((self.build_info or {}).get('unity_cs') or '').strip()
        self.cs_from = self.build_info_at
        self.token = cs_token(self.unity_cs)
        self.build_id = ''
        self.running = []       # [{proc, item, folder, started, tryno, logfile}]
        self.done = []          # [(folder, status, wall_s, tryno, item)]
        self.retry = []         # やり直す回
        self.stopping = False   # KIOXIA が外れたら True（新しい回を始めない）
        self.runs_csv = None    # 1回ごとの開始・終了（2026-10-26）
        self.lost = 0           # 記録が欠けた数（codex レビュー C4）
        self.ledger_failed = []  # 台帳に書けなかった回（終了コード 3。依頼 v14 の 3）

    def note_lost(self, msg):
        """
        記録（runs.csv）が書けなかったとき。
        ログ自体が書けないこともあるので、必ず標準エラーにも出す。
        夜の本体は止めず、欠けた数を最後のまとめに出す（codex レビュー C4）
        """
        self.lost += 1
        try:
            self.log(msg)
        except Exception:
            pass
        try:
            sys.stderr.write('[night_runner] %s\n' % msg)
            sys.stderr.flush()
        except Exception:
            pass

    # ---- 前の確かめ ----
    def precheck(self, queue):
        ok = True
        if not volume_ok(self.a.volume, self.unity_dir):
            self.log('NG: KIOXIA がつながっていません（%s / %s）' % (self.a.volume, self.unity_dir))
            ok = False
        if not (os.path.isfile(self.a.build) and os.access(self.a.build, os.X_OK)):
            # から回し（--dry-run）は、実行ファイルを作る前に打つコマンドを見るためのものなので通す
            msg = '実行ファイルがありません（または実行できません）: %s' % self.a.build
            if self.a.dry_run:
                self.log('（から回しなので通します）' + msg)
            else:
                self.log('NG: ' + msg)
                ok = False
        if ok:
            try:
                cols = L.read_header(self.unity_dir)
                self.log('台帳: %s（%d 列）' % (L.ledger_path(self.unity_dir), len(cols)))
            except L.LedgerError as e:
                self.log('NG: %s' % e)
                ok = False
        # ★ 版は build_info.json からしか取らない（依頼 v14 の 5）。
        #   無ければ始めない（cm status には頼らない）
        if self.build_info is None:
            msg = ('build_info.json がありません（実行ファイルの .app と同じ所）: %s\n'
                   '  実行ファイルを ServiceSim/画面なしの実行ファイルを作る で'
                   '作り直してください' % self.build_info_at)
            if self.a.dry_run:
                self.log('（から回しなので通します）' + msg)
            else:
                self.log('NG: ' + msg)
                ok = False
        elif not self.unity_cs:
            msg = ('build_info.json に unity_cs がありません: %s' % self.build_info_at)
            if self.a.dry_run:
                self.log('（から回しなので通します）' + msg)
            else:
                self.log('NG: ' + msg)
                ok = False
        else:
            self.log('版: %s（%s）→ 出力フォルダの字 %s'
                     % (self.unity_cs, os.path.relpath(self.build_info_at, PROJECT), self.token))

        # ★ 保留がある状態で作った実行ファイルでは始めない（依頼 v14 の 6）。
        #   台帳の unity_cs が cs:N なのに中身が違う、という取り違えが残る
        pending = ((self.build_info or {}).get('pending_changes') or '').strip()
        if pending:
            msg = ('実行ファイルは保留中の変更があるまま作られています: %s\n'
                   '  台帳の unity_cs（%s）と中身が合いません。'
                   'check-in して保留 0 で作り直してください' % (pending, self.unity_cs))
            if self.a.dry_run:
                self.log('（から回しなので通します）' + msg)
            else:
                self.log('NG: ' + msg)
                ok = False
        if self.build_info is not None:
            self.build_id = (self.build_info.get('build_id') or '').strip()

        pm = queue.get('params_model', '')
        if pm:
            self.log('並べた回の試算の版: %s / 店の形の版: %s'
                     % (pm, queue.get('layout_generation', '')))
        return ok

    # ---- 1回ごとの開始・終了（runs.csv。2026-10-26）----
    #
    # なぜ要るか：run_info.json の parallel_actual は **起動したときの** 本数なので、
    # 並びの最後で同時本数が減るのは出てこない（減るのは回している途中）。
    # 1回ごとの開始・終了の時刻をそのまま残せば、
    # 「その回が回っている間、ほかに何本走っていたか」を **あとから正確に計算**できる。
    # 台帳の列は増やさない（data_manifest_rules.md 10節）。
    # started_mono / ended_mono は単調時計（time.monotonic）の値
    # （2026-10-26・codex レビュー A10）。
    # 夜の途中で時計が合わせ直されても、重なりの計算が狂わないため。
    # 絶対の時刻（人が読む用）は started_iso / started_unix を見る
    RUNS_CSV_HEADER = ('folder,condition,seed,pass_by,tryno,launch_running,parallel,'
                       'started_iso,ended_iso,started_unix,ended_unix,'
                       'started_mono,ended_mono,wall_s,status,exit_code\n')

    def _runs_csv_path(self):
        return os.path.join(self.night_dir, 'runs.csv')

    def _runs_csv_open(self):
        """追記で開く。ヘッダはコードの並びと同じでなければ足さない（台帳と同じ用心）"""
        if self.runs_csv is not None or self.a.dry_run:
            return
        p = self._runs_csv_path()
        if not volume_ok(self.a.volume, self.unity_dir):
            self.note_lost('runs.csv を開けません（KIOXIA がつながっていません）')
            return
        try:
            if not os.path.isdir(self.night_dir):
                os.makedirs(self.night_dir)
            new = not os.path.exists(p) or os.path.getsize(p) == 0
            if not new:
                with open(p, encoding='utf-8') as f:
                    first = f.readline()
                if first != self.RUNS_CSV_HEADER:
                    self.log('runs.csv のヘッダがコードと違うので書きません: %s' % p)
                    return
            self.runs_csv = open(p, 'a', encoding='utf-8')
            if new:
                self.runs_csv.write(self.RUNS_CSV_HEADER)
                self.runs_csv.flush()
        except Exception as e:
            self.log('runs.csv を開けません（%s）: %s' % (p, e))
            self.runs_csv = None

    def _runs_csv_row(self, r, status, code, wall):
        """1回ぶんを runs.csv に足す。失敗しても夜は止めない"""
        self._runs_csv_open()
        if self.runs_csv is None:
            return
        cond = parse_condition(r['item']['condition']) or {}
        ended = r['started'] + wall

        def iso(t):
            return dt.datetime.fromtimestamp(t).strftime('%Y-%m-%dT%H:%M:%S')

        try:
            # csv.writer で書く（2026-10-26・codex レビュー B2）。
            # 前は % で組んでいたので、名札や条件にカンマが入ると列がずれた
            w = csv.writer(self.runs_csv, lineterminator='\n')
            mono0 = r.get('started_mono')
            w.writerow([r['folder'], r['item']['condition'], r['item']['seed'],
                        norm_pass_by(r['item'].get('pass_by', 'off')), r['tryno'],
                        r.get('launch_running', 0), self.a.parallel,
                        iso(r['started']), iso(ended),
                        '%.3f' % r['started'], '%.3f' % ended,
                        '' if mono0 is None else '%.3f' % mono0,
                        '' if mono0 is None else '%.3f' % (mono0 + wall),
                        '%.3f' % wall,
                        status, '' if code is None else code])
            self.runs_csv.flush()
            os.fsync(self.runs_csv.fileno())
        except Exception as e:
            self.note_lost('runs.csv に書けませんでした: %s' % e)

    def _runs_csv_close(self):
        if self.runs_csv is not None:
            try:
                self.runs_csv.close()
            except Exception:
                pass
            self.runs_csv = None

    # ---- 1回ぶん ----
    def start_one(self, item, tryno):
        cond = parse_condition(item['condition'])
        if cond is None:
            self.log('飛ばします（条件の名前が読めません）: %s' % item['condition'])
            self.done.append((item['condition'], 'skipped', 0.0, tryno, item))
            return
        pass_by = norm_pass_by(item.get('pass_by', 'off'))
        folder = folder_name(self.a.night, self.a.label, self.token, cond, item['seed'],
                             pass_by, tryno)
        logfile = os.path.join(self.night_dir, 'logs', folder + '.log')
        if not self.a.dry_run:
            d = os.path.dirname(logfile)
            if not os.path.isdir(d):
                os.makedirs(d)

        # この回を起動したときに、実際に同時に走ることになる本数（この回を含む）。
        # -ssParallel は「決めた N」のまま、-ssRunning が「そのときの実際」。
        # 並びの最後の N-1 本は N より少なくなるので、
        # あとで「薄い回だったか」を台帳と run_info.json から見分けられるようにする
        # （2026-10-26。busycheck で同時本数が stuck_count・robot_share などを
        #   動かすと分かったため）
        running_now = len(self.running) + 1
        if self.a.dry_run:
            # から回しは実際に起動しないので self.running に積まれない。
            # 本番の起動時の本数を模して出す（1,2,…,N,N,…）。
            # ★ これは **起動したとき** の本数。並びの最後の方で
            #   同時本数が減るのは回している **途中** なので、この数には出ない（下の注）
            running_now = min(self.a.parallel, len(self.done) + 1)

        cmd = [self.a.build, '-batchmode', '-nographics', '-logFile', logfile,
               '-ssCondition', item['condition'], '-ssSeed', str(item['seed']),
               '-ssLabel', self.a.label, '-ssNight', self.a.night,
               '-ssParallel', str(self.a.parallel),
               '-ssRunning', str(running_now),
               # 2026-10-26（依頼 v13）：すれ違いの入切を回ごとに渡す
               '-ssPassBy', pass_by]
        if tryno > 1:
            cmd += ['-ssTry', str(tryno)]
        if self.a.dry_run:
            self.log('（から回し）%d 回め %s 種%s すれ違い%s 同時%d → %s' %
                     (len(self.done) + 1, item['condition'], item['seed'],
                      '入' if pass_by == 'on' else '切', running_now, folder))
            self.log('  %s' % ' '.join(cmd))
            self.done.append((folder, 'dry-run', 0.0, tryno, item))
            return

        proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        self.running.append({'proc': proc, 'item': item, 'folder': folder,
                             'started': time.time(), 'started_mono': time.monotonic(),
                             'tryno': tryno, 'logfile': logfile,
                             'launch_running': running_now})
        self.log('始めました（%d本目・同時 %d）: %s 種%s すれ違い%s → %s（pid %d）'
                 % (len(self.done) + len(self.running), running_now,
                    item['condition'], item['seed'],
                    '入' if pass_by == 'on' else '切', folder, proc.pid))

    def reap(self):
        """
        終わった回を片付ける。1つでも片付けたら True。

        2026-10-26（codex レビュー A10）：終わった回の **時刻を先に全部採ってから**、
        そのあとで台帳や runs.csv に書く。
        前は1本ずつ「見つける→台帳に書く（鍵を最大60秒待つ）」としていたので、
        2本目以降の終了時刻に、1本目の鍵待ちの時間が混ざっていた
        """
        found = []
        for r in list(self.running):
            code = r['proc'].poll()
            over = (time.time() - r['started']) > self.limit_s
            if code is None and not over:
                continue
            if code is None and over:
                self.log('上限 %.1f 分を超えたので止めます: %s（pid %d）'
                         % (self.a.limit_min, r['folder'], r['proc'].pid))
                self._kill(r['proc'])
                code = 2
            found.append((r, code, time.monotonic()))   # ← ここで採時する（単調時計）

        changed = False
        for r, code, ended_mono in found:
            wall = ended_mono - r.get('started_mono', ended_mono)
            # 終了コード 3 は「回は終わったが台帳に書けなかった」（依頼 v14 の 3）
            status = {0: 'completed', 2: 'stopped', 3: 'ledger_failed'}.get(code, 'failed')
            if code == 3:
                self.ledger_failed.append(r['folder'])
                self.log('★ 台帳に書けなかった回です（終了コード 3）: %s\n'
                         '  ログを見てください: %s' % (r['folder'], r['logfile']))

            # 時間切れで止める直前に、実行ファイルが completed の行を
            # 書き終えていることがある（2026-10-26・codex レビュー A5）。
            # そのときは台帳が正しいので、runs.csv もそれに合わせ、やり直しもしない。
            # 台帳と記録で status が食い違ったままにしない
            if status != 'completed' and volume_ok(self.a.volume, self.unity_dir):
                try:
                    if r['folder'] in L.completed_folders(self.unity_dir):
                        self.log('台帳には completed の行がもうあるので、'
                                 'この回は completed として扱います: %s' % r['folder'])
                        status = 'completed'
                        code = 0
                except (L.LedgerError, OSError) as e:
                    self.log('台帳を読めなかったので status はそのままにします: %s' % e)

            self.log('おわり: %s / %s / 終了コード %s / 実時間 %.1f 分'
                     % (r['folder'], status, code, wall / 60.0))
            # 1回ごとの開始・終了を残す（completed も含めて全部）
            self._runs_csv_row(r, status, code, wall)
            if status not in ('completed', 'ledger_failed'):
                self._write_row(r, status, wall)
            elif status == 'ledger_failed':
                # 実行ファイルが書けなかった所に実行係が書いても、たぶん同じ理由で書けない。
                # ここでは書かず、記録とまとめに出すだけにする（人が見て決める）
                self.log('  （実行係からも台帳には書きません。原因を直してから回し直してください）')
                base = r['folder']
                already = base in getattr(self, 'tried2', set())
                if r['tryno'] == 1 and not self.a.no_retry and not already:
                    self.retry.append(r['item'])
                    self.log('その夜のうちに1度やり直します: %s 種%s'
                             % (r['item']['condition'], r['item']['seed']))
                elif already:
                    self.log('やり直しはもう使っているので、やり直しません: %s' % base)
            self.running.remove(r)
            self.done.append((r['folder'], status, wall, r['tryno'], r['item']))
            changed = True
        return changed

    def _kill(self, proc):
        """
        止める。SIGTERM → 10 秒待つ → SIGKILL → **終わるまで待つ**。

        2026-10-26（codex レビュー A5）：前は SIGKILL のあと待っていなかったので、
        まだ生きている回が台帳に書く余地があった（二重書きの道）。
        止めるのに失敗したらログに出す（黙らない）
        """
        try:
            proc.send_signal(signal.SIGTERM)
        except Exception as e:
            self.log('SIGTERM を送れませんでした（pid %s）: %s' % (proc.pid, e))
        for _ in range(100):           # 10 秒待つ
            if proc.poll() is not None:
                return
            time.sleep(0.1)
        try:
            proc.kill()
        except Exception as e:
            self.log('SIGKILL を送れませんでした（pid %s）: %s' % (proc.pid, e))
        try:
            proc.wait(timeout=30)      # 終わるまで待つ（これが抜けていた）
        except Exception as e:
            self.log('★ 止めた回がまだ終わっていません（pid %s）: %s'
                     '\n  台帳に二重に書く恐れがあります。あとで確かめてください'
                     % (proc.pid, e))

    def _write_row(self, r, status, wall):
        """止まった・落ちた回の行を台帳に足す（実行ファイルが書けていないときだけ）"""
        if not volume_ok(self.a.volume, self.unity_dir):
            self.log('台帳に書けません（KIOXIA が外れています）: %s' % r['folder'])
            return
        cond = parse_condition(r['item']['condition']) or {}
        row = {
            'timestamp': dt.datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
            'run_folder': r['folder'],
            'status': status,
            'unity_cs': self.unity_cs,
            'store': cond.get('store', ''),
            'staff': cond.get('staff', ''),
            'robots': cond.get('robots', ''),
            'arrivals_per_h': cond.get('arrivals_per_h', ''),
            'mode': r['item'].get('mode', ''),
            'trays': r['item'].get('trays', ''),
            'fragile_by': r['item'].get('fragile_by', ''),
            'seed': r['item']['seed'],
            'wall_seconds': '%.4f' % wall,
            # 台帳の版（2026-10-26・codex レビュー D7）。
            # 前は空欄だったので、Python が書いた行だけ版が分からなかった
            'ledger_schema_version': str(L.SCHEMA_VERSION),
        }
        try:
            L.append_row(self.unity_dir, row, log=self.log)
        except L.LedgerError as e:
            self.log('台帳に書けませんでした: %s' % e)

    def _copy_queue(self, queue_path):
        """
        並べた回（queue.json）を夜のフォルダに写す（2026-10-26）。
        queue.json はプロジェクトの中で夜ごとに作り直す（VCS に入れない）ので、
        写しておかないと **その夜に何をどの順で並べたか** がどこにも残らない。
        出力のそばに置けば、あとで judge と突き合わせられる。
        もうあれば上書きしない（同じ夜を続きから回したときに、
        最初に並べたものを残すため）
        """
        if self.a.dry_run:
            return
        # KIOXIA が無いときに Mac の中へ同じ道のフォルダを作らない
        # （2026-10-26・codex レビュー D6）
        if not volume_ok(self.a.volume, self.unity_dir):
            self.log('並べた回を写せません（KIOXIA がつながっていません）')
            return
        dst = os.path.join(self.night_dir, 'queue.json')
        try:
            if not os.path.isdir(self.night_dir):
                os.makedirs(self.night_dir)
            if os.path.exists(dst):
                self.log('並べた回の写しはもうあります（上書きしません）: %s' % dst)
                return
            # 一時ファイルに写してから置く（2026-10-26・codex レビュー B5）。
            # 直に copy すると、途中で切れた写しが「もうある」と見なされる
            tmp = dst + '.tmp'
            shutil.copy2(queue_path, tmp)
            os.replace(tmp, dst)
            self.log('並べた回を写しました: %s' % dst)
        except Exception as e:
            self.log('並べた回を写せませんでした（%s）: %s' % (dst, e))

    # ---- 回す ----
    def run(self, queue):
        self._copy_queue(self.a.queue)
        # もう completed の回は飛ばす。
        # 2026-10-26（codex レビュー A6・C2）：前は tryno=1 の名前だけ見ていたので、
        # 「初回 failed・やり直し（_try2）completed」の回を再開すると、
        # もう一度 初回 を回し、そのあと _try2 まで回してしまう道があった。
        # **初回と _try2 の両方**を見て、どちらかが completed なら飛ばす。
        # また、_try2 の出力がもうある回は、やり直しをもう使ったものとして扱う
        skip = L.completed_folders(self.unity_dir)
        items, skipped = [], 0
        self.tried2 = set()
        for item in queue['runs']:
            cond = parse_condition(item['condition'])
            if cond is None:
                continue
            pb = norm_pass_by(item.get('pass_by', 'off'))
            f1 = folder_name(self.a.night, self.a.label, self.token, cond, item['seed'], pb, 1)
            f2 = folder_name(self.a.night, self.a.label, self.token, cond, item['seed'], pb, 2)
            if f1 in skip or f2 in skip:
                skipped += 1
                continue
            # やり直しの行がもう台帳にある（completed でなくても）なら、
            # その回のやり直しは使い切っている
            if f2 in L.all_folders(self.unity_dir):
                self.tried2.add(f1)
            items.append(item)
        self.log('並べた回 %d / もう completed の回を飛ばす %d / 回す %d'
                 % (len(queue['runs']), skipped, len(items)))

        queue_items = list(items)
        while queue_items or self.running or self.retry:
            # KIOXIA が外れたら、新しい回を始めない
            if not self.stopping and not volume_ok(self.a.volume, self.unity_dir):
                self.stopping = True
                self.log('KIOXIA が外れました。新しい回は始めません。'
                         '走っている %d 本が終わったら止まります' % len(self.running))

            while (not self.stopping and len(self.running) < self.a.parallel
                   and (queue_items or self.retry)):
                # 1本ごとに確かめる（2026-10-26・codex レビュー C3）。
                # 前は外側のループだけだったので、4本を立ち上げている途中に
                # 外れると、残りを始めてしまった
                if not volume_ok(self.a.volume, self.unity_dir):
                    self.stopping = True
                    self.log('KIOXIA が外れました（立ち上げの途中）。'
                             'これ以上は始めません。走っている %d 本が終わったら止まります'
                             % len(self.running))
                    break
                if queue_items:
                    self.start_one(queue_items.pop(0), 1)
                else:
                    self.start_one(self.retry.pop(0), 2)
                if self.a.stagger > 0 and not self.a.dry_run:
                    time.sleep(self.a.stagger)

            if not self.running:
                if self.stopping:
                    break
                if not queue_items and not self.retry:
                    break
                continue
            if not self.reap():
                time.sleep(self.a.poll)

        self._runs_csv_close()
        return not self.stopping

    def record_interrupted(self, why):
        """
        中断されたとき、まだ走っている回を runs.csv に残す
        （2026-10-26・codex レビュー A9）。
        Ctrl+C や SIGTERM では、前は子を止めるだけで記録が残らなかった。
        ★ SIGKILL・電源断は救えない。その場合も runner_log.txt の
          「始めました」の行に開始時刻が残るので、そこから追える
        """
        now = time.time()
        for r in list(self.running):
            wall = now - r['started']
            self._runs_csv_row(r, 'interrupted', None, wall)
            self.log('中断で記録しました（%s）: %s / 実時間 %.1f 分'
                     % (why, r['folder'], wall / 60.0))
        self._runs_csv_close()

    # ---- まとめ ----
    def summary(self):
        n = {}
        for _, status, _, _, _ in self.done:
            n[status] = n.get(status, 0) + 1
        self.log('--- まとめ ---')
        for k in sorted(n):
            self.log('  %s: %d 回' % (k, n[k]))
        walls = [w for _, s, w, _, _ in self.done if s == 'completed' and w > 0]
        if walls:
            self.log('  completed の実時間: 平均 %.1f 分 / 最長 %.1f 分'
                     % (sum(walls) / len(walls) / 60.0, max(walls) / 60.0))
        return n

    def call_judge(self):
        judge = os.path.join(HERE, 'judge_night.py')
        if not os.path.exists(judge):
            self.log('judge_night.py がないので判定は呼びません: %s' % judge)
            return None
        pattern = '%s_%s_*' % (self.a.night, self.a.label)
        out = os.path.join('night', self.a.night)
        cmd = [sys.executable, judge, '--runs', pattern, '--out', out]
        if self.a.baseline:
            cmd += ['--baseline', self.a.baseline]
        self.log('判定を呼びます（%s で）: %s' % (self.unity_dir, ' '.join(cmd)))
        if self.a.dry_run:
            self.log('（から回しなので呼びません）')
            return None
        r = subprocess.run(cmd, cwd=self.unity_dir, capture_output=True, text=True)
        if r.returncode != 0:
            self.log('判定が失敗しました（終了コード %d）: %s' % (r.returncode, r.stderr.strip()))
        else:
            self.log('判定を書きました: %s' % os.path.join(self.unity_dir, out, 'judge.md'))
        return r.returncode


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--queue', default=os.path.join(PROJECT, 'queue.json'))
    ap.add_argument('--parallel', type=int, default=2, help='同時に走らせる本数')
    ap.add_argument('--night', required=True, help='その夜の名前（例 20261026）')
    ap.add_argument('--label', default='night', help='出力フォルダに入る名札')
    ap.add_argument('--build', default=DEFAULT_BUILD, help='画面なしの実行ファイル')
    ap.add_argument('--volume', default=DEFAULT_VOLUME)
    ap.add_argument('--unity-dir', default=DEFAULT_UNITY_DIR)
    ap.add_argument('--limit-min', type=float, default=DEFAULT_LIMIT_MIN,
                    help='1回の上限（分）。超えたら止めて stopped にする')
    ap.add_argument('--baseline', default='', help='判定に渡す基準データの glob')
    ap.add_argument('--stagger', type=float, default=5.0,
                    help='同時に起こすときに少しずらす秒数（焼き直しがぶつからないように）')
    ap.add_argument('--poll', type=float, default=5.0, help='終わったかを見に行く間隔（秒）')
    ap.add_argument('--no-retry', action='store_true', help='止まった回をやり直さない')
    ap.add_argument('--no-caffeinate', action='store_true')
    ap.add_argument('--dry-run', action='store_true',
                    help='実行ファイルを起こさず、打つコマンドだけ見る')
    a = ap.parse_args()

    if a.parallel < 1:
        sys.exit('--parallel は 1 以上にしてください')
    if not re.match(r'^\d{8}$', a.night):
        sys.exit('--night は 20261026 の形にしてください')
    if not os.path.exists(a.queue):
        sys.exit('並べた回がありません: %s（先に make_queue.py）' % a.queue)

    # ★ 書くより先にマウントを確かめる（2026-10-26・codex レビュー D6）。
    # 前はここで夜フォルダを作ってログを開いていたので、
    # KIOXIA がつながっていないと **Mac の中に同じ道のフォルダを作って**
    # 書き始めてしまった（precheck はそのあと）
    if not volume_ok(a.volume, a.unity_dir):
        sys.exit('KIOXIA がつながっていません（%s / %s）。\n'
                 '  何も作らずに終わります（Mac の中には書きません）'
                 % (a.volume, a.unity_dir))

    night_dir = os.path.join(a.unity_dir, 'night', a.night)
    try:
        if not os.path.isdir(night_dir):
            os.makedirs(night_dir)
    except OSError as e:
        sys.exit('記録の置き場所を作れません: %s\n  %s' % (night_dir, e))
    log = Log(os.path.join(night_dir, 'runner_log.txt'))

    with open(a.queue, encoding='utf-8') as f:
        queue = json.load(f)

    log('=== 夜の実行係を始めます（night %s・同時 %d 本・上限 %.1f 分）==='
        % (a.night, a.parallel, a.limit_min))
    log('並べた回: %s（%d 回）' % (a.queue, len(queue.get('runs') or [])))

    r = Runner(a, log)
    if not r.precheck(queue):
        log('確かめで引っかかったので始めません')
        log.close()
        return 1

    caff = None
    if not a.no_caffeinate and not a.dry_run:
        try:
            caff = subprocess.Popen(['/usr/bin/caffeinate', '-dimsu'])
            log('スリープを止めました（caffeinate pid %d）' % caff.pid)
        except Exception as e:
            log('caffeinate を起こせませんでした（手でスリープを切ってください）: %s' % e)

    # SIGTERM（kill や再起動）でも、Ctrl+C と同じ道を通して記録を残す（A9）
    def on_term(signum, frame):
        raise KeyboardInterrupt()
    try:
        signal.signal(signal.SIGTERM, on_term)
        signal.signal(signal.SIGHUP, on_term)
    except Exception:
        pass

    code = 0
    try:
        finished = r.run(queue)
        n = r.summary()
        if getattr(r, 'ledger_failed', None):
            log('★ 台帳に書けなかった回が %d 件あります（終了コード 3）:' % len(r.ledger_failed))
            for f in r.ledger_failed:
                log('    %s' % f)
            log('  台帳の鍵が残っていないか・ヘッダが合っているかを見てください')
            code = 2
        if getattr(r, 'lost', 0):
            log('★ 記録（runs.csv）が %d 回ぶん書けませんでした。'
                '重なりの計算はその分だけ抜けます' % r.lost)
        if not finished:
            log('KIOXIA が外れたので、途中で止まりました')
            code = 2
        elif n.get('failed') or n.get('stopped'):
            code = 2
        if finished and not a.dry_run:
            r.call_judge()
    except KeyboardInterrupt:
        log('手で止められました。走っている回を止めます')
        for x in list(r.running):
            r._kill(x['proc'])
        r.record_interrupted('手で止めた')        # runs.csv に残す（A9）
        code = 2
    finally:
        if caff is not None:
            try:
                caff.terminate()
                log('スリープの設定を戻しました')
            except Exception:
                pass
        log('=== 夜の実行係を終わります（終了コード %d）===' % code)
        log.close()
    return code


if __name__ == '__main__':
    sys.exit(main())
```

### tools/ledger.py

```python
#!/usr/bin/env python3
"""ledger.py — 台帳 unity_ledger_v1.csv に鍵をかけて1行足す（依頼 v12 の 3 章）

夜は実行ファイルを何本も同時に走らせるので、台帳に同時に書かないよう鍵をかける。
ここは **台帳を壊すと戻しにくい所** なので、決まりを狭くしてある。

決まり（C# の ServiceSimLedger と同じ）
  ・**追記だけ**。前の行は読むだけで、書き換えない・消さない
  ・同じ run_folder の行は2度書かない
  ・列の並びは **実体の1行め（ヘッダ）が正**。
    C# 側が「ヘッダとコードの列が1字でも違えば追記しない」を守っているので、
    実体のヘッダをそのまま信じれば、C# と Python の列がずれることはない。
    そのうえで、頭の並びと ledger_schema_version があることだけ確かめる
  ・値にカンマ・引用符・改行は入れない（CSV を壊さないため）

鍵
  unity_ledger_v1.lock を O_EXCL で作れたら持ち主。閉じるときに消す。
  作れなければ少し待って何度か試す。
  C# から new FileStream(lock, FileMode.CreateNew) で取るのと同じやり方なので、
  Unity 側（依頼 v12 の 3 章）と同じ鍵を取り合える。

  中身は「札 pid ホスト名 unix秒」の1行。札はこの回だけの字。
  2026-10-26（依頼 v14 の 1）：**古い鍵を年だけ見て壊す処理はやめた**。
  2本が同時に「古い」と判定すると、先に取り直した側の新しい鍵を
  もう一方が消して二重所有になりうるため。いまは
    ・待ちの上限（既定 60 秒）を過ぎたら LedgerError（＝台帳に書けなかった）
    ・取れたあと、読み直して自分の札か確かめる
    ・返すときも、自分の札のときだけ消す
  待つ長さは単調時計（time.monotonic）で測る。
  鍵が残ったときは人が中身を見て消す（自動では消さない）。
"""
import csv
import errno
import os
import socket
import uuid
import time

LEDGER_NAME = 'unity_ledger_v1.csv'
LOCK_NAME = 'unity_ledger_v1.lock'

# ヘッダの頭の並び（ここだけは変わらない前提で確かめる）
HEADER_PREFIX = ('timestamp', 'run_folder', 'status', 'unity_cs',
                 'layout_generation', 'params_model')
# これが無ければ台帳と見なさない
HEADER_MUST_HAVE = ('seed', 'store', 'robots', 'ledger_schema_version')

# 台帳の列（59列）。C# の ServiceSimLedger.Columns と **同じ名前・同じ順番**。
# 2026-10-26（依頼 v14 の 3）：前は「頭の6列」と「末尾10列」だけ見ていたので、
# 途中の列の並びが変わった台帳を通してしまった。全部の名前と順番を照合する。
# **列を足すときは、ここと C# の両方を直す**（data_manifest_rules.md 10節）
COLUMNS = (
    'timestamp', 'run_folder', 'status', 'unity_cs', 'layout_generation', 'params_model',
    'store', 'staff', 'robots', 'arrivals_per_h', 'mode', 'trays', 'fragile_by', 'seed',
    'staff_first_yield', 'yield_order_enabled', 'one_robot_per_aisle',
    'robot_first_wait_s', 'everyone_left', 'total_minutes', 'wall_seconds',
    'entry_wait_mean', 'staff_wait_mean', 'dish_wait_mean', 'cold_share', 'robot_share',
    'staff_util', 'robot_util', 'walk_km_per_staff', 'site_entry_wait_mean',
    'site_staff_wait_mean', 'site_dish_wait_mean', 'site_robot_share', 'robot_speed_mps',
    'pause_rate', 'pause_s_mean', 'stuck_rate', 'trips', 'timeouts', 'stuck_count',
    'stop_s_total', 'corner_stop_time_share', 'gave_up', 'warped', 'table_freed',
    'rescues', 'late_staff_dishes', 'staff_yield_share_of_staff_time',
    'ledger_schema_version', 'pay_wait_mean', 'site_pay_wait_mean', 'blocked_wait_mean',
    'site_blocked_wait_mean', 'turn_mean', 'site_turn_mean', 'clear_wait_mean_s',
    'clear_wait_max_s', 'clear_wait_over_count', 'pay_job_wait_mean_s')

EXPECT_COL_COUNT = len(COLUMNS)

# 台帳の版（C# の ServiceSimLedger.SchemaVersion と同じ値）。
# Python が書く行（止めた回・落ちた回）にも入れる（codex レビュー D7）
SCHEMA_VERSION = 2

# 2026-10-26（依頼 v14 の 1）：古い鍵を年だけ見て壊す処理はやめた。
# 鍵が残ったときは待ちの上限で失敗にし、人が中身を見て消す


class LedgerError(Exception):
    pass


# ---------------- 鍵 ----------------

class Lock(object):
    """with Lock(dir) as lk: ... の形で使う"""

    def __init__(self, unity_dir, timeout=60.0, poll=0.25, log=None):
        self.path = os.path.join(unity_dir, LOCK_NAME)
        self.timeout = timeout
        self.poll = poll
        self._fd = None
        self._log = log
        # この回だけの札（C# の NewTag と同じ使い方）
        self._tag = 'p%d-%s' % (os.getpid(), uuid.uuid4().hex[:12])

    def _body(self):
        return '%s %d %s %f\n' % (self._tag, os.getpid(), socket.gethostname(), time.time())

    def _tag_of(self):
        """鍵の中身の先頭（札）。読めなければ None"""
        try:
            with open(self.path, encoding='utf-8') as f:
                line = f.readline()
        except OSError:
            return None
        if not line:
            return ''
        return line.split(' ', 1)[0].strip()

    def _note(self, msg):
        if self._log:
            self._log(msg)

    def __enter__(self):
        start = time.monotonic()           # 待つ長さは単調時計で測る
        while True:
            try:
                self._fd = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
                os.write(self._fd, self._body().encode('utf-8'))
                os.fsync(self._fd)
                if self._tag_of() == self._tag:    # 取れたあと読み直して確かめる
                    return self
                self._note('台帳の鍵を取り直します（取った直後に札が自分ではありません）: %s'
                           % self.path)
                try:
                    os.close(self._fd)
                except OSError:
                    pass
                self._fd = None
            except OSError as e:
                if e.errno != errno.EEXIST:
                    raise
            # ★ 古い鍵を **年だけ見て壊すことはしない**（依頼 v14 の 1）。
            #   2本が同時に「古い」と見ると、先に取り直した側の新しい鍵を
            #   もう一方が消して二重所有になりうる。
            #   鍵が残ったときは、上限を過ぎて LedgerError を投げ、
            #   呼ぶ側が「台帳に書けなかった回」として扱う（残った鍵は人が消す）
            if time.monotonic() - start >= self.timeout:
                raise LedgerError(
                    '台帳の鍵を %.0f 秒待っても取れません: %s\n'
                    '  ほかの回が書いている途中か、前の回の鍵が残っています。\n'
                    '  残っているなら、中身（札 pid ホスト名 時刻）を見てから'
                    '手で消してください（自動では消しません）'
                    % (self.timeout, self.path))
            time.sleep(self.poll)

    def __exit__(self, *exc):
        if self._fd is not None:
            try:
                os.close(self._fd)
            except OSError:
                pass
            self._fd = None
        # 自分の札のときだけ消す（A2）
        now = self._tag_of()
        if now is None:
            return False
        if now != self._tag:
            self._note('台帳の鍵が自分のものではないので消しません: %s（いま %s / 自分 %s）'
                       % (self.path, now, self._tag))
            return False
        try:
            os.unlink(self.path)
        except OSError:
            pass
        return False


# ---------------- 読む ----------------

def ledger_path(unity_dir):
    return os.path.join(unity_dir, LEDGER_NAME)


def read_header(unity_dir):
    """実体の1行めを列の一覧で返す。台帳の形でなければ LedgerError"""
    p = ledger_path(unity_dir)
    if not os.path.exists(p):
        raise LedgerError('台帳がありません: %s' % p)
    with open(p, encoding='utf-8') as f:
        line = f.readline()
    if not line:
        raise LedgerError('台帳が空です（ヘッダがありません）: %s' % p)
    cols = tuple(line.rstrip('\r\n').split(','))
    if cols == COLUMNS:
        return list(cols)

    # どこが違うかを出す（直すときに分かるように）
    if len(cols) != EXPECT_COL_COUNT:
        raise LedgerError('台帳の列の数が %d ではありません（%d 列）: %s\n  実体: %s'
                          % (EXPECT_COL_COUNT, len(cols), p, ','.join(cols)))
    diff = [(i, a, b) for i, (a, b) in enumerate(zip(cols, COLUMNS)) if a != b]
    raise LedgerError('台帳のヘッダがコードの列と違います（%d か所）: %s\n%s'
                      % (len(diff), p,
                         '\n'.join('  %d 列め: 実体 %s / コード %s' % (i + 1, a, b)
                                   for i, a, b in diff[:5])))


def read_rows(unity_dir):
    """台帳の行を run_folder → 行（dict）で返す。読むだけ"""
    p = ledger_path(unity_dir)
    out = {}
    if not os.path.exists(p):
        return out
    with open(p, encoding='utf-8') as f:
        for r in csv.DictReader(f):
            folder = (r.get('run_folder') or '').strip()
            if folder:
                out[folder] = r
    return out


def has_row(unity_dir, run_folder):
    return run_folder in read_rows(unity_dir)


def all_folders(unity_dir):
    """
    台帳にある run_folder をぜんぶ返す（status は見ない）。
    2026-10-26（codex レビュー A6）：やり直し（_try2）の行がもうあるかを
    調べるのに使う。failed のやり直しを2度使わないため
    """
    try:
        return set(read_rows(unity_dir).keys())
    except (LedgerError, OSError):
        return set()


def completed_folders(unity_dir, status='completed'):
    return {k for k, r in read_rows(unity_dir).items()
            if (r.get('status') or '').strip() == status}


# ---------------- 書く ----------------

def clean(value):
    """CSV を壊さない字にする（C# の ServiceSimLedger.Clean と同じ）"""
    if value is None:
        return ''
    s = str(value)
    s = s.replace(',', ' ').replace('"', ' ')
    s = s.replace('\r', ' ').replace('\n', ' ')
    return s.strip()


def append_row(unity_dir, row, log=None, timeout=60.0):
    """
    台帳に1行足す。足したら True、足さなかったら False（理由は log に出す）。
    row は 列名 → 値。ヘッダに無い列名が入っていたら LedgerError（打ち間違いを通さない）。
    鍵は呼ぶ側で取らず、ここで取る。
    """
    def note(msg):
        if log:
            log(msg)

    folder = clean(row.get('run_folder'))
    if not folder:
        raise LedgerError('run_folder が空なので足しません（照合キーです）')

    with Lock(unity_dir, timeout=timeout, log=log):
        cols = read_header(unity_dir)          # 鍵を取ってから読む
        unknown = [k for k in row if k not in cols]
        if unknown:
            raise LedgerError('台帳に無い列名です: %s' % ', '.join(sorted(unknown)))

        if folder in read_rows(unity_dir):
            note('もう台帳にあるので足しません（2度書かない）: %s' % folder)
            return False

        line = ','.join(clean(row.get(c)) for c in cols)
        if line.count(',') != len(cols) - 1:
            raise LedgerError('列の数が合いません（%d 列のはず）' % len(cols))

        p = ledger_path(unity_dir)
        # 末尾が改行で終わっていなければ **足さない**（2026-10-26・codex レビュー A4）。
        # 前は改行を1つ継ぎ足していたが、それでは
        # 「前の追記が途中で切れた」ことが台帳の中に黙って残る。
        # C# 側（ServiceSimLedger.EndsWithNewline）と同じ断り方にそろえた。
        # 自動で直さない（台帳は書き換えない決まり）
        with open(p, 'rb') as f:
            if f.seek(0, os.SEEK_END) > 0:
                f.seek(-1, os.SEEK_END)
                if f.read(1) != b'\n':
                    raise LedgerError(
                        '台帳の最後が改行で終わっていません（前の追記が途中で切れた疑い）: %s\n'
                        '  つなげて書くと列がずれるので、何も足しませんでした。\n'
                        '  最後の行を見て、エイイチに相談してから直してください'
                        '（自動では直しません）' % p)
        # 1行ぶんを UTF-8 の byte にしてから **1回で** 書く（依頼 v14 の 2）
        data = (line + '\n').encode('utf-8')
        with open(p, 'ab') as f:
            f.write(data)
            f.flush()
            os.fsync(f.fileno())
        note('台帳に1行足しました: %s / %s' % (folder, clean(row.get('status'))))
        return True
```

### tools/judge_night.py

```python
#!/usr/bin/env python3
"""judge_night.py — 夜に回した Unity の結果を自動で振り分ける（読むだけ。Unity も台帳も変えない）

振り分け（2026-10-25 エイイチ決定）
  回の失敗        : 最後まで回っていない・台帳に行がない・ロボットが1コマで0.05m超え動いた
  Unity の作り    : 立ち往生（ロボットが30秒以上止まった）などが、基準データと比べて外れた
  試算を見直す候補 : ロボットを増やしたときの向きが、種の過半数で試算と逆
                    または Unity ÷ 試算 の大きさが、基準データの同じ条件の範囲から外れた
  正常            : どれにも当たらない
  試算との差の大きさそのものは、記録するだけで印にしない（歩く距離の差などは既知のずれとして扱う）

決めた線
  STUCK_S = 30            立ち往生とみなす1回の止まりの長さ（Unity の『動けなくなった』60秒とは別の、判定だけの線）
  DEAD_BAND = 5% と 0.2分  これより小さい変化は「変わらない」として向きに数えない
  基準データ v3            最初の夜 20261027（ServiceSim/cs:21）の すれ違い「切」60回
                           （③⑦ × 0/1/2台 × 種0〜9）。幅は1条件10回の最小〜最大。
                           すれ違い「入」の10回と、cs:7・cs:144 の18回は使わない

使い方（service_sim/unity/ で）
  python3 judge_night.py --runs '20261026_night_*' --out night/20261026
  python3 judge_night.py --runs '20261027_night_cs21_*' --out /tmp/self   （基準どうしの確かめ）
標準ライブラリだけで動く。
"""
import argparse, csv, fnmatch, glob, json, os, statistics as st, sys, collections

STUCK_S = 30.0
DEAD_REL, DEAD_ABS = 0.05, 0.2
# 基準データ v4（2026-10-27 エイイチが決めた）
#   night 20261029・ServiceSim/cs:26 の **すれ違い「入」の60回**。
#   6条件（③⑦ × ロボット 0/1/2 台）× 種0〜9。
#   v3（night 20261027・cs:21・すれ違い「切」の60回）と比べて、
#   ③2台で向かい合って止まる最長がはっきり短く、それ以外の数字には
#   はっきりした差がなかったため、すれ違い「入」を基準にした。
#   v4 の回は全部 _passon が付くので、既定では何も外さない。
#   外したいときだけ --baseline-exclude で名前（glob）を渡す
#   （v3 を見るときは --baseline '20261027_night_cs21_*' --baseline-exclude '*_passon*'）
BASELINE_DEFAULT = '20261029_night_cs26_*'
BASELINE_EXCLUDE = ''
LEDGER = 'unity_ledger_v1.csv'
# pass_by は台帳の列にないので run_info.json から取る（無い回 = 切）。robots は必ず最後（向きの比べで使う）
COND_KEYS = ('store', 'staff', 'arrivals_per_h', 'mode', 'trays', 'pass_by', 'robots')
# 向きと大きさを見る数字（Unity の列, 試算の列, 表示名）
PAIRS = [('entry_wait_mean', 'site_entry_wait_mean', '入店待ち'),
         ('dish_wait_mean', 'site_dish_wait_mean', '料理の待ち'),
         ('staff_wait_mean', 'site_staff_wait_mean', '席は空いているのに待った')]
# Unity の作りを見る数字（1回ごと）。多いほど悪い
BUILD_METRICS = [('stuck30_per_trip', '30秒以上の止まり（1往復あたり）'),
                 ('robot_robot30', '30秒以上の止まりのうち、もう1台のロボットが原因'),
                 ('stuck_count', '動けなくなった（60秒）'),
                 ('table_freed', '卓を空けた（逃がし弁）'),
                 ('warped', '瞬間移動'),
                 ('timeouts', '打ち切り')]


def fnum(x):
    try:
        return float(x)
    except (TypeError, ValueError):
        return None


def load_ledger():
    rows = {}
    with open(LEDGER, encoding='utf-8') as f:
        for r in csv.DictReader(f):
            rows[r['run_folder']] = r   # 同じ出力フォルダは2度書かない決まりなので後勝ちでよい
    return rows


def run_record(folder, ledger):
    """1回分の数字をまとめる。足りないものは失敗の理由に入れる"""
    rec = {'run_folder': folder, 'fail': []}
    lr = ledger.get(folder)
    if lr is None:
        rec['fail'].append('台帳に行がない')
        return rec
    rec['ledger'] = lr
    rec['seed'] = lr.get('seed')
    pass_by = '切'
    if lr.get('status') != 'completed':
        rec['fail'].append(f"status={lr.get('status')}")
    if lr.get('everyone_left') not in ('1', '1.0', 'True', 'true'):
        rec['fail'].append('全員が帰っていない')
    ri_path = os.path.join(folder, 'run_info.json')
    if os.path.exists(ri_path):
        ri = json.load(open(ri_path, encoding='utf-8'))
        if (ri.get('pass_by') or {}).get('enabled'):
            pass_by = '入'
        if (ri.get('frames_over_005m') or 0) > 0:
            rec['fail'].append(f"ロボットが1コマで0.05m超え {ri['frames_over_005m']}コマ")
    else:
        rec['fail'].append('run_info.json がない')
    vals = dict(lr)
    vals['pass_by'] = pass_by
    rec['cond'] = tuple(vals.get(k, '') for k in COND_KEYS)
    # 止まり
    stops_path = os.path.join(folder, 'stops.csv')
    long_n = rr_n = 0
    if os.path.exists(stops_path):
        with open(stops_path, encoding='utf-8') as f:
            for s in csv.DictReader(f):
                d = fnum(s.get('duration_s')) or 0
                if d >= STUCK_S:
                    long_n += 1
                    if s.get('cause') == 'もう1台のロボット':
                        rr_n += 1
    trips = fnum(lr.get('trips')) or 0
    rec['m'] = {
        'stuck30': long_n,
        'stuck30_per_trip': (long_n / trips) if trips else 0.0,
        'robot_robot30': rr_n,
        'stuck_count': fnum(lr.get('stuck_count')) or 0,
        'table_freed': fnum(lr.get('table_freed')) or 0,
        'warped': fnum(lr.get('warped')) or 0,
        'timeouts': fnum(lr.get('timeouts')) or 0,
    }
    for u, s, _ in PAIRS:
        rec['m'][u] = fnum(lr.get(u))
        rec['m'][s] = fnum(lr.get(s))
    return rec


def collect(pattern, ledger, superseded=None):
    """出力フォルダがある回と、台帳にだけ行がある回（failed / stopped で
    フォルダが無いもの）の両方を拾う。やり直し（_try2）があるときは1つにまとめる：
    最後まで回った方を使い、どちらも失敗なら後の方を使う"""
    names = {p for p in glob.glob(pattern) if os.path.isdir(p)}
    names |= {k for k in ledger if fnmatch.fnmatch(k, pattern)}
    recs = [run_record(f, ledger) for f in sorted(names)]
    by_key = collections.OrderedDict()
    for r in recs:
        key = r['run_folder'][:-len('_try2')] if r['run_folder'].endswith('_try2') else r['run_folder']
        by_key.setdefault(key, []).append(r)
    out = []
    for key, rs in by_key.items():
        rs.sort(key=lambda r: r['run_folder'].endswith('_try2'))   # 元 → やり直し の順
        ok = [r for r in rs if not r['fail']]
        pick = ok[-1] if ok else rs[-1]
        out.append(pick)
        if superseded is not None:
            superseded.extend(r['run_folder'] for r in rs if r is not pick)
    return out


def by_cond(recs):
    g = collections.defaultdict(list)
    for r in recs:
        if 'cond' in r and not r['fail']:
            g[r['cond']].append(r)
    return g


def sign(a, b):
    """a→b の向き。小さい変化は 0"""
    if a is None or b is None:
        return None
    d = b - a
    if abs(d) < max(DEAD_ABS, DEAD_REL * max(abs(a), abs(b))):
        return 0
    return 1 if d > 0 else -1


def direction_flags(groups):
    """台数を1つ増やしたときの向きを、種ごとに試算と比べる。過半数が逆なら印"""
    out = []   # (条件の頭, 台数a→b, 数字, 逆の種の数, 比べられた種の数, 試算の向き)
    heads = collections.defaultdict(dict)
    for cond, rs in groups.items():
        heads[cond[:-1]][cond[-1]] = rs
    for head, byr in heads.items():
        counts = sorted(byr, key=lambda x: float(x))
        for a, b in zip(counts, counts[1:]):
            sa = {r['seed']: r for r in byr[a]}
            sb = {r['seed']: r for r in byr[b]}
            seeds = sorted(set(sa) & set(sb), key=lambda x: int(x))
            for u, s, name in PAIRS:
                site_a = [sa[k]['m'][s] for k in seeds if sa[k]['m'][s] is not None]
                site_b = [sb[k]['m'][s] for k in seeds if sb[k]['m'][s] is not None]
                if not site_a or not site_b:
                    continue
                site_dir = sign(st.mean(site_a), st.mean(site_b))
                if site_dir in (None, 0):
                    continue   # 試算が変わらないところは向きを比べない
                opp = n = 0
                for k in seeds:
                    d = sign(sa[k]['m'][u], sb[k]['m'][u])
                    if d is None:
                        continue
                    n += 1
                    if d == -site_dir:
                        opp += 1
                if n and opp * 2 > n:
                    out.append((head, f'{a}→{b}台', name, opp, n, '短くなる' if site_dir < 0 else '長くなる'))
    return out


def ratio_values(rs, u, s):
    v = []
    for r in rs:
        a, b = r['m'][u], r['m'][s]
        if a is not None and b:
            v.append(a / b)
    return v


def compare_to_base(groups, base_groups):
    """条件ごとに、種の中央値が基準の同じ条件の最小〜最大から外れたか"""
    build, site, nobase = [], [], []
    for cond, rs in groups.items():
        brs = base_groups.get(cond)
        if not brs:
            nobase.append(cond)   # 比べる相手がないので判定しない（Unity の作りにしない）
            continue
        for key, name in BUILD_METRICS:
            now = st.median(r['m'][key] for r in rs)
            bv = [r['m'][key] for r in brs]
            if now > max(bv) and now - max(bv) >= (0.01 if key == 'stuck30_per_trip' else 1):
                build.append((cond, name, round(now, 3), round(min(bv), 3), round(max(bv), 3)))
        for u, s, name in PAIRS:
            nv, bv = ratio_values(rs, u, s), ratio_values(brs, u, s)
            if not nv or not bv:
                continue
            now = st.median(nv)
            if now < min(bv) or now > max(bv):
                site.append((cond, f'{name}（Unity÷試算）', round(now, 2), round(min(bv), 2), round(max(bv), 2)))
    return build, site, nobase


def cond_label(c):
    d = dict(zip(COND_KEYS, c))
    pb = '・すれ違い入' if d.get('pass_by') == '入' else ''
    return f"{d['store']} スタッフ{d['staff']} ロボット{d['robots']}台{pb}"


def head_label(h):
    d = dict(zip(COND_KEYS[:-1], h))
    pb = '・すれ違い入' if d.get('pass_by') == '入' else ''
    return f"{d['store']} スタッフ{d['staff']}{pb}"


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--runs', required=True, help='振り分ける回の出力フォルダ（glob）')
    ap.add_argument('--baseline', default=BASELINE_DEFAULT, help='基準データの出力フォルダ（glob）')
    ap.add_argument('--baseline-exclude', default=BASELINE_EXCLUDE,
                    help='基準データから外す名前（glob）。既定は外さない')
    ap.add_argument('--out', required=True, help='judge.csv と judge.md を書くフォルダ')
    a = ap.parse_args()
    if not os.path.exists(LEDGER):
        sys.exit(f'{LEDGER} がない。service_sim/unity/ で動かす（KIOXIA がつながっているか確かめる）')
    ledger = load_ledger()
    superseded = []
    recs = collect(a.runs, ledger, superseded)
    base = [r for r in collect(a.baseline, ledger) if not r['fail']]
    if a.baseline_exclude:
        before = len(base)
        base = [r for r in base
                if not fnmatch.fnmatch(r['run_folder'], a.baseline_exclude)]
        if before != len(base):
            print(f'基準データから {before - len(base)} 回 外した'
                  f'（{a.baseline_exclude}）', file=sys.stderr)
    if not recs:
        sys.exit('振り分ける回が見つからない')
    if not base:
        sys.exit(f'基準データが見つからない（{a.baseline}）。--baseline を確かめる')
    groups, bgroups = by_cond(recs), by_cond(base)
    build, site_mag, nobase = compare_to_base(groups, bgroups)
    dirs = direction_flags(groups)
    known_dirs = {(d[0], d[1], d[2]) for d in direction_flags(bgroups)}

    # 1回ごとの判定（回の失敗だけは1回ごと。ほかは条件ごとの印を、その条件の回に付ける）
    build_conds = {b[0] for b in build}
    site_conds = {c[0] for c in site_mag}
    for d in dirs:
        if (d[0], d[1], d[2]) not in known_dirs:
            for cond in groups:
                if cond[:-1] == d[0]:
                    site_conds.add(cond)
    os.makedirs(a.out, exist_ok=True)
    counts = collections.Counter()
    with open(os.path.join(a.out, 'judge.csv'), 'w', encoding='utf-8', newline='') as f:
        w = csv.writer(f)
        w.writerow(['run_folder', 'store', 'staff', 'robots', 'seed', 'judge', 'reasons', 'stuck30', 'stuck30_per_trip', 'robot_robot30'])
        for r in recs:
            if r['fail']:
                j, why = '回の失敗', ' / '.join(r['fail'])
            elif r['cond'] in nobase:
                j, why = '基準なし', '基準データに同じ条件がない（判定していない）'
            elif r['cond'] in build_conds:
                j, why = 'Unity の作り', '条件ごとの印（judge.md）'
            elif r['cond'] in site_conds:
                j, why = '試算を見直す候補', '条件ごとの印（judge.md）'
            else:
                j, why = '正常', ''
            counts[j] += 1
            d = dict(zip(COND_KEYS, r.get('cond', ('',) * len(COND_KEYS))))
            m = r.get('m', {})
            w.writerow([r['run_folder'], d['store'], d['staff'], d['robots'], r.get('seed', ''), j, why,
                        m.get('stuck30', ''), round(m.get('stuck30_per_trip', 0), 4) if m else '', m.get('robot_robot30', '')])

    L = []
    L.append(f'# 夜の結果の振り分け（{a.runs}）\n')
    L.append(f'基準データ: {a.baseline}'
             + (f'（{a.baseline_exclude} を除く）' if a.baseline_exclude else '')
             + f'（{len(base)}回）／振り分けた回: {len(recs)}回\n')
    pm_now = sorted({r['ledger'].get('params_model', '') for r in recs if 'ledger' in r} - {''})
    pm_base = sorted({r['ledger'].get('params_model', '') for r in base if 'ledger' in r} - {''})
    if pm_now != pm_base:
        L.append(f'**注意：試算の版が基準と違う（今回 {", ".join(pm_now)}／基準 {", ".join(pm_base)}）。試算との比べは参考にとどめる**\n')
    L.append('| 振り分け | 回数 |\n|---|---|')
    for k in ('正常', '試算を見直す候補', 'Unity の作り', '回の失敗', '基準なし'):
        L.append(f'| {k} | {counts.get(k, 0)} |')
    L.append('\n## 回の失敗\n')
    fails = [r for r in recs if r['fail']]
    L += [f"- {r['run_folder']}: {' / '.join(r['fail'])}" for r in fails] or ['- なし']
    if superseded:
        L.append('\nやり直しで置き換えた回（判定に入れていない）: ' + ', '.join(superseded))
    if nobase:
        L.append('\n## 基準なし（基準データに同じ条件がないので判定していない）\n')
        L += [f'- {cond_label(c)}' for c in sorted(nobase)]
    L.append('\n## Unity の作り（種の中央値が、基準の同じ条件の最大を超えた）\n')
    L += [f'- {cond_label(c)}: {name} {now}（基準 {lo}〜{hi}）' if now is not None else f'- {cond_label(c)}: {name}'
          for c, name, now, lo, hi in build] or ['- なし']
    L.append('\n## 試算を見直す候補\n')
    L.append('### ロボットを増やしたときの向きが、種の過半数で試算と逆\n')
    if dirs:
        for h, step, name, opp, n, sd in dirs:
            tag = '基準 v2 でも同じ（既知）' if (h, step, name) in known_dirs else '新しく出た'
            L.append(f'- {head_label(h)} {step}: {name}（試算は{sd}）— 逆 {opp}/{n} 種 — {tag}')
    else:
        L.append('- なし')
    L.append('\n### Unity÷試算 の大きさが、基準の同じ条件の範囲から外れた\n')
    L += [f'- {cond_label(c)}: {name} {now}倍（基準 {lo}〜{hi}倍）' for c, name, now, lo, hi in site_mag] or ['- なし']
    L.append('\n## 記録（印にしない）：試算との差の大きさ（条件ごとの種の中央値・Unity÷試算）\n')
    L.append('| 条件 | 種の数 | ' + ' | '.join(p[2] for p in PAIRS) + ' |')
    L.append('|---|---|' + '---|' * len(PAIRS))
    for cond in sorted(groups):
        rs = groups[cond]
        cells = []
        for u, s, _ in PAIRS:
            v = ratio_values(rs, u, s)
            cells.append(f'{st.median(v):.2f}' if v else '-')
        L.append(f'| {cond_label(cond)} | {len(rs)} | ' + ' | '.join(cells) + ' |')
    # すれ違い 入 の条件は、同じ夜の 切 の同じ条件と並べる（記録。印にしない）
    pb_i = COND_KEYS.index('pass_by')
    pairs = [(c, c[:pb_i] + ('切',) + c[pb_i + 1:]) for c in groups if c[pb_i] == '入']
    if pairs:
        L.append('\n## すれ違い 入 と 切 の比べ（同じ夜・同じ条件・種の中央値。印にしない）\n')
        cols = [('entry_wait_mean', '入店待ち'), ('dish_wait_mean', '料理の待ち'), ('staff_wait_mean', '席は空いているのに待った'),
                ('stuck_count', '動けなくなった'), ('gave_up', 'あきらめた'), ('robot_share', 'ロボットが運んだ割合')]
        L.append('| 条件 | ' + ' | '.join(n for _, n in cols) + ' |')
        L.append('|---|' + '---|' * len(cols))
        for on, off in pairs:
            for c in (off, on):
                rs = groups.get(c, [])
                cells = []
                for k, _ in cols:
                    v = [fnum(r['ledger'].get(k)) for r in rs if fnum(r['ledger'].get(k)) is not None]
                    cells.append(f'{st.median(v):.2f}' if v else '-')
                L.append(f'| {cond_label(c)}（{len(rs)}回） | ' + ' | '.join(cells) + ' |')
    L.append(f'\n線: 立ち往生 {STUCK_S:.0f}秒以上の止まり／向きは {int(DEAD_REL*100)}%・{DEAD_ABS}分より小さい変化を数えない／外れは基準の同じ条件の最小〜最大')
    open(os.path.join(a.out, 'judge.md'), 'w', encoding='utf-8').write('\n'.join(L) + '\n')
    print('\n'.join(L))


if __name__ == '__main__':
    main()
```

