P28-01: Fix Ops ON input, startup order, render counts and proof roles in spec

Representative input (602 min, 200 guests) cannot start Operations
(Day1 06:00 only, population cap 100), so Ops ON uses the real new-game
opening and 200-guest Ops ON stays unmet. Pin the autoload/main startup
order, define render counts from the world_preview model, separate input
shortfall from natural change, split checker/static review/owned-run proof,
and make the collector self-contained (tracked run_all_tests.py and
owned_outer.py only). Budgets and original denominator unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Takeshi
2026-10-20 20:13:56 +09:00
co-authored by Claude Opus 5.5
parent 91676c6e16
commit 8fe128c5a8
2 changed files with 136 additions and 46 deletions
@@ -54,60 +54,145 @@
- 通常 player の保全
- 全 raw stdout と SHA の結合
- 上の artifacts/ は gitignore 対象のため、Root が worker へ build/ 配下の読取専用として渡します。
- **collector の依存は自己完結させる。** r02 の `collect_perf.py` は、gitignore 対象の `windows-current1297-tomari-release-adapter-prep-r01/windows_adapter.py` を動的に import しています(:153)。そのadapterは、さらに `legacy/windows_adapter.py` と `support/owned_outer.py` を読み込みます。新しい collector はこの外部パスに依存しません。
- import してよいのは、Git で追跡している次の2つだけで、どちらも変更しません。
- `tools/run_all_tests.py`: `source_refs`、`default_player`、`private_project`、`copy_source`、`profile_path`
- `tools/regression/owned_outer.py`: `run_phase`、`finish_captured`。SHA `c2e3483b…66e7` で、adapter の support 版とバイト一致し、内部で `owned_diagnostic_process.py` を読み込みます
- それ以外の補助処理は、allowed_paths の中に実装します。対象は、private import 用の project、`run_engine` 相当の owned phase の実行(上限は1800 s で、adapter の固定値1200 s ではない)、`profile_files`、`no_live_engine`、厳密な診断です。
- adapter の2ファイルは仕様の参照にだけ使い、import しません。読取専用入力の SHA は REGISTRATION_PROPOSAL.json に記載します。
## 4. 不足を埋める測定(この対象の新規分)
## 4. 入力の選択と前提(API監査の結果を反映)
windowed(非headless)の main scene で、1回の collector 実行の中に次の4 phase を置きます。
### 4.1 Operations ON は代表入力では開始できない(事実)
| phase | Operations | 速度 |
- `SimulationRuntime.enable_production_operations()`(`autoload/simulation_runtime.gd:1534-1544`)は、Day 1 06:00、つまり `absolute_minute == OPERATIONS_START_MINUTE`(360、:49)のときだけ開始できます。
- 代表入力は602分(Day 1 10:02)に保存されているため、`INVALID_START_TIME` になります。
- clock だけを巻き戻すことは禁止です。
- Operations ON の人口上限は100人です(`scripts/operations/operations_coordinator.gd:85`、:203、:333)。
- 開始時には `set_population_count(guest_count)`(simulation_runtime.gd:1579)が上限を照合するため、200客では `INITIAL_POPULATION_INVALID` になります。
- 正規契約の上での200客 Operations ON の状態は存在しません(PERFORMANCE_AUDIT.md「100客上限と200客診断」)。
- 既存の成果物の中に、代表規模の Operations ON の保存はありません。代表入力の build 記録は、`operations_enabled_mixed_load_accepted: false` です。
### 4.2 選択した入力(製品変更なし、fixtureの全生成なし)
| 入力 | 中身 | Operations の実状態 | 使い道 |
|---|---|---|---|
| **R:代表入力** | 上の reference.variant(SHA `eb4f7700…`、602分、20,000/200/50/150) | main の `_ready` が `enable_production_operations()` を呼び、`INVALID_START_TIME` で失敗する。この実結果を記録したうえで OFF | 原 work の「20,000マス・200客・50職員・150施設」の windowed x1/x3 |
| **O:新規開始** | 起動時に autoload の `SimulationRuntime._ready` が行う `initialize(GameClock, GameState, DEFAULT_WORLD_SEED)`(:201-206)。Day 1 06:00 | main の `_ready` が開始に成功した実際の ON | 通常UIで始まる Operations ON の自然営業 |
- O は代表規模ではありません。O の値を「200客」や「代表負荷」と呼ばず、実数を併記します。
- 200客での Operations ON は、製品契約(上限100)のために測定できません。これは P28 の未達として残し、P28-01 では満たしたことにしません。
### 4.3 起動順序(独立した Runtime は作らない)
計測するのは、既定の autoload(`/root/GameClock`、`/root/GameState`、`/root/SimulationRuntime`)と、main が描画する同じ世界だけです。`SimulationRuntime.new()` や別の `initialize` で作った Runtime は計測しません。
1. engine は phase ごとに1プロセスを起動し、r02 と同じ Guard の bootstrap(`profile_guard.gd`)と `--script` の wrapper を使います。windowed なので `--headless` は付けません。
2. autoload の準備が終わったら、`GameClock.set_paused(true)` で時間を止めます。
3. **R の場合**: `/root/SimulationRuntime.restore_snapshot(<get_var(false)で読んだ入力>)` で復元します。
- 復元が成功し、登録数が20,000/200/50/150と完全に一致すること、および入力 SHA の一致を確認します。
- 一致しなければ開始前の不足として停止します(§5)。
- **O の場合**は何も復元しません。
4. `res://scenes/main/main.tscn` を instantiate して root に追加します。main の `_ready`(main.gd:218-250)の処理は次のとおりで、それぞれの戻り値と `get_operations_report().enabled` の実値を記録します。
- `_pause_fresh_opening`
- `enable_production_save_policy`
- `enable_production_operations` の自動呼出し
5. `focus_operating_area` の deferred 処理が終わってから、camera(zoom、pan、layer、viewport の大きさ)を記録し、計測中はそのまま固定します。
6. 速度は `main._set_speed(n)`(:2727、`GameClock.set_speed` を呼ぶ)で設定し、一時停止の解除は `main._toggle_pause()`(:2689)で行います。どちらも UI のボタンと同じ処理です。
- 以降、時間は `SimulationKernel._process → process_real_delta` だけで進めます(MAX_TICKS_PER_FRAME 8、MAX_BACKLOG 120、simulation_kernel.gd:30-31、:54-72)。
- `advance_minutes`、`advance_one_minute`、`spawn_guest` などの直接呼出しはしません。
7. production save policy の下では autosave が起きることがあります。保存先は collector の private user:// に限り、通常 player の保全を前後で照合します。区間内に起きた autosave は stall の原因として記録します。
## 5. 測定
phase は次の4つです。それぞれ別の engine プロセスで実行し、区間は warm-up 30ゲーム分のあと、計測120ゲーム分です。
| phase | 入力 | 速度 |
|---|---|---|
| P1 | OFF | x1 |
| P2 | OFF | x3 |
| P3 | ON | x1 |
| P4 | ON | x3 |
| P1 | R(OFF) | x1 |
| P2 | R(OFF) | x3 |
| P3 | O(ON) | x1 |
| P4 | O(ON) | x3 |
各 phase の区間は warm-up 30ゲーム分のあと、計測120ゲーム分です。
- x1 headless での保存・読込・経路・snapshot は r02 の値を採用し、windowed では測りません。
1. **実UIと同じ Clock 経路を使う。** 速度は `main.gd::_set_speed`(速度ボタンと同じ処理)で設定し、時間は `SimulationKernel._process → process_real_delta`(MAX_TICKS_PER_FRAME 8、MAX_BACKLOG 120)だけで進めます。`advance_minutes(3)` などを直接呼んで出した値を x3 frame とは呼びません。
2. **SHA と実数を分けて残す。** 元 fixture の SHA と、各 phase の準備状態の SHA を記録します。数は sample ごとに3種類を区別して残します。
- 登録総数(客・職員・施設)
- 実稼働数(在館で、退館中でない客、勤務中の職員、稼働中の施設)
- 描画対象の数(viewport 内で可視の node)
**段階0(同じ collector)**
- P1・P2 の長い計測の前に、R を x1 で10ゲーム分だけ動かします。O についても P3・P4 の前に、x1 で10ゲーム分の限定動作を行います。
- 各段階0で次のものを記録してから、各 phase の所要時間を外挿します。
- Operations の enable の実結果
- 1分ごとの時間
- 実数
- 完了時間
- 見積り: R は 827 ms × 150分 × 2 ≒ 250 s です。O は段階0の実測から外挿します。
- phase ごとの上限は1800 s です。
warm-up で客が減った後の値を「200客の測定」とは呼ばず、実数を併記します。
3. **seed と RNG state を別欄にする。** seed は restore 後の `SimulationRuntime.world_seed` の実値です。RNG state(`rng_streams`)は別欄に記録し、state を seed の名前で代用しません。
4. **未取得は null と理由にする。** GPU 時間は `RenderingServer.viewport_set_measure_render_time` と `viewport_get_measured_render_time_gpu` で取得します。非対応・未取得なら null と理由を残し、0 や合格で補いません。CPU の描画時間、frame 時間、`Performance.TIME_PROCESS` も同じ扱いです。
5. **段階を踏んで実行する。**
- 所要時間の見積り: 1分 tick 827 ms から、(30+120)分 × 4 phase = 約500 s の CPU 時間。起動と import は別。
- 同じ collector の段階0として、Operations ON の限定動作を先に1回行います。enable の結果、10ゲーム分の1分ごとの時間、実数、完了時間を見ます。
- 段階0の実測から P3・P4 の所要時間を外挿し、記録してから長い計測へ進みます。
- 時間の超過(phase ごとに上限1800 s)や実数の不足があれば、その時点の原記録を保存して止めます。次の最小手順は判断担当が決めます。
6. **各 sample に残すもの**
- 機種: hw.model、コア数、メモリ
- build: engine の文字列・hash・SHA、project の commit、source map の SHA
- seed と RNG state(3. のとおり別欄)
- 速度、区間(開始・終了のゲーム分と wall 時刻)
- frame 時間、1分 tick、stall(50 ms 超の frame の数と最大)、描画 CPU/GPU のそれぞれについて、平均・P95・最大・標本数
- 予算は合否を生の値で並べるだけで、新しい許容値は作りません。
**各 sample に残すもの**(取得できない値は null と理由にし、0 や合格で補いません)
- 機種: hw.model、コア数、メモリ
- build: engine の文字列・hash・SHA、project の commit、source map の SHA
- seed: restore 後、または新規開始後の `SimulationRuntime.world_seed` の実値
- RNG state: `rng_streams`。seed とは別欄にします
- 入力 SHA と、phase 準備状態の SHA
- 速度と、`GameClock.speed` の実値
- 区間: 開始・終了のゲーム分と wall 時刻、frame 数、各 frame で進んだ分数
- 時間の統計: frame 時間、kernel の1分 tick(`get_performance_reports`)、stall(50 ms を超えた frame の数と最大)、描画 CPU/GPU の時間(`RenderingServer.viewport_set_measure_render_time`、`viewport_get_measured_render_time_cpu`/`_gpu`)、`Performance.TIME_PROCESS`。それぞれについて平均・P95・最大・標本数を記録します
- **実数は3種類を分けて記録します**
- 登録総数: 客・職員・施設・歩行可能セル
- 実稼働数: 退館中でない在館の客、勤務中の職員、稼働中の施設
- 描画数: 描画に実際に使われた model の数です。world_preview は Canvas に即時描画するため(`_draw` :512、`_draw_pixel_actor` :1525)、人数分の Node はありません。そのため Node の数ではなく、次のように数えます。
- actor: `world_preview.get_life_observation_report()`(:564)の `actor_drawings` のうち、現在の layer にあるもの(`_observe_actors` が layer で絞ったもの)の件数と、そのうち `at` が `Rect2(Vector2.ZERO, size)` の中にあるものの件数。observation_serial も添えます
- 施設: 完成した施設のうち、現在の layer にあり、cell の外接矩形が viewport と交わるもの(`_draw_world_entity_icons` :966 と同じ判定)の件数
- セル: bounds の中で `get_cell_rect(cell)` が viewport と交わるセル(`_draw_world_cells` :765 と同じ判定)の件数
- あわせて layer、zoom、pan、viewport の大きさを記録します
## 5. 対象外と非主張
**判定**: 予算(§2)との合否を生の値で並べます。新しい許容値は作りません。
- Windows 実機は未検証のまま残します(P02-03 / P28-03 の範囲)。
## 6. 停止条件と人数の扱い
- **開始前の不足**: 次のいずれかに当たれば、その phase の計測を始めずに原記録を残して止め、`input_shortfall` とします。
- R の入力 SHA の不一致
- 復元の失敗
- 登録数が20,000/200/50/150と一致しない
- O の Operations が開始できない(enabled が false、または開始が360分でない)
- **計測中の自然な変化**: 客の退館・来館、職員の状態の変化は、すべての sample の実数として記録します。これは停止条件ではありません。
- 計測の途中で客を再注入したり、状態を固定したり、復元をやり直したりすることは、通常の計測に入れません。
- 区間の要約には、登録数と実稼働数の最小・最大を必ず併記します。登録が200で、実稼働数も示した区間だけを「200客」と表記できます。
- **途中停止**: 次のいずれかに当たれば、そこまでの原記録を残して止めます。次の最小手順は判断担当が決めます。
- phase の上限(1800 s)を超えた
- engine が異常終了した
- Operations の runtime failure の latch、または terminal(破産)になった
- 要対応イベントのために clock が止まり、進まない
- collector がイベントを自動で選んで解消することはしません。
## 7. 証明の役割分担
| 担当 | 確認すること | 確認しないこと |
|---|---|---|
| checker(`check_p28_01_report.py`)と、合成 JSON での unittest | report の形式、必須の欄、null に理由があること、自己矛盾がないこと。たとえば次のものを拒否します。<br>・速度とゲーム分・frame の不整合<br>・seed 欄が RNG state と同じ値<br>・登録数が稼働数や描画数の欄に流用されている<br>・GPU 時間が 0 で理由がない<br>・「200客」の表記と実数の不一致 | 経路・seed・実稼働数が事実かどうか |
| probe の実経路の静的レビュー(独立 Opus) | §4.3 の順序、`_set_speed`/`_toggle_pause`/`_process` の経路、autoload の世界を計測していること、直接 advance・再注入・別 Runtime がないこと | 実行したかどうか |
| Root の実測(owned PID、raw の結合) | 実際の engine プロセスで、PID が一致すること、raw stdout と report が一致すること、source map と入力 SHA を照合した実行であること | — |
この3つがすべてそろったときに限り、記録は事実として扱われます。
## 8. 対象外と非主張
- Windows 実機は未検証のまま残します。
- 200客での Operations ON は、製品の上限100のために測定できず、未達として残します。
- P28 パッケージの予算達成、P28-02、P28-03、長期バランス、プレイ時間は対象外です。
- 4回分の全回帰と、fixture の全生成は行いません。
## 6. 完了の扱い
## 9. 完了の扱い
- **この Runner task**: collector、probe、checker が下の C1〜C8 を満たし、Runner の validation、独立レビュー、integration、final を通ること。これで原本の加算は0です。
- **原本 P28-01 の完了**: Root が統合済みの collector で実測し、その receipt を結合した後の証拠ゲートが通ったときに限ります。予算が未達でも、記録の項目がそろえば P28-01 の受入は成立します。未達の予算は P28 パッケージの未達として残します。
- **この Runner task**: collector、probe、checker が C1〜C9 を満たし、Runner の validation、独立レビュー、integration、final を通ること。これで原本の加算は0です。
- **原本 P28-01 の完了**: Root が統合済みの collector で実測し、その receipt を結合した後の証拠ゲートが通ったときに限ります。記録の項目がそろえば、予算が未達でも P28-01 の受入は成立します。未達の予算と200客 Operations ON は、P28 の未達として残します。
## 7. 登録条件(collector task の baseline)
## 10. 登録条件(collector task の baseline)
- **C1**: 速度は実UI/Clock の経路だけで与え、x3 を直接 advance の値で代用しないことを、checker が拒否試験で確認する。
- **C2**: 元 fixture と各 phase の準備状態の SHA、および sample ごとの登録・稼働・描画の3種類の実数を、別々の欄に記録する。
- **C3**: seed の実値と RNG state を別欄にし、取得できなければ null と理由にする。
- **C4**: GPU や描画時間を取得できなければ null と理由にする。0 や合格への補完を checker が拒否する。
- **C5**: 段階0(Operations ON の限定動作)と外挿の記録、phase ごとの上限、超過時に原記録を保存して止めることを実装する。
- **C1**: 速度は `main._set_speed`/`_toggle_pause`→`SimulationKernel._process` だけで与え、直接 advance の値を x3 とは呼ばない。checker は不整合を拒否し、probe は静的レビューの対象とする。
- **C2**: 入力 SHA と phase 準備状態の SHA、登録・実稼働・描画(§5の定義)の3種類の実数を、別の欄に記録する。
- **C3**: seed の実値(`world_seed`)と RNG state を別の欄にし、取得できなければ null と理由にする。
- **C4**: GPU や描画時間を取得できなければ null と理由にし、0 や合格への補完を checker が拒否する。
- **C5**: 段階0(R と O のそれぞれ)と外挿の記録、phase ごとの上限1800 s、超過時に原記録を保存して止めることを実装する。
- **C6**: private user://、owned PID・guard・cleanup・通常 player の保全・全 raw ログと SHA の結合を、r02 と同等に保つ。
- **C7**: 製品 source map が変わらないことを開始前と終了後に照合する。変われば拒否する。
- **C8**: 機種・build・seed・速度・区間・平均・P95・最大の各欄を全 sample に要求する。欠けた欄や偽の値は checker が拒否する。
- **C7**: 製品 source map が開始前と終了後で変わらないことを照合し、変われば拒否する。
- **C8**: 機種・build・seed・速度・区間・平均・P95・最大の各欄を全 sample に要求し、欠けた欄や偽の値を checker が拒否する。
- **C9**: §4.3 の起動順序(autoload の世界、main の `_ready` の実際の Operations の結果を記録)と、§6 の停止条件(開始前の不足と、計測中の自然な変化を区別する。再注入も状態の固定もしない)を実装する。
@@ -6,12 +6,13 @@
1. **`collect_p28_01.py`**: r02 の `collect_perf.py` を基にした外部 collector です。
- 1版(main の現 source)だけを、私有の CoW stage に materialize します。
- import は1回だけ行い、その後に段階0と P1〜P4 を1つずつ実行します。
- import は1回だけ行い、その後は R の段階0、P1、P2、O の段階0、P3、P4 の順に、それぞれ別の engine プロセスで1つずつ実行します。
- `--check` は入力と予定の argv を検証して表示するだけで、stage も engine も作りません。
- `--run` は実測します。Root が実行します。
2. **`measure_p28_01.gd`**: windowed の main scene 用 probe です。
- 代表入力の読込は r02 と同じく `FileAccess.get_var(false)`、`initialize`、`restore_snapshot` で行います。保存は private user:// だけです。
- 速度は `main.gd::_set_speed` で設定し、`SimulationKernel._process` が進めるのを観測するだけにします。直接 advance を呼んではいけません。
- 代表入力(R)は `FileAccess.get_var(false)` で読み、既定の autoload `/root/SimulationRuntime.restore_snapshot` に復元します。新規開始(O)は何も復元しません。どちらの場合も、別の Runtime の生成や `initialize` の再呼出しはしません(SPEC §4.3)。保存は private user:// だけです。
- main.tscn は、復元の後に root へ追加します。`_ready` で起きた Operations の実際の結果を記録します。
- 速度は `main._set_speed`、一時停止の解除は `main._toggle_pause` で行い、あとは `SimulationKernel._process` が進めるのを観測するだけにします。直接 advance を呼んではいけません。
- 計測は frame ごとに行います。
- stdout にちょうど1行 `P28_01_PERF_REPORT=<json>` を出します。
3. **`check_p28_01_report.py`**: report と収集結果の checker です。合格の条件と、拒否すべき偽の report は下に示します。
@@ -19,7 +20,7 @@
## 必須事項
- SPEC.md の §4 と C1〜C8 を、そのとおりに実装します。
- SPEC.md の §4〜§7 と C1〜C9 を、そのとおりに実装します。
- **checker が拒否すべきもの**(それぞれに対応する拒否試験を書く)
- x3 が直接 advance で作られている(Clock の速度と frame の記録が一致しない)
- 登録数を稼働数や描画数の欄に流用している
@@ -36,7 +37,11 @@
- native engine の起動(実測は Root が統合後に行います)
- fixture の生成
- P14 の再調査
- 参照用の入力(r02 の `collect_perf.py`、`measure.gd`、`profile_guard.gd`、`run-r02/execution.json` と report)は、Root が `build/p28-01-readonly/` に読取専用で置きます。SHA は SPEC.md §3 のとおりです。それ以外の場所や、拒否されたパスにはアクセスしないでください。
- 必ず守ること: SPEC §4.3 の起動順序と、§6 の停止条件。§4.1 のとおり、代表入力(R)で Operations は開始できず、200客での Operations ON は製品の上限で不可能です。clock を巻き戻すこと、上限を上書きすること、客を再注入すること、状態を固定することは禁止です。
- 描画数は Node の数ではなく、SPEC §5 の model の定義で数えます。
- checker が証明するのは形式と矛盾だけです(SPEC §7)。経路や seed が事実かどうかは、静的レビューと Root の実測で確かめます。
- 依存は自己完結させます(SPEC §3)。gitignore 対象の windows_adapter.py とその legacy/support は import しません。import してよいのは `tools/run_all_tests.py` と `tools/regression/owned_outer.py` だけで、どちらも変更しません。unittest の中で、collector の import 先にこの2つ以外の外部パスがないことを確認します。
- 参照用の入力(r02 の `collect_perf.py`、`measure.gd`、`profile_guard.gd`、`run-r02/execution.json` と report、さらに adapter の `windows_adapter.py`・`legacy/windows_adapter.py`)は、Root が `build/p28-01-readonly/` に読取専用で置きます。SHA は SPEC.md §3 と REGISTRATION_PROPOSAL.json の readonly_inputs_for_worker のとおりです。それ以外の場所や、拒否されたパスにはアクセスしないでください。
- commit はしません(Runner が submit します)。最終出力には変更したファイルと、試験の実行結果をそのまま出します。
## 検証