====================================================================
依頼 v12 の 1〜3 章：画面なしの実行ファイル・1回の回し方・台帳の鍵
2026-10-26 13:00　check-in：ServiceSim/cs:13・cs:14（保留 0）
====================================================================

結論

  ・1〜3 章を作りました。組み立ては通っています（59 本・エラー 0）。
  ・Unity 抜きで確かめられる所はぜんぶ確かめました（下の 5 章）。
    いちばん怖い2つ（**台帳の鍵**と**フォルダ名の食い違い**）は、
    実際に動かして確かめてあります。
      鍵 … 8本同時に追記して 8 行・59 列・ヘッダ無傷。
            C# と Python が同じ鍵を待ち合えることも両方向で確認
      名前 … C# と night_runner.py が作る名前を 8 通り突き合わせて1字も違わない
  ・**まだ実行ファイルは作っていません**。いま Unity が ServiceSim を開いているので、
    私からはビルドを打てません（プロジェクトが押さえられています）。
    5 章 A〜C は、エイイチが 6 章の順で押してからになります。
  ・check-in は2つに分かれました（cm が足した分と直した分を分けたため）。
    **cs:13**（新しい3本と台帳の移動）と **cs:14**（直した5本）。中身は一組です。


====================================================================
1. 作った・直したファイル
====================================================================

  新しく作った（3本）

    Assets/ServiceSim/Editor/BuildHeadless.cs       1 章（実行ファイルを作る）
    Assets/ServiceSim/Experiment/HeadlessRun.cs     2 章（1回を回して終わる）
    Assets/ServiceSim/Experiment/LedgerRow.cs       台帳の行を作る所（下の 4 章）

  場所を移した（1本・中身は変えていない）

    Assets/ServiceSim/Editor/ServiceSimLedger.cs
      → Assets/ServiceSim/Experiment/ServiceSimLedger.cs

  直した（5本）

    Experiment/ServiceSimLedger.cs   鍵を付けた（3 章）。名前空間を ServiceSim に
    Experiment/ExperimentRunner.cs   run_info の4項目・上限の既定・画面のものを切る
    Experiment/TripLogger.cs         run_info.json に runner・build_id・night・parallel
    Editor/ServiceSimStageB.cs       行は LedgerRow に任せる。ReadChangeset を公開
    Editor/ServiceSimSetup.cs        シーンに HeadlessRun を置く。店の形の場所の const

  店の回し方・ロボットと人の動き・台帳の列（59・版 2）は変えていません。


====================================================================
2. 1 章：画面なしの実行ファイルを作る
====================================================================

  ２通りの作り方（どちらも同じ関数を通ります）

    メニュー  ServiceSim → 画面なしの実行ファイルを作る
    コマンド  /Applications/Unity/Hub/Editor/2022.3.62f3/Unity.app/Contents/MacOS/Unity \
                -batchmode -quit -nographics \
                -projectPath /Users/shimazueiichi/Robotics_UX/ServiceSim \
                -executeMethod ServiceSim.EditorTools.BuildHeadless.Build \
                -logFile /dev/stdout

  決めたこと

    ・**普通の macOS の player** にしました（Dedicated Server ではない）。
      理由：-batchmode -nographics で画面を作らずに回せるので足りる。
            Dedicated Server は別のモジュールを入れる手間が増える。
            いまは「画面なしでも同じ数字が出るか」を確かめる段（5 章 B）なので、
            段階Bと同じ player のまま回すほうが、違いの元が増えません。
    ・**Apple Silicon（ARM64）** 向けにします。Rosetta で動くと遅くなり、
      何本同時に回せるか（5 章 C）の実測が変わってしまうためです。
      この設定は PlaybackEngines の DLL の中にあり、Unity 抜きの組み立てでは
      参照できないので反射で入れています。入らなければ既定のまま作り、
      build_info.json の architecture にそう書きます（黙って変えない）。
    ・シーンは **コードの中で直接指定**します（opt.scenes に
      Assets/ServiceSim/Scenes/ServiceSim_Family.unity の1本だけ）。
      ビルド設定（EditorBuildSettings）は読みも書きもしません。
    ・前の ServiceSim.app は消して作り直します
      （古い実行ファイルで夜を回してしまわないため）。

  出力

    Builds/headless/ServiceSim.app
    Builds/headless/build_info.json

    Builds は ignore.conf に前から入っているので、VCS には入りません。

  build_info.json（14 項目。区切りの形も確かめ済み）

    schema / build_id / unity_cs / pending_changes / built_at /
    editor_version / target / architecture / scene / app / exe /
    ledger_schema_version / total_size_bytes / build_seconds

    build_id は 版＋作った時刻（例 cs14_20261026_1310）。
    unity_cs と build_id は、夜の実行係（night_runner.py の read_unity_cs）と
    実行ファイルの中（HeadlessRun）の両方が読みます。

  ★ 保留があるまま作ると、警告を出して build_info.json の pending_changes に残します
    （止めはしません）。夜に使う実行ファイルは、**保留 0 で作り直してください**。
    保留があるまま作ると、台帳の unity_cs が「cs:14」なのに中身が違う、
    という取り違えが起きます。

  版の取り方は段階Bと同じ ServiceSimStageB.ReadChangeset です。
  2通りに分けると、台帳の unity_cs と build_info.json の unity_cs が
  食い違うので、入り口を1つにしました。


