diff --git a/autoload/simulation_runtime.gd b/autoload/simulation_runtime.gd index a174f28..c9a0106 100644 --- a/autoload/simulation_runtime.gd +++ b/autoload/simulation_runtime.gd @@ -12420,6 +12420,8 @@ func _on_guest_despawned(guest_id: StringName, persistent_record: Dictionary) -> elevators.cancel_actor(actor_id) var path_removed := path_service.despawn_actor(actor_id) _movement_credits.erase(actor_id) + # A13: a rain hold describes a present guest, so it leaves with the guest. + weather.release_rain_hold(guest_id) _synchronize_operations_population() var payload := persistent_record.duplicate(true) payload[&"path_actor_id"] = actor_id diff --git a/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json b/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json index 8120b82..5b146bd 100644 --- a/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json +++ b/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json @@ -3964,7 +3964,7 @@ "template": "PASS: checks", "required": true, "fixed_check_count_used": false, - "source_sha256_at_schema_audit": "0dfba98e091f015f74fc7897f8a7b7ce8e286826dc2a17785095ab4e0d51a946", + "source_sha256_at_schema_audit": "e34b52ba3511f7391de1435c2f907997dd10235742ebc029831808af969c68e0", "authority": { "path": "docs/plans/original-completion-2026-10-02/execution/p07-01/SPEC.md", "scope": "new P07-01 suite marker; native execution pending" @@ -3975,7 +3975,7 @@ "template": "PASS: checks", "required": true, "fixed_check_count_used": false, - "source_sha256_at_schema_audit": "39ca0eb47eef46a5c2c73cb165b300360753ce74010b6512901c0f921d2e00a6", + "source_sha256_at_schema_audit": "d85e56dcf75535b19b1af615f8b75e010f4f3906a0e71afdf97102e1bfbffc65", "authority": { "path": "docs/plans/original-completion-2026-10-02/execution/p07-01/SPEC.md", "scope": "new P07-01 F7 restart writer marker; native execution pending" @@ -3986,7 +3986,7 @@ "template": "PASS: checks", "required": true, "fixed_check_count_used": false, - "source_sha256_at_schema_audit": "dd7c41952ced7edf38d2fbc5517128bc7b955c49ddccca90eb933062f4b8e8a7", + "source_sha256_at_schema_audit": "e5c2376e9cb3197d6a0a8d99c9dea020cad2648328178949cd188d999e72071a", "authority": { "path": "docs/plans/original-completion-2026-10-02/execution/p07-01/SPEC.md", "scope": "new P07-01 F7 restart reader marker; native execution pending" diff --git a/docs/plans/original-completion-2026-10-02/execution/p07-01/wx/IMPLEMENTATION_NOTES.md b/docs/plans/original-completion-2026-10-02/execution/p07-01/wx/IMPLEMENTATION_NOTES.md index 47f427b..1cbaa1e 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p07-01/wx/IMPLEMENTATION_NOTES.md +++ b/docs/plans/original-completion-2026-10-02/execution/p07-01/wx/IMPLEMENTATION_NOTES.md @@ -91,3 +91,33 @@ - 比べる状態の `needs` は、名前を文字の順に並べる。読み直した客は needs のキーの順が違うことがあり、`StringName` の並べ替えは文字の順ではなく、process ごとに違うため。 - 固定の値: 変えた3本(`test_p07_weather_runtime.gd`・writer・reader)の source の SHA を、`tools/regression/suite-policy.json` と `completion_marker_contracts.json` で更新した。 - 確かめ(局所。正式の証拠ではない): scratch の stage で、変えた suite と狭い roster の 12 本、kitchen を直接動かして合格した(`P07_01_WX_REPAIR1_LOCAL_CHECKS_R01.json`)。正式の証拠は、新しい S の native の収集、登録の validation、全 roster、独立レビュー。 + +## 7. 直し 2(S `f99c881b` の最終の独立レビューの後。repair 2) + +- 契機: + - S `f99c881b` の最終の独立レビュー(`local_c4c1a8d6`)は changes_requested(medium 1・low 1)。前のレビューの high と medium は解消したと判定された。 + - S `f99c881b` の全 roster は合格した(198 本・22,857 checks)。 + - 管理の記録は `COMMANDER_REPAIR_PLAN_P07_01_WX_R02.md`。 +- 診断(scratch の stage)で分かったこと: + - 施設の無い試験の世界では、延びた客は 06:03 に帰り始め(`NO_SUITABLE_FACILITY`)、06:04 に居なくなる。必要(needs)の値や客の種類を変えても同じ。 + - そのため、再開の保存には延長の記録が一度も乗っていなかった。 + - 製品の不具合(持ち越したコード): 延長の記録は、次の tick の天候の境界まで片づかなかった。 + - そのため、客が居なくなった分の保存は、保存は成功するが読み込めなかった(`RESTORE_FAILED`)。 + - 原因は、復元の照合「延長は館内の客の予定と一致すること」。 +- 直したもの: + 1. 製品: 客が居なくなった時(`autoload/simulation_runtime.gd` の `_on_guest_despawned`)に、その客の延長の記録をすぐ片づける。 + - `scripts/environment/weather_timeline.gd` に `release_rain_hold` を足した。 + - 天候の境界での片づけは今のまま残した。 + 2. medium(WX7): 再開の台本の雨の場面に、2つの保存を足した。 + - 363 分: 延びた客が帰る途中で館内に居て、延長の記録がある。 + - 364 分: 客が居なくなった直後で、延長の記録が無い。上の不具合の回帰の確かめ。 + - writer は、雨の全ての保存を同じ process で読み直す。読んだ直後・1分後・最後の状態が、straight と bytes で一致することを確かめる。 + - reader は、別の process で同じことを確かめる。 + - 363 分の保存では、延長の記録が1つで、`planned_exit_at` が 602 であることを厳密に確かめる。1分後の一致で、二度目の延長が無いことを示す。 + - 500 分の確かめから、誤っていた「with the hold of a present guest」を外した。 + 3. 回帰: `test_p07_weather_runtime.gd` で、通常の進行で延びた客が帰った分に、延長の記録が片づいていて、その分の snapshot が復元できることを確かめる。 +- 開示(U1): 施設の無い試験の世界では、延びた客は延長の時刻より前に、施設が無いので帰る。 + - そのため、通常の時刻の進行で延長の時刻(10:02)に着いて帰る場面は、この試験では作れない。施設を建てて客が使う場面が要る。 + - 延長の時刻の帰りは、決定の処理を延長の時刻でじかに呼んで確かめている(`_test_rain_hold_exit`)。延長の時刻に、二度目の延長をせず、PLANNED_EXIT で帰り始める。 +- 固定の値: 変えた3本の source の SHA を、`tools/regression/suite-policy.json` と `completion_marker_contracts.json` で更新した。 +- 確かめ(局所。正式の証拠ではない): scratch の stage で、狭い roster の 12 本・再開の2段・kitchen を直接動かして合格した(`P07_01_WX_REPAIR2_LOCAL_CHECKS_R01.json`)。 diff --git a/scripts/environment/weather_timeline.gd b/scripts/environment/weather_timeline.gd index 10e3996..d07f480 100644 --- a/scripts/environment/weather_timeline.gd +++ b/scripts/environment/weather_timeline.gd @@ -145,6 +145,11 @@ func prune_rain_holds(present_guest_ids: Dictionary) -> void: if not present_guest_ids.has(StringName(key)): _rain_holds.erase(key) +## The hold of a guest that has just left; a save in that same minute then +## holds only present guests' holds (P07-01 SPEC section 5). +func release_rain_hold(guest_id: StringName) -> void: + _rain_holds.erase(String(guest_id)) + func create_snapshot() -> Dictionary: return {&"version": VERSION, &"minute": _minute, &"kind": _kind, &"since": _since, &"pending": _pending.duplicate(true), diff --git a/tests/environment/test_p07_weather_restart_1_write.gd b/tests/environment/test_p07_weather_restart_1_write.gd index a02f294..ecb09d7 100644 --- a/tests/environment/test_p07_weather_restart_1_write.gd +++ b/tests/environment/test_p07_weather_restart_1_write.gd @@ -26,15 +26,23 @@ func _run() -> void: var straight := fixtures.new_fixture() var straight_runtime: Variant = straight[&"runtime"] var held_guest: Variant = straight_runtime.guest_service.get_guest(Fixture.HELD_GUEST) - var held := {"hold": {}, "reason": &""} - fixtures.play(straight, Fixture.FIRST_MINUTE, Fixture.LAST_MINUTE, func(_minute: int) -> void: + var held := {"hold": {}, "reason": &"", "after": {}} + fixtures.play(straight, Fixture.FIRST_MINUTE, Fixture.LAST_MINUTE, func(minute: int) -> void: if (held["hold"] as Dictionary).is_empty(): held["hold"] = straight_runtime.weather.get_rain_hold(Fixture.HELD_GUEST) if StringName(held["reason"]).is_empty(): - held["reason"] = held_guest.exit_reason) + held["reason"] = held_guest.exit_reason + if minute == 363: + _expect(straight_runtime.guest_service.get_guest(Fixture.HELD_GUEST) != null and held_guest.state == GuestRuntime.State.LEAVING + and straight_runtime.weather.get_rain_hold(Fixture.HELD_GUEST) == Fixture.HOLD, "at 06:03 the held guest is still present and leaving with its hold") + if minute == 364: + _expect(straight_runtime.guest_service.get_guest(Fixture.HELD_GUEST) == null and straight_runtime.weather.get_rain_holds().is_empty(), + "at 06:04 the held guest is gone and its hold left with it") + if minute - 1 in Fixture.SAVE_MINUTES: + held["after"][minute] = fixtures.state_bytes(straight_runtime)) # The hold only moves the planned exit; every other current rule still applies, # so in this world without facilities the guest leaves for lack of one. - _expect(held["hold"] == {"original_exit": 362, "extended_to": 602} and held_guest.planned_exit_at == 602 and held["reason"] == &"NO_SUITABLE_FACILITY", + _expect(held["hold"] == Fixture.HOLD and held_guest.planned_exit_at == 602 and held["reason"] == &"NO_SUITABLE_FACILITY", "the guest due at 06:02 was held to 10:02 and still left by the current rules (%s)" % String(held["reason"])) _expect(straight[&"draws"][&"count"] == Fixture.DRAW_MINUTES.size() and straight_runtime.weather.get_last_draw_day() == 1, "the uninterrupted script draws day 1 once") var saving := fixtures.new_fixture() @@ -43,16 +51,39 @@ func _run() -> void: var saved_paths := {} fixtures.play(saving, Fixture.FIRST_MINUTE, Fixture.LAST_MINUTE, func(minute: int) -> void: if minute in Fixture.SAVE_MINUTES: + if minute == 363: + _expect(saving_runtime.guest_service.get_guest(Fixture.HELD_GUEST) != null and saving_runtime.weather.get_rain_holds() == {String(Fixture.HELD_GUEST): Fixture.HOLD} + and saving_runtime.guest_service.get_guest(Fixture.HELD_GUEST).planned_exit_at == 602, "minute 363 saves the hold of the present held guest") + if minute == 364: + _expect(saving_runtime.guest_service.get_guest(Fixture.HELD_GUEST) == null and saving_runtime.weather.get_rain_holds().is_empty(), "minute 364 saves just after the held guest left, without its hold") if minute == 500: - var held_present: bool = saving_runtime.guest_service.get_guest(Fixture.HELD_GUEST) != null - _expect(saving_runtime.weather.get_kind() == &"rain" and (not held_present or not saving_runtime.weather.get_rain_hold(Fixture.HELD_GUEST).is_empty()), "minute 500 saves mid-rain with the hold of a present guest") + _expect(saving_runtime.weather.get_kind() == &"rain", "minute 500 saves mid-rain") var path := Fixture.WRITER_SAVE % minute saved_states[minute] = fixtures.state_bytes(saving_runtime) _expect(bool(saving_runtime.save_to_path(path).get(&"ok", false)), "save file at minute %d" % minute) saved_paths[minute] = path) _expect(fixtures.state_bytes(saving_runtime) == fixtures.state_bytes(straight_runtime), "writing the save files leaves the weather state as without saving") - var saves := _packages(Fixture.SAVE_MINUTES, saved_paths, saved_states, Fixture.WRITER_SAVE) var final_state := fixtures.state_bytes(straight_runtime) + for minute: int in Fixture.SAVE_MINUTES: + var reload := fixtures.new_fixture(false) + var reload_runtime: Variant = reload[&"runtime"] + _expect(bool(reload_runtime.load_from_path(str(saved_paths.get(minute, Fixture.WRITER_SAVE % minute))).get(&"ok", false)) + and fixtures.state_bytes(reload_runtime) == saved_states.get(minute, PackedByteArray()) and reload[&"clock"].absolute_minute == minute, + "this process reloads the save file of minute %d exactly" % minute) + if minute == 363: + _expect(reload_runtime.weather.get_rain_holds() == {String(Fixture.HELD_GUEST): Fixture.HOLD} and reload_runtime.guest_service.get_guest(Fixture.HELD_GUEST).planned_exit_at == 602, + "the reloaded minute 363 save keeps the one hold and the extended plan") + fixtures.play(reload, minute, minute + 1, Callable()) + _expect(fixtures.state_bytes(reload_runtime) == held["after"].get(minute + 1, PackedByteArray()), + "one minute after the minute %d reload equals the uninterrupted run (no second extension)" % minute) + fixtures.play(reload, minute + 1, Fixture.LAST_MINUTE, Callable()) + _expect(fixtures.state_bytes(reload_runtime) == final_state, "the minute %d reload continues to the uninterrupted final state" % minute) + fixtures.free_fixture(reload) + var saves := _packages(Fixture.SAVE_MINUTES, saved_paths, saved_states, Fixture.WRITER_SAVE) + var after := {} + for minute: int in held["after"]: + var bytes: PackedByteArray = held["after"][minute] + after[str(minute)] = {"sha256": Fixture.sha256(bytes), "base64": Marshalls.raw_to_base64(bytes)} var fog := _fog_scene(fixtures) var handoff := { "schema": Fixture.HANDOFF_SCHEMA, @@ -62,6 +93,7 @@ func _run() -> void: "last_minute": Fixture.LAST_MINUTE, "straight_final_state_sha256": Fixture.sha256(final_state), "straight_final_state_base64": Marshalls.raw_to_base64(final_state), + "after_states": after, "saves": saves, "fog": fog, } diff --git a/tests/environment/test_p07_weather_restart_2_read.gd b/tests/environment/test_p07_weather_restart_2_read.gd index 4d898ab..2f78efa 100644 --- a/tests/environment/test_p07_weather_restart_2_read.gd +++ b/tests/environment/test_p07_weather_restart_2_read.gd @@ -55,19 +55,28 @@ func _check_restarts(handoff: Dictionary) -> void: _expect(Fixture.sha256(final_state) == str(handoff.get("straight_final_state_sha256", "")), "the writer's final state arrived intact") var saves: Array = handoff.get("saves", []) _expect(saves.size() == Fixture.SAVE_MINUTES.size(), "every save package was handed off") + var after: Dictionary = handoff.get("after_states", {}) for save: Variant in saves: var minute := int((save as Dictionary).get("minute", -1)) var path := Fixture.READER_SAVE % minute _expect(_write_package(save, path), "save package at minute %d arrived byte-exact" % minute) var expected_state := Marshalls.base64_to_raw(str((save as Dictionary).get("state_base64", ""))) + var next_row: Dictionary = after.get(str(minute + 1), {}) + var next_state := Marshalls.base64_to_raw(str(next_row.get("base64", ""))) + _expect(not next_state.is_empty() and Fixture.sha256(next_state) == str(next_row.get("sha256", "")), "the writer's state one minute after %d arrived intact" % minute) var fixture := fixtures.new_fixture(false) var runtime: Variant = fixture[&"runtime"] _expect(bool(runtime.load_from_path(path).get(&"ok", false)), "the reader loads the minute %d save" % minute) _expect(fixtures.state_bytes(runtime) == expected_state and fixture[&"clock"].absolute_minute == minute, "the minute %d save resumes the writer's weather, holds, stream and plans" % minute) + if minute == 363: + _expect(runtime.weather.get_rain_holds() == {String(Fixture.HELD_GUEST): Fixture.HOLD} and runtime.guest_service.get_guest(Fixture.HELD_GUEST) != null + and runtime.guest_service.get_guest(Fixture.HELD_GUEST).planned_exit_at == 602, "the reader resumes the one hold of the present held guest and its extended plan") # Loading replaces the event stream, so the draw counter subscribes again. fixture[&"draws"][&"count"] = 0 fixtures.watch_draws(fixture) - fixtures.play(fixture, minute, Fixture.LAST_MINUTE, Callable()) + fixtures.play(fixture, minute, minute + 1, Callable()) + _expect(fixtures.state_bytes(runtime) == next_state, "one minute after the minute %d save the reader equals the writer (no second extension)" % minute) + fixtures.play(fixture, minute + 1, Fixture.LAST_MINUTE, Callable()) var expected_draws := 0 for draw_minute: int in Fixture.DRAW_MINUTES: if draw_minute > minute: diff --git a/tests/environment/test_p07_weather_runtime.gd b/tests/environment/test_p07_weather_runtime.gd index 972fc02..1ce7eb1 100644 --- a/tests/environment/test_p07_weather_runtime.gd +++ b/tests/environment/test_p07_weather_runtime.gd @@ -238,6 +238,16 @@ func _test_rain_stay_and_saves() -> void: ## itself runs at the extended time: it is not held again and begins leaving ## with the ordinary PLANNED_EXIT. func _test_rain_hold_exit() -> void: + var natural := _new_runtime(SEED_RAIN) + var natural_runtime: Variant = natural[&"runtime"] + var departing: Variant = _spawn(natural_runtime, &"p07_departing_guest", &"traveler", 362) + natural[&"clock"].advance_minutes(3) + _expect(natural_runtime.weather.get_rain_hold(&"p07_departing_guest") == {"original_exit": 362, "extended_to": 602} and departing.state == GuestRuntime.State.LEAVING + and departing.exit_reason == &"NO_SUITABLE_FACILITY", "in ordinary minutes the held guest still leaves by the current rules") + natural[&"clock"].advance_minutes(1) + var departed: Dictionary = natural_runtime.create_snapshot() + _expect(natural_runtime.guest_service.get_guest(&"p07_departing_guest") == null and natural_runtime.weather.get_rain_holds().is_empty() + and natural_runtime.restore_snapshot(departed) and natural_runtime.create_snapshot() == departed, "the hold leaves with the guest, so the minute it left restores") var rainy := _new_runtime(SEED_RAIN) var runtime: Variant = rainy[&"runtime"] var guest: Variant = _spawn(runtime, &"p07_exit_guest", &"traveler", 362) diff --git a/tests/helpers/p07_weather_restart_fixture.gd b/tests/helpers/p07_weather_restart_fixture.gd index 3f0f905..819aa56 100644 --- a/tests/helpers/p07_weather_restart_fixture.gd +++ b/tests/helpers/p07_weather_restart_fixture.gd @@ -5,6 +5,8 @@ extends RefCounted ## (tests/environment/test_p07_weather_restart_2_read.gd), which run as separate ## engine processes. Seed 1106 opens day 0 with rain 06:00-12:00 (A13), a guest ## due at 06:02 is held to 10:02, production arrivals run, and day 1 draws at 06:00. +## With no facility in this world the held guest starts leaving at 06:03 (still +## present with its hold) and is gone at 06:04 (its hold leaves with it). ## The fog scene (WX7): seed 1592 opens day 0 with mana fog 06:00-10:00, two ## adult-verified guests arrive at 06:58 and 09:58 (with no facility in this ## world a guest leaves after a few minutes, so each one is present at its @@ -16,8 +18,10 @@ const WRITER_SAVE := "user://p07_restart_write/minute_%d" const READER_SAVE := "user://p07_restart_read/minute_%d" const SEED := 1106 const FIRST_MINUTE := 360 -# Mid-rain, just before and just after the rain end, just before and just after the day 1 draw. -const SAVE_MINUTES: Array[int] = [500, 719, 720, 1799, 1800] +# The held guest present with its hold, the minute it left, mid-rain, just before and +# just after the rain end, just before and just after the day 1 draw. +const SAVE_MINUTES: Array[int] = [363, 364, 500, 719, 720, 1799, 1800] +const HOLD := {"original_exit": 362, "extended_to": 602} const LAST_MINUTE := 1830 const DRAW_MINUTES: Array[int] = [1800] const HELD_GUEST := &"p07_restart_guest" diff --git a/tools/regression/suite-policy.json b/tools/regression/suite-policy.json index 03b091a..befcf95 100644 --- a/tools/regression/suite-policy.json +++ b/tools/regression/suite-policy.json @@ -440,21 +440,21 @@ } , "tests/environment/test_p07_weather_runtime.gd": { - "source_sha256": "0dfba98e091f015f74fc7897f8a7b7ce8e286826dc2a17785095ab4e0d51a946", + "source_sha256": "e34b52ba3511f7391de1435c2f907997dd10235742ebc029831808af969c68e0", "ordinary_cleanup_prepared": false, "ordinary_control_labels": [], "old_bounded_teardown": true, "new_audio_QA": false }, "tests/environment/test_p07_weather_restart_1_write.gd": { - "source_sha256": "39ca0eb47eef46a5c2c73cb165b300360753ce74010b6512901c0f921d2e00a6", + "source_sha256": "d85e56dcf75535b19b1af615f8b75e010f4f3906a0e71afdf97102e1bfbffc65", "ordinary_cleanup_prepared": false, "ordinary_control_labels": [], "old_bounded_teardown": false, "new_audio_QA": false }, "tests/environment/test_p07_weather_restart_2_read.gd": { - "source_sha256": "dd7c41952ced7edf38d2fbc5517128bc7b955c49ddccca90eb933062f4b8e8a7", + "source_sha256": "e5c2376e9cb3197d6a0a8d99c9dea020cad2648328178949cd188d999e72071a", "ordinary_cleanup_prepared": false, "ordinary_control_labels": [], "old_bounded_teardown": false,