**現状では、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回へ進むのが妥当です。