====================================================================
3. 2 章：実行ファイルの1回の回し方
====================================================================

  引数（night_runner.py が渡す形。2 章のとおり）

    -ssCondition fam_p1_s4_r1_a42   条件（必須）
    -ssSeed 3                        種（必須）
    -ssLabel night                   名札（既定 night）
    -ssNight 20261026                その夜の名前。フォルダ名の先頭にもなる
    -ssParallel 3                    同時に走っていた本数（記録だけ）
    -ssPassBy on|off                 すれ違い（既定 off。依頼 v13）
    -ssTry 2                          やり直しの回。2 以上で _try2

  ★ **-ssCondition が無ければ、この部品は何もしません。**
    エディタで手で Play したときも、段階Bの Run All のときも必ずこちらなので、
    いまの回し方には1つも効きません（cs:12 と同じ動き）。

  回し方は段階Bと同じ道

    ExperimentRunner.Job を1つ作って SetSingleJob する、という
    段階Bの StartNext とまったく同じ形にしました。
      siteRun = true / conditionIndex / storeKey / robots / seed /
      aisleWidthIndex / crowd = Normal / applyDeliveryOffset = false
      timeMode = FixedStep / captureDeltaTimeOverride = 0（1コマ 0.05 秒のまま）
      maxWallSecondsPerRun = 40 分（段階Bと同じ値。置き場所を1つにしました）

  出力フォルダ

    {夜}_{名札}_cs{N}_{店}_r{台数}_s{種}[_passon][_try{n}]
    例 20261026_night_cs14_fam_p1_r1_s3

    ★ 名前を決めるのは **実行ファイル側**です。実行係は同じ式で先に名前を作って
      「もう回した回」を飛ばすので、1字でも違うと同じ回を2度回したり、
      回したのに飛ばしたりします。
      なので C# と Python の両方に同じ式を書いたうえで、
      **8 通りを突き合わせて1字も違わないことを確かめました**（5 章）。
      読めない版は両方とも csunknown になります（ここも一致）。

  KIOXIA（書く前に確かめる）

    回し始める前に、ボリュームと置き場所の両方を確かめます。
    どちらか無ければ **何も書かずに終了コード 1** で終わります（Mac の中に書かない）。
    出力フォルダがもうあるときも、上書きせずに 1 で終わります
    （やり直しは -ssTry 2 で別の名前にしてください）。
    台帳のヘッダがコードの列と違うときも、**回す前に** 1 で止めます
    （12 分回してから書けないと分かるのは無駄なので）。

  終わり方

    0 … 最後まで回った（status completed）
    2 … 途中で止まった（status stopped。実時間 40 分の上限など。記録は残る）
    1 … 失敗（引数が読めない・条件が無い・KIOXIA が無い・記録が出ない）

  画面のもの（2 章）

    ・キッチンの屋根のモニター … 画面なしでは出しません。
      そもそも吹き出しを描くのは Editor だけの ServiceSimLabels なので、
      player では文を作っても誰も見ません。
      **文を作るか作らないかは、店の回し方・数字には1つも効きません**
      （KitchenMonitor は文字列を作るだけで、店に書き込みません）。
    ・カメラ … 画面なしでは切ります（-nographics では元から何も描かれませんが、
      はっきり切っておきます）。
    ・録画・字幕・スクリーンショット … ServiceSim にはそういうコードがありません
      （grep で確認。ScreenCapture も Recorder も1件も無い）。
    ・SimHud（依頼 v13 の 依頼2）… 画面なしでは描かず、1倍にもなりません。

  run_info.json に足したもの

    runner      "editor"（段階B・Site Run）/ "headless"（画面なしの実行ファイル）
    build_id    実行ファイルの版
    night       その夜の名前
    parallel    同時に走っていた本数
    （seed は前からあります）

    エディタの回は runner が "editor" になるだけで、前と同じ値です。


