====================================================================
依頼 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。