====================================================================
4. 3 章：台帳に同時に書かない（鍵）＋ 行を作る所を1つに
====================================================================

  困った所と、どう解いたか

    player（実行ファイル）には **Editor/ の中のコードが入りません**。
    ところが台帳に書くコード（ServiceSimLedger）と行を作るコード
    （段階Bの WriteLedgerRow）は、両方 Editor/ の中にありました。
    そのままでは、夜の回は「別のコード」で台帳に書くしかなく、
    同じ回でも書く値が違う、という最悪の壊れ方をします。

    なので2つ動かしました。

      ① ServiceSimLedger.cs を Editor/ → Experiment/ に移した
         このクラスは UnityEditor の API を **1つも使っていなかった**ので、
         移すだけで player からも呼べます。中身（列・追記ガード・
         2度書かない決まり）は移すときに1字も変えていません。
         名前空間も ServiceSim にしました。呼んでいたのは ServiceSimStageB
         だけで、そちらは ServiceSim.EditorTools（= ServiceSim の内側）に
         あるので、**呼ぶ側の字は1つも変わりません**。

      ② 段階Bの WriteLedgerRow を Experiment/LedgerRow.cs に移した
         Editor でしか取れない値（.asset・EditorPrefs）は、呼ぶ側が
         LedgerRow.Inputs に詰めて渡す形にしました。
           Editor 側 … ServiceSimStageB が AssetDatabase と EditorPrefs から詰める
           player 側 … HeadlessRun がシーンの参照と build_info.json から詰める

         ★ row[...] の中身は cs:12 と **1行も変えていません**。
           差分を取って確かめました。変わったのは次の5か所だけです。
             署名（Inputs を受ける）／sp・cond・seed・folder・runDir・wallS の出どころ／
             RobotSpec・PeopleSpec の出どころ／unity_cs の出どころ／台帳の場所
           59 列ぜんぶの row[...] = … は字も並びもそのままです。

  鍵（依頼 v12 の 3 章）

    鍵ファイル   {台帳と同じフォルダ}/unity_ledger_v1.lock
    取り方       FileMode.CreateNew で作れたら持ち主（Python の O_EXCL と同じ）
    中身         "pid ホスト名 unix秒" の1行（Python 側と同じ形）
    待ち         0.25 秒ごとに試し、上限 60 秒。取れなければ書かずにあきらめる
    古い鍵       120 秒より古ければ、持ち主が落ちたと見て1度だけ壊す
                 （tools/ledger.py の LOCK_STALE_SEC と同じ値）
    返し方       finally で必ず閉じて消す

    ★ 大事な所：**ヘッダの確かめと「もうあるか」の確かめも、鍵の中でやります。**
      鍵の外でやると、2本が同時に「まだ無い」と見てから両方書いてしまいます。
      書く字（59 列）は鍵を取る前に作って、鍵を持っている時間を短くしています。

    ★ 書いたあと fs.Flush(true) でディスクまで落とします。
      夜に Mac が落ちても、書いた行が消えないようにするためです。

    今までのガードはそのままです。
      ・実体のヘッダとコードの列が1字でも違えば追記しない
      ・同じ run_folder の行は2度書かない
      ・追記だけ。前の行は読むだけ
      ・unity_cs は "ServiceSim/cs:N" の形

    鍵のコードはエンジンの要る API を使いません（SystemInfo を使うのをやめて
    Environment.MachineName にしました）。player でも Editor でも同じに動きます。


====================================================================
5. 確かめた結果（Unity 抜きでできる分。ぜんぶ OK）
====================================================================

  組み立て　59 本・エラー 0（同梱の csc。Editor の中も含む）

  ① 台帳の鍵を実際に動かした（12 件）

      同時に 8 本足して 8 本とも書けた          8 / 8
      行数はヘッダ＋8 行                        9 行
      ヘッダが壊れていない                      一致
      どの行も 59 列（行が混ざっていない）      OK
      8 本ぶんの run_folder が重複なく入った    8 種
      書き終わったら鍵ファイルは消えている      OK
      同じ run_folder は2度書かない             断った
      断られたあとも 8 行のまま／鍵も残らない   OK
      ヘッダが1字違えば足さない（ガード生存）   断った
      そのときも鍵は残らない                    OK
      run_folder が空なら足さない               断った

  ② C# と Python が同じ鍵を待ち合えるか（両方向）

      C# が 3 秒持っている間 → Python から鍵が見える／2.5 秒待って取れた
      Python が 3 秒持っている間 → C# が 2.1 秒待って取れた
      どちらも、返したあと鍵ファイルは消えている

      ★ これは夜にいちばん効く確かめです。実行ファイル（C#）が completed の行を
        書き、実行係（Python）が stopped / failed の行を書くので、
        同じ鍵を取り合えないと行が混ざります。

  ③ 出力フォルダの名前が実行係と一致するか（8 通り・1字も違わない）

      20261026_night_cs12_fam_p1_r1_s3
      20261026_night_cs12_fam_p1_r2_s0_passon
      20261026_night_cs12_fam_p5_r0_s9_try2
      20261026_night_cs12_fam_p5_r2_s7_passon_try3
      20261101_stageB_cs7_fam_p1_r0_s0
      20261026_night_csunknown_fam_p1_r1_s1     ← 版が読めないときも一致
      20261026_night_cs144_fam_p5_r2_s5
      20261026_night_cs3_fam_p1_r1_s2_passon

  ④ 引数の読み方（10 件）

      night_runner.py が実際に組む引数列をそのまま読ませて、
      7 つの引数を正しく取れること、無い引数は null になること、
      Unity 自身の引数（-batchmode など）を値と取り違えないこと、
      その引数列から作った名前が実行係の名前と同じになることを確かめました。

  ⑤ 筋の確かめ（45 件・コードを読んで）

      1 章 … メニューとコマンドの両方／シーンを直接指定／ビルド設定を書き換えない／
             出力の場所／StandaloneOSX／build_info の項目／版の取り方は1つ／
             保留の警告と記録／失敗したら終了コード 1／前の版を消す／ignore.conf
      2 章 … 引数 7 つ／-ssCondition が無ければ何もしない／段階Bと同じ Job ／
             1コマ 0.05 秒のまま／上限の値は1か所／KIOXIA を書く前に確かめる／
             無ければ何も書かない／フォルダがあれば上書きしない／
             台帳が書けない形なら回す前に止める／終了コード 0・2・1 の割り当て／
             Application.Quit で終了コードを付ける／run_info の4項目／
             runner の既定は editor ／画面なしでは屋根のモニターとカメラを切る／
             すれ違いの入切を引数で入れる
      3 章 … 鍵の名前が Python と同じ／FileMode.CreateNew ／120 秒で壊す／
             確かめは鍵の中／finally で必ず返す／追記ガード生存／
             ディスクまで落とす／エンジンの要る API を使わない
      ほか … 行を作るのは LedgerRow 1つだけ／段階Bも画面なしも同じ所を呼ぶ／
             台帳の列は 59・版 2 のまま／unity_cs は ServiceSim/ の形／
             Setup が HeadlessRun を置く／店の形の場所は1か所／
             build_info.json の区切り（最後だけカンマ無し・14 項目）

  ⑥ まだ確かめていない（Unity が要る。5 章 A〜C）

      ・実行ファイルが実際に作れるか（BuildPipeline が通るか）
      ・画面なしで1回が最後まで回り、段階Bと同じファイルが全部出るか
      ・2本同時で台帳に2行がきちんと入るか
      ・画面なしでも同じ数字が出るか（5 章 B）
      ・何本同時に回せるか（5 章 C）


====================================================================
6. 押す順番（エイイチへ）
====================================================================

  ★ 先に1回だけ必要なこと

    cs:13・cs:14 で、シーンに置く部品が2つ増えました（SimHud と HeadlessRun）。
    シーンに入れるのは Setup なので、**1回だけ Setup を押してください**。

    1. Unity に切り替える（スクリプトが読み込み直され、新しい .cs の .meta が
       3〜4 件 保留に出ます。これは私が次に check-in します）
    2. ServiceSim → Setup（アセット・prefab・シーンを作る）
       → Hierarchy の ServiceSim の下に SimHud と HeadlessRun が増えます
       → シーンが保留に出ます（これも私が check-in します）
    3. 声をかけてください。.meta とシーンを check-in して保留 0 にします

  ★ そのあと（5 章 A の前半。ビルド）

    4. ServiceSim → 画面なしの実行ファイルを作る
       → Builds/headless/ServiceSim.app と build_info.json が出ます
       → Console に「版: ServiceSim/cs:15（保留 0）」と出ることを確かめてください
         （保留があると警告が出ます。そのときは3に戻ってください）
    5. 声をかけてください。build_info.json を読んで、
       そのあと私が短い1回と2本同時を走らせます（5 章 A の後半）。

  ★ 1つ決めてほしいこと（5 章 A の後半の前に）

    5 章 A の「2本同時に短い回を走らせて、台帳に2行がきちんと入る」を
    実際にやると、**本物の台帳に確かめ用の行が2〜3行 残ります**。
    台帳は追記だけで消さない決まりなので、あとで取り消せません。

    ・名札を acheck にすれば、夜の回や段階Bとは混ざりません
      （フォルダ名が 20261026_acheck_cs15_… になり、判定の対象にも入りません）
    ・それでも台帳の行としては残ります

    どちらにしますか。
      (ア) 本物の台帳に acheck の行を残して確かめる（依頼どおり）
      (イ) KIOXIA の別の場所に台帳のコピーを置いて、そこで確かめる
           （ExperimentPlan の置き場所を一時的に変える必要があるので、
             確かめたあと必ず元に戻します）
    私は (ア) でよいと思います。名札で分けられるうえ、
    **本物の台帳に本当に書けるか**を確かめるのが 5 章 A の目的だからです。
    ただ、台帳はエイイチが「消さない・書き換えない」と決めた所なので、
    勝手には進めません。


====================================================================
7. 困る所・気をつけること
====================================================================

  ① 出力フォルダの cs の字は **cs{N}** で進めました

    依頼 v12 の 2 章の例は `20261026_night_ss12_fam_p1_r1_s3`（ss12）ですが、
    いま動いている段階Bと夜の実行係・判定はすべて `cs{N}` で揃っています。
    `ss` にすると、判定の glob と基準データの名前も直す必要があります。
    **cs のまま**で進めました。`ss` にしたいときは言ってください
    （night_runner.py の --cs-token でその場でも変えられます）。

  ② player で -batchmode / -nographics がどう効くかは、実物で確かめます

    Application.isBatchMode と graphicsDeviceType を見て
    「画面のものを切る」「1倍にしない」を決めています。
    macOS の player でこの2つがきちんと立つかは、5 章 A の1回で確かめます。
    ★ 立たなかった場合でも壊れません。屋根のモニターの文が作られるだけで
      （数字には効かない）、SimHud は表示の初期値が切なので何も出ません。
      結果が変わる所はありません。

  ③ 実行ファイルは「作ったときのシーンの中身」で固まります

    シーンに置いた部品（HeadlessRun の参照・店の形の2つ）は、
    ビルドしたときの状態で焼き付きます。
    .asset（RobotSpec・ExperimentPlan など）も同じです。
    **.asset を直したら、実行ファイルを作り直してください。**
    作り直さないと、台帳の unity_cs は新しいのに中身が古い、という
    取り違えが起きます（build_info.json の built_at で気づけます）。

  ④ 1回の実時間の上限は 40 分（段階Bと同じ）

    実行係側の上限は 90 分（night_runner.py の --limit-min）です。
    中（40 分）が先に効いて status = stopped・終了コード 2 になります。
    実行係の 90 分は「中の上限も効かずに固まった」ときの最後の備えです。
    5 章 C の実測で 1回 12 分前後なら、このままでよいと思います。

  ⑤ check-in が2つに分かれました

    cs:13 … 新しい3本（BuildHeadless・HeadlessRun・LedgerRow）と台帳の移動
    cs:14 … 直した5本
    cm が「足した分」と「直した分」を分けたためで、中身は一組です。
    夜に使う実行ファイルは cs:14 以降（Setup のシーンを入れたら cs:15）で
    作ってください。

  ⑥ 残っている既知のもの（前の報告のまま）

    ・受け渡し口の点が1つしかないため、すれ違い「切」でも2台が向かい合う件
      （559 秒・体が重なる）
    ・来客の締め切りのずれ 9.3 秒／記録の中に時計が2つある件
    ・plan_ms_max が実時間であること
    ・判定は出力フォルダがある回だけ見る／基準が空だと全部に印が付く
    ・_try2 と元の回の両方が判定に入る


====================================================================
8. 次（順に）
====================================================================

  1. エイイチ：6 章の 1〜3（Setup を押す）→ 私が .meta とシーンを check-in
  2. エイイチ：6 章の 4（実行ファイルを作る）
  3. 私：5 章 A の後半（短い1回・2本同時）。6 章の「決めてほしいこと」の返事待ち
  4. エイイチ：5 章 B（③1台・種1 を画面なしで1回）→ 私が cs:7 の同じ回と並べる
  5. 私とエイイチ：5 章 C（1本・2本・3本の実測）→ N を決める
  6. 私：5 章 D の codex レビューの依頼文を作る（台帳に書く所。
     3 章の鍵・LedgerRow・night_runner の stopped/failed の行）
  7. 最初の夜 70 回（台帳を archive/ に写してから）

  check-in：**cs:13・cs:14**。保留 0。
