Compare commits
5
Commits
7ac2224e47
...
0b59d7c23c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b59d7c23c | ||
|
|
9b5b09b35e | ||
|
|
db87270972 | ||
|
|
f99c881bca | ||
|
|
c50136017b |
@@ -37,6 +37,7 @@ const CleaningBridge = preload("res://scripts/environment/facility_cleaning_brid
|
||||
const CleaningPolicy = preload("res://scripts/environment/facility_cleaning_policy.gd")
|
||||
const UPKEEP_RUNTIME_SNAPSHOT_VERSION := 10
|
||||
const WeatherTimeline = preload("res://scripts/environment/weather_timeline.gd")
|
||||
const NaturalWeather = preload("res://scripts/environment/natural_weather_policy.gd")
|
||||
const STAIRS_RUNTIME_SNAPSHOT_VERSION := 9
|
||||
const TRANSPORT_RUNTIME_SNAPSHOT_VERSION := 8
|
||||
const SUPPLY_RUNTIME_SNAPSHOT_VERSION := 7
|
||||
@@ -85,6 +86,7 @@ const RNG_STREAM_IDS: Array[StringName] = [
|
||||
&"defense",
|
||||
&"event",
|
||||
&"loot",
|
||||
&"weather",
|
||||
]
|
||||
const DEFENSE_COMMAND_TYPES: Array[StringName] = [
|
||||
&"create_defense_raid",
|
||||
@@ -463,6 +465,8 @@ func initialize(clock: SimulationKernel, state: Variant, seed := DEFAULT_WORLD_S
|
||||
if not report[&"valid"]:
|
||||
_clock.set_paused(true)
|
||||
definition_validation_failed.emit(report)
|
||||
# A13: a new game opening at 06:00 draws its first day once streams exist.
|
||||
_draw_natural_weather_if_due(_clock.absolute_minute)
|
||||
_publish_snapshot(_clock.absolute_minute)
|
||||
return report
|
||||
|
||||
@@ -1291,6 +1295,18 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
return false
|
||||
elif runtime_data.has(&"weather") or not restored_weather.initialize(&"clear", restored_minute):
|
||||
return false
|
||||
# A13: the saved day's draw must be the draw rule's own result (SPEC section 5.3).
|
||||
if not NaturalWeather.is_consistent_draw(restored_weather.get_today_draw()):
|
||||
return false
|
||||
# A13 rain holds must still describe a present guest's extended plan.
|
||||
var rain_holds := restored_weather.get_rain_holds()
|
||||
for held_key: Variant in rain_holds:
|
||||
var held_guest := restored_guests.get_guest(StringName(held_key))
|
||||
if held_guest == null or held_guest.planned_exit_at != int(rain_holds[held_key]["extended_to"]):
|
||||
return false
|
||||
var saved_weather: Variant = runtime_data.get(&"weather", null)
|
||||
var weather_stream_required: bool = saved_weather is Dictionary \
|
||||
and int(saved_weather.get("version", 0)) >= WeatherTimeline.VERSION
|
||||
var restored_cleanliness := FacilityCleanliness.new()
|
||||
if runtime_schema_version >= CLEANING_RUNTIME_SNAPSHOT_VERSION:
|
||||
if not runtime_data.get(&"facility_cleanliness") is Dictionary or not restored_cleanliness.restore_snapshot(runtime_data[&"facility_cleanliness"]):
|
||||
@@ -1323,6 +1339,12 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
if has_defense_snapshot:
|
||||
return false
|
||||
restored_streams[&"defense"] = DeterministicRng.from_root_seed(restored_world_seed, &"defense")
|
||||
# A13 migration: saves before natural weather start the weather stream from
|
||||
# the world seed, as defense did (SPEC section 5.2).
|
||||
if not restored_streams.has(&"weather"):
|
||||
if weather_stream_required:
|
||||
return false
|
||||
restored_streams[&"weather"] = DeterministicRng.from_root_seed(restored_world_seed, &"weather")
|
||||
if runtime_schema_version >= DEFENSE_RUNTIME_SNAPSHOT_VERSION:
|
||||
for required_stream_id: StringName in RNG_STREAM_IDS:
|
||||
if not restored_streams.has(required_stream_id):
|
||||
@@ -2951,6 +2973,8 @@ func _operations_arrival_boundary(
|
||||
&"ok": not captured.is_empty(),
|
||||
&"token": captured,
|
||||
&"capacity_remaining": maxi(population_cap - guest_service.guest_count(), 0) if int(_state.get_resource(&"mana")) > 0 and weather.permits_new_arrivals() else 0,
|
||||
# A13: rain halves the planned new arrivals of this boundary.
|
||||
&"arrival_percent": NaturalWeather.arrival_percent(weather.get_kind()),
|
||||
}
|
||||
if phase == OperationsCoordinator.PHASE_VALIDATE:
|
||||
return {
|
||||
@@ -8784,6 +8808,7 @@ func _advance_guest_vitals(absolute_minute: int) -> void:
|
||||
&"is_night": minute_of_day < 360 or minute_of_day >= 1_080,
|
||||
&"intimacy_day": 3,
|
||||
&"intimacy_night": 7,
|
||||
&"mana_fog": weather.get_kind() == &"mana_fog",
|
||||
}
|
||||
})
|
||||
|
||||
@@ -9035,7 +9060,7 @@ func _advance_guest_ai_decisions(absolute_minute: int) -> void:
|
||||
GuestRuntime.State.ENTERING:
|
||||
guest_service.transition_guest(guest.guest_id, GuestRuntime.State.DECIDING, absolute_minute, &"ENTRY_COMPLETED")
|
||||
GuestRuntime.State.DECIDING:
|
||||
if guest.planned_exit_at <= absolute_minute:
|
||||
if guest.planned_exit_at <= absolute_minute and not _hold_guest_for_rain(guest, absolute_minute):
|
||||
guest_service.begin_leaving(guest.guest_id, absolute_minute, &"PLANNED_EXIT")
|
||||
continue
|
||||
guest.wallet_gold = economy_service.get_balance(_guest_wallet_account(guest.guest_id))
|
||||
@@ -10993,13 +11018,14 @@ func _reservation_references_are_valid(
|
||||
## HUD view of the A06 reserve: supply, consumption and storage per interval.
|
||||
func get_mana_reserve_report() -> Dictionary:
|
||||
var demand := _mana_reserve_demand()
|
||||
var generation := NaturalWeather.mana_yield(ManaReserveCycle.WORLD_TREE_GENERATION, weather.get_kind())
|
||||
return {
|
||||
&"amount": int(_state.get_resource(&"mana")),
|
||||
&"capacity": ManaReserveCycle.CAPACITY,
|
||||
&"generation": ManaReserveCycle.WORLD_TREE_GENERATION,
|
||||
&"generation": generation,
|
||||
&"interval_minutes": ManaReserveCycle.INTERVAL_MINUTES,
|
||||
&"demand": demand,
|
||||
&"net": ManaReserveCycle.WORLD_TREE_GENERATION - demand,
|
||||
&"net": generation - demand,
|
||||
&"last_flow": mana_reserve.get_last_flow(),
|
||||
}
|
||||
|
||||
@@ -11032,8 +11058,10 @@ func _advance_mana(absolute_minute: int) -> void:
|
||||
return
|
||||
# A06: generation and upkeep demand settle before local distribution and the
|
||||
# whole-tower gate, so the network and projection see the committed reserve.
|
||||
# A13: mana fog raises the world tree yield of this boundary to x1.2 (floor).
|
||||
var reserve_flow := mana_reserve.advance_to(
|
||||
absolute_minute, int(_state.get_resource(&"mana")), _mana_reserve_demand()
|
||||
absolute_minute, int(_state.get_resource(&"mana")), _mana_reserve_demand(),
|
||||
NaturalWeather.mana_yield(ManaReserveCycle.WORLD_TREE_GENERATION, weather.get_kind())
|
||||
)
|
||||
if not bool(reserve_flow.get(&"ok", false)):
|
||||
_latch_infrastructure_failure(
|
||||
@@ -12392,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
|
||||
@@ -13572,10 +13602,66 @@ func get_staff_rest_report(staff_id: StringName) -> Dictionary:
|
||||
|
||||
func _advance_weather(absolute_minute: int) -> void:
|
||||
weather.advance_to(absolute_minute)
|
||||
_draw_natural_weather_if_due(absolute_minute)
|
||||
if not weather.get_rain_holds().is_empty():
|
||||
var present := {}
|
||||
for guest: GuestRuntime in guest_service.get_guests():
|
||||
present[guest.guest_id] = true
|
||||
weather.prune_rain_holds(present)
|
||||
|
||||
|
||||
## A13: one draw per day at 06:00 from the saved weather stream. The weather
|
||||
## boundary runs first, so the same minute's arrivals and closes see the result.
|
||||
func _draw_natural_weather_if_due(absolute_minute: int) -> void:
|
||||
if not NaturalWeather.is_draw_minute(absolute_minute) or weather.get_minute() != absolute_minute:
|
||||
return
|
||||
var day := NaturalWeather.day_of(absolute_minute)
|
||||
var stream := get_rng_stream(&"weather")
|
||||
if day <= weather.get_last_draw_day() or stream == null:
|
||||
return
|
||||
var draw := NaturalWeather.draw(stream, day)
|
||||
var applied := weather.apply_draw(draw)
|
||||
var no_entities: Array[StringName] = []
|
||||
_publish_actor_event(&"WEATHER_FORECAST_DRAWN", &"weather", no_entities, {
|
||||
&"day": day,
|
||||
&"kind": String(draw[&"kind"]),
|
||||
&"roll": int(draw[&"roll"]),
|
||||
&"slot": int(draw[&"slot"]),
|
||||
&"start": int(draw[&"start"]),
|
||||
&"end": int(draw[&"end"]),
|
||||
&"applied": bool(applied.get(&"ok", false)),
|
||||
&"code": String(applied.get(&"code", &"")),
|
||||
})
|
||||
|
||||
|
||||
## A13: a guest due to leave while it rains stays until the rain ends, at most
|
||||
## 240 minutes after the original plan, once per guest (SPEC section 4.3).
|
||||
func _hold_guest_for_rain(guest: GuestRuntime, absolute_minute: int) -> bool:
|
||||
if weather.get_kind() != &"rain" or not weather.get_rain_hold(guest.guest_id).is_empty():
|
||||
return false
|
||||
var original := guest.planned_exit_at
|
||||
var extended := original + NaturalWeather.RAIN_HOLD_MAX_MINUTES
|
||||
var rain_end := weather.rain_end_minute()
|
||||
if rain_end >= 0:
|
||||
extended = mini(extended, rain_end)
|
||||
if extended <= absolute_minute or not weather.add_rain_hold(guest.guest_id, original, extended):
|
||||
return false
|
||||
guest.planned_exit_at = extended
|
||||
return true
|
||||
|
||||
|
||||
func get_weather_report() -> Dictionary:
|
||||
return {&"kind": weather.get_kind(), &"minute": weather.get_minute()}
|
||||
var kind := weather.get_kind()
|
||||
return {
|
||||
&"kind": kind,
|
||||
&"minute": weather.get_minute(),
|
||||
&"since": weather.get_since(),
|
||||
&"next_change": weather.next_change(),
|
||||
&"today_draw": weather.get_today_draw(),
|
||||
&"effect_text": NaturalWeather.effect_text(kind),
|
||||
&"arrival_percent": NaturalWeather.arrival_percent(kind),
|
||||
&"rain_holds": weather.get_rain_holds().size(),
|
||||
}
|
||||
|
||||
|
||||
## Read-only migration preview; applying dimensions remains a separate transaction.
|
||||
|
||||
@@ -3958,6 +3958,39 @@
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/SPEC.md",
|
||||
"scope": "new P05-02 F6 restart reader marker; native execution pending"
|
||||
}
|
||||
},
|
||||
{
|
||||
"test": "tests/environment/test_p07_weather_runtime.gd",
|
||||
"template": "PASS: <checks> checks",
|
||||
"required": true,
|
||||
"fixed_check_count_used": false,
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
"test": "tests/environment/test_p07_weather_restart_1_write.gd",
|
||||
"template": "PASS: <checks> checks",
|
||||
"required": true,
|
||||
"fixed_check_count_used": false,
|
||||
"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"
|
||||
}
|
||||
},
|
||||
{
|
||||
"test": "tests/environment/test_p07_weather_restart_2_read.gd",
|
||||
"template": "PASS: <checks> checks",
|
||||
"required": true,
|
||||
"fixed_check_count_used": false,
|
||||
"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"
|
||||
}
|
||||
}
|
||||
],
|
||||
"unknown_roster_suite_policy": "refuse, explicitly review its source/schema before accepting",
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"task": "P07-01",
|
||||
"spec": "docs/plans/original-completion-2026-10-02/execution/p07-01/SPEC.md",
|
||||
"rule": "SPEC F8: every changed expectation of an existing suite is listed with the value before and after and the reason. Only the natural-weather contract (A13, SPEC sections 4 and 5) causes these changes.",
|
||||
"changes": [
|
||||
{
|
||||
"suite": "tests/environment/test_runtime_weather.gd",
|
||||
"check": "old neutral save migration preserves other state",
|
||||
"before": "runtime.create_snapshot() == original (the whole snapshot after migrating a version 10 save equals the original new game)",
|
||||
"after": "the whole snapshot without systems.runtime.weather equals the original without it, and the migrated weather has last_draw_day -1 and no drawn day",
|
||||
"reason": "A13: a new game opening at 06:00 draws its first day (SPEC 4.1), and a pre-weather save has no draw and must not draw for the past (SPEC 5.2). Only the weather section can differ; every other system stays byte-equal."
|
||||
},
|
||||
{
|
||||
"suite": "tests/environment/test_runtime_weather.gd",
|
||||
"check": "future weather clock rejects atomically",
|
||||
"before": "the refused restore leaves runtime.create_snapshot() == original",
|
||||
"after": "the refused restore leaves runtime.create_snapshot() == the migrated snapshot taken just before",
|
||||
"reason": "The state before this refusal is the migrated state of the previous check, which now differs from the original only in the weather section; the check still proves the refusal is atomic."
|
||||
},
|
||||
{
|
||||
"suite": "tests/environment/test_weather_timeline.gd",
|
||||
"check": "malformed or contradictory state rejects atomically (unknown version case)",
|
||||
"before": "{\"version\": 2} is an unknown version",
|
||||
"after": "{\"version\": 3} is an unknown version",
|
||||
"reason": "Version 2 is now the current weather snapshot (natural state and rain holds, SPEC 5.1); version 1 is read as the legacy format (SPEC 5.2)."
|
||||
},
|
||||
{
|
||||
"suite": "tests/environment/test_weather_recovery.gd",
|
||||
"check": "disk-restored recovery occurs once at scheduled minute",
|
||||
"before": "after the recovery the pending weather list is empty",
|
||||
"after": "after the recovery the pending weather list equals only the opening day's natural draw (rain 16:30-22:30 for seed 424242)",
|
||||
"reason": "A13: the opening day's natural draw schedules its own start and return to clear (SPEC 4.1); the manually scheduled storm and recovery are still consumed exactly once."
|
||||
}
|
||||
],
|
||||
"unchanged_related_suites_passing_locally": [
|
||||
"tests/mvp/test_runtime_operations_integration.gd",
|
||||
"tests/s2/test_runtime_operations_cycle.gd",
|
||||
"tests/s2/test_runtime_mana_reserve.gd",
|
||||
"tests/s2/test_mana_reserve_cycle.gd",
|
||||
"tests/s3/test_guest_service.gd",
|
||||
"tests/s6/test_runtime_save_lifecycle.gd",
|
||||
"tests/s6/test_runtime_snapshot_serialization.gd",
|
||||
"tests/ui/test_main_dashboard.gd"
|
||||
],
|
||||
"local_run_scope": "diagnostic direct runs on a scratch stage; the formal evidence is the submitted S native collection, the registered validation and the final full roster"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# P07-01 実装の記録(実 Opus の実装担当、2026-10-24)
|
||||
|
||||
- 仕様: 同じフォルダの `SPEC.md`(固定の commit `4654789`、Runner の `spec_sha256` `2626d70b…7956`)。決定 A13(`PROVENANCE.json`)。
|
||||
- この記録は、仕様の §4・§5 を実装した場所と、実装担当として選んだ詳細、開示を書く。人の新しい承認は主張しない。
|
||||
|
||||
## 1. 実装した場所
|
||||
|
||||
- `scripts/environment/natural_weather_policy.gd`(新しい): A13 の値(確率の境 60・85・95、続く時間 360・240・120 分、開始の 29 個の 30 分刻み、雨の来客 50%、雨の延長の上限 240 分、霧のマナ ×1.2・性欲 ×1.5)と、1日1回の抽選の手順。天候の仕組みは確率を持たない方針のままにするため分けた。
|
||||
- `scripts/environment/weather_timeline.gd`(VERSION 2): 自然天候の状態(最後に抽選した日、その日の抽選)と雨の延長の記録を持つ。予定の規則(同じ分の別の種類・過去の時刻・上限)は変えない。その日の抽選の当てはめ(`apply_draw`)は、その日の 06:00 にだけ、1回だけ受け付ける。予定と重なれば `SCHEDULE_CONFLICT` で拒む。VERSION 1 の保存は、過去の抽選をさかのぼらない形で読む。
|
||||
- `autoload/simulation_runtime.gd`:
|
||||
- 系列 `weather` を足した。
|
||||
- 06:00 の抽選と出来事 `WEATHER_FORECAST_DRAWN`。新しいゲームが 06:00 に始まる時は、初期化の最後に開始の日を抽選する。
|
||||
- 来客の受付の境界は、雨の割合(`arrival_percent`)を渡す。
|
||||
- 帰りの判定は、雨の延長(`_hold_guest_for_rain`)を通す。
|
||||
- マナの境界は、霧の産出を渡す。
|
||||
- 客の生理の文脈に `mana_fog` を足した。
|
||||
- 予報を含む `get_weather_report`。
|
||||
- 復元では、延長の記録と客の予定を照合し、旧保存の系列を `world_seed` から作る。
|
||||
- `scripts/operations/operations_coordinator.gd`: 来客の計画を2段にした(行ごとの受け入れと持ち越しを決め、雨なら合計を floor(×50/100) まで後ろの行から減らし、減らした分は持ち越さない)。雨でない時の計画と客の ID は今と同じ。
|
||||
- `scripts/mana/mana_reserve_cycle.gd`: 境界の世界樹の産出を引数で受ける(既定 20、霧 24)。炉の受け入れの判定と `plan` も霧の量(24・120)を受ける。保存の照合は産出 20 と 24 だけを許す。上限 300(A06)は変えない。
|
||||
- `scripts/guest/guest_service.gd`: 文脈の真偽値 `mana_fog` を検査し、成人確認済みの客の正の性欲の増分 n を n + floor(n / 2)(= floor(1.5n))にする。
|
||||
- `scenes/main/main.gd`: 上部の天候の名前の表示はそのままにし、隣に予報(`WeatherForecastLabel`、「次: 晴れ 12:00」「次: 予定なし」)を足した。天候の名前の案内(tooltip)に効果の案内を出す。
|
||||
|
||||
## 2. 実装担当として選んだ詳細(SPEC §2 のとおり人の承認ではない)
|
||||
|
||||
- 抽選の乱数: 0〜99 の整数を1つ、晴れ以外は開始の枠(0〜28)を1つ引く。
|
||||
- 日の数え方: 絶対分 ÷ 1440 の切り捨て。06:00 ちょうどは、まだ抽選していない日として数える(新しいゲームの開始の日のため)。旧保存の移行では、06:00 を過ぎていればその日は抽選済みとして数える。
|
||||
- 当てはめに失敗した抽選(手で置いた予定と同じ分になる時など)は、系列を進めたまま `applied: false` と理由を出来事に残し、その日の予定は変えない。次の日の 06:00 には抽選する。
|
||||
- 雨の延長は、帰りの判定(客が「決める」状態の時)で行う。延長の後の時刻には天候に関わらず今の規則で帰る。帰った客の記録は、天候の境界で片づける。
|
||||
- 霧の性欲は、その分の正の増分だけに掛ける(減少と、成人確認の無い客は変えない)。
|
||||
|
||||
## 3. 開示
|
||||
|
||||
- マナ炉の施設と産出の経路は今のゲームに無い(P06 の未決定)。霧の炉の ×1.2(120)は、マナの貯蔵の規則(受け入れの判定と `plan`)にだけ入り、通常の営業で観測できるのは世界樹の産出(20→24)だけ。
|
||||
- 今の客の種類で性欲が増えるのは、成人確認済みの客(`adventurer` など)に Runtime が渡す昼 3・夜 7 だけなので、霧の効果は昼 4・夜 10 として表れる。
|
||||
- 予報はその日の予定だけを出す(翌日は 06:00 に決まる)。UI は今の部品のままで、完成の見た目ではない。Windows の実機は後。
|
||||
- 性能の調整はしていない。横断条件の性能の回帰は、最終の S の全 roster の既存の性能の suite の結果で見る。
|
||||
- 新しい出来事 `WEATHER_FORECAST_DRAWN` が毎日 1 件増える。出来事の番号は、その分だけ後ろへずれる。
|
||||
|
||||
## 4. 確かめ(局所。正式の証拠ではない)
|
||||
|
||||
- 既存の天候の 3 本(期待値の変更は `EXPECTATION_CHANGES.json`)、関係する既存の 8 本、新しい suite(88 checks)、再開の書き手(28 checks)と読み手(47 checks、別の process)が、scratch の stage で全て合格した。正式の証拠は、提出の S の native の収集、登録の validation、最終の S の全 roster。
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"task": "P07-01-WX",
|
||||
"spec": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/SPEC.md",
|
||||
"rule": "SPEC WX8: every changed expectation of an existing suite is listed with the value before and after and the reason. The four changes carried from the original task stay listed in ../EXPECTATION_CHANGES.json (byte for byte); this file lists the changes made in P07-01-WX.",
|
||||
"changes": [
|
||||
{
|
||||
"suite": "tests/business/test_required_kitchen.gd",
|
||||
"check": "whole typed loaded state unchanged except complete identity/saveID-bound IO1: legacy14-load (and the byte-identical-copy legacy migration control, which uses the same mapped expectation)",
|
||||
"before": "_approved_runtime_migrations maps A06 (mana reserve fresh start), P14-03 (elevators v1 to v2) and P05-02 A12 (empty fund accounts) into the old checkpoint before the whole typed comparison",
|
||||
"after": "it also maps the P07-01 A13 migration, each part checked against its own contract before it is mapped: the weather snapshot v1 to v2 gains only the natural state (last_draw_day counts every day whose 06:00 has passed, today empty) and empty rain_holds, with minute, kind, since and pending unchanged; rng_streams gains only the weather stream from the world seed, every other stream byte-identical",
|
||||
"reason": "A13 (SPEC section 5.2) migrates a save from before natural weather on load. The genuine legacy14 fixture is such a save, so its loaded state differs from the old checkpoint exactly in these two fields. Found by the S c501360 full roster (phase 003, 163 checks, 2 failures) and the independent review finding 1."
|
||||
}
|
||||
],
|
||||
"found_by": {
|
||||
"full_roster": "FULL_ROSTER_C501360_FAILURE_R01.json",
|
||||
"review": "p07-01-wx-independent-review-c501360-r01/DESKTOP_RESULT.json (sha256 aea0cc4f2c00a6a093ca256154c62812a4d83749d075c6a4a9d36c1270026da7)"
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
# P07-01-WX 実装の記録(実 Opus の実装担当、2026-10-24)
|
||||
|
||||
- 仕様: 同じフォルダの `SPEC.md`(main `7ac2224` に固定。Runner の `spec_sha256` は `f1cd4262…fbd783`)。出典は `PROVENANCE.json`。
|
||||
- 原本への加算は 0。原 P07-01 の F1〜F8・炉 120・`AR.weather.fog_magic`・原 88 の分母は未達のまま。
|
||||
- 管理の記録は `artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/` にある(git の外)。
|
||||
|
||||
## 1. 持ち越し(SPEC §3)
|
||||
|
||||
- 原 task の提出 S `dfacc83` の差分 24 ファイルを、`CARRY_OVER_DFACC83.patch`(SHA-256 `a880ea89…58f9`)で W に1回だけ当てた。
|
||||
- 当てた後、24 ファイル全部が manifest(`CARRY_OVER_DFACC83.json`)の bytes・SHA-256 と、S `dfacc83` の blob の両方に一致した(`CARRY_OVER_APPLY_R01.json`)。
|
||||
- 1回目は sandbox が W への書き込みを拒み、何も当たらなかった(W は clean のまま)。その記録を残してから当て直した。
|
||||
- 製品と試験は持ち越しのまま。変えたのは下の照合の道具だけ。
|
||||
|
||||
## 2. 照合の直し(`tools/run_p07_weather_tests.py` の `verify`)
|
||||
|
||||
- 原因:
|
||||
- S `dfacc83` の native の収集では、14 本の suite が全て PASS した。その後の照合が「exact controlled Guard/proof environment」で止まった。
|
||||
- 照合は `ordinary_cleanup_prepared` の suite の環境しか期待していなかった。`old_bounded_teardown` の suite(`tests/environment/test_p07_weather_runtime.gd` と `tests/ui/test_main_dashboard.gd`)には、共通の収集が `THE_TREE_AUDIO_TEARDOWN_PROOF_DIR` を渡しているのに、それを期待していなかった。
|
||||
- 直し(`VERIFY_OLD_BOUNDED_FIX_DRAFT_R01.md` のとおり):
|
||||
1. 段ごとの分類(ordinary・old_bounded)を、共通の収集(`tools/run_all_tests.py`)と同じ `phase_policy` から決める。
|
||||
2. 期待する環境:
|
||||
- ordinary の時は今のまま(`<段>-teardown/owned-audio` と `control.json`)。
|
||||
- old_bounded の時は `<段>-teardown`(共通の収集と同じ)。
|
||||
- どちらでもない時は無し。
|
||||
3. old_bounded の suite は、共通の `teardown_gate` で証明を照合し直し、収集の記録(`old_bounded_proof`)と一致することを求める。
|
||||
4. 照合を build の場所の写しにかけた時のために、`teardown_gate` が読む証明の path を読み替える。方法は ordinary の照合(`owners.read`)と同じ。照合の道具の中で作った module の写しだけを変え、共通の gate は変えていない。
|
||||
5. ordinary の証明・old_bounded の証明・`<段>-teardown` のファイルが、分類された suite にだけあることを求める(import と parse の段を含む)。
|
||||
- 消した検査は無い。任意の環境は許さない。
|
||||
|
||||
## 3. 訂正
|
||||
|
||||
- 持ち越した `../IMPLEMENTATION_NOTES.md` §3 の「(P06 の未決定)」は誤り。
|
||||
- 炉の産出を通常の営業で示すのに要るものは、次の3つ(SPEC §6):
|
||||
- P13 の炉の施設
|
||||
- D04 のユニットの消費
|
||||
- A06 の別承認
|
||||
- 持ち越しのファイルは bytes のまま残し、訂正はここに書く。
|
||||
|
||||
## 4. 確かめ(局所。正式の証拠ではない)
|
||||
|
||||
- 直した判定を、旧 S の native の生の出力(読むだけ)に当てた(`VERIFIER_FIX_LOCAL_CHECK_R01.json`)。照合の全体は、受領が旧い道具の bytes を固定しているので、旧い出力にはかけられない。
|
||||
- 16 段の環境と分類が記録と一致した。
|
||||
- bounded の 2 本の証明が、元の場所と別の場所の写しの両方で記録と一致した。
|
||||
- 直す前の期待は、元の失敗と同じ 000・011 の段で外れる。
|
||||
- 読み替えの無い写しと、1 byte 変えた写しは拒まれる。
|
||||
- 直した道具の `--prepare-only` は exit 0(engine 0)。
|
||||
- 製品は S `dfacc83` と同じ bytes なので、engine の局所の再実行はしていない。
|
||||
- 正式の証拠は、新しい S の native の収集、登録の validation、全 roster、独立レビュー。旧 S の結果は流用しない。
|
||||
|
||||
## 5. 開示
|
||||
|
||||
- 炉 120 と `AR.weather.fog_magic` は受け入れない(SPEC §6)。
|
||||
- 他の開示は、持ち越した `../IMPLEMENTATION_NOTES.md` §2・§3(P06 の句を除く)と `../EXPECTATION_CHANGES.json` のとおり。
|
||||
- 出来事の一覧の表示: 毎日 06:00 の出来事 `WEATHER_FORECAST_DRAWN` には、`scripts/ui/event_log_dialog.gd` の `EVENT_NAMES` に日本語の名前が無い。そのため、プレイヤーが見る出来事の一覧では「未識別の記録」と出る。
|
||||
- このファイルは契約の `allowed_paths` の外なので、この作業では直さない。
|
||||
- 名前を足す作業は、別の単位として提案する。
|
||||
- 照合の ordinary の分岐: 狭い roster に `ordinary_cleanup_prepared` の suite が無い。そのため、照合の ordinary の分岐は、この validation では実地には動かない。
|
||||
- 実地に動くのは、old_bounded(000・011)と、後片付けの無い段だけ。
|
||||
- ordinary の分岐は、共通の収集と同じ環境と証明の照合を、コードの上で求めている。
|
||||
|
||||
## 6. 直し 1(S `c5013601` の独立レビューと全 roster の後。repair 1)
|
||||
|
||||
- 契機:
|
||||
- S `c5013601` の全 roster が、段 003 の `tests/business/test_required_kitchen.gd` で止まった(`FULL_ROSTER_C501360_FAILURE_R01.json`)。
|
||||
- 独立レビュー(`local_21295ab0`)は changes_requested で、high 1・medium 1・low 4。
|
||||
- 管理の記録は `COMMANDER_REPAIR_PLAN_P07_01_WX_R01.md`。
|
||||
- 直したもの:
|
||||
1. high: kitchen の試験の `_approved_runtime_migrations` に、A13 の移行を足した。2つの部分を、それぞれの契約で照合してから写す(`./EXPECTATION_CHANGES.json`)。
|
||||
- 天候 v1→v2 では、`natural` と空の `rain_holds` だけが足される。
|
||||
- `rng_streams` には、`world_seed` から作った `weather` だけが足される。
|
||||
2. medium: 再開の台本に霧の場面を足した(`tests/helpers/p07_weather_restart_fixture.gd`、writer、reader)。
|
||||
- seed 1592(0 日目 06:00〜10:00 がマナ霧)を使う。
|
||||
- 06:58 と 09:58 に成人確認済みの客が着く。この世界には施設が無いので、客は数分で帰る。そのため、保存の時刻に館内に居るように着く時刻を選んだ。
|
||||
- 保存は 419・420・421・599・600・601 分。
|
||||
- 比べる状態に、客の `needs`(性欲)と、マナの貯蔵の snapshot を足した。
|
||||
- writer は、straight の進行で次を確かめる。
|
||||
- 成人の客の性欲が、霧の間は1分に4、霧の後は3上がる。
|
||||
- 10 分ごとのマナの境界が、霧の間は 24、霧の後は 20。
|
||||
- writer は、同じ process で各保存ファイルを読み直す。読んだ直後と1分後、最後の状態が、straight と bytes で一致することを確かめる。
|
||||
- reader は、別の process で同じことを確かめる。
|
||||
- これで、霧の産出と性欲の効果が二重に効かないことを示す。
|
||||
3. low(試験の不足):
|
||||
- (a) 霧の間に帰りの時刻になった客は延びない。
|
||||
- (b) 延びた客は、10:02 の決定(`_advance_guest_ai_decisions`)で PLANNED_EXIT で帰り始める。施設の無いこの世界では、客は延長の前に次の決定で施設が無いので帰る。そのため writer の雨の場面では、hold の記録の後も今の規則どおり(`NO_SUITABLE_FACILITY`)帰ることを、厳密に確かめた。元の「or has left since」は外した。
|
||||
- (c) 上部の表示で、嵐とマナ霧の名前・予報・効果の案内を確かめる。霧の案内が炉を主張しないことも確かめる。
|
||||
4. low(§5.3 の細部): 製品の `scripts/environment/natural_weather_policy.gd` に `is_consistent_draw` を足した。
|
||||
- 保存されたその日の抽選について、roll と種類、slot と開始、終わりと続く時間の対応を照合する。
|
||||
- Runtime の復元(`autoload/simulation_runtime.gd`)は、矛盾した記録を拒否し、状態を保つ。
|
||||
- 拒否の試験を3つ足した。
|
||||
5. 開示: 上の §5 の2つ。
|
||||
- 比べる状態の `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`)。
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
# P07-01-WX 正式受入記録(補助の作業。原本への加算 0)
|
||||
|
||||
- P07-01-WX は、原 P07-01 の補助の作業である。対象は、仕上がった天候の機能(自然天候・雨・嵐・世界樹と成人客のマナ霧・上部の表示と予報・保存と移行)。
|
||||
- 次の3つをそろえて、正式に完了した。
|
||||
- 受入 8 件(WX1〜WX8)についての、別の fresh な Desktop Opus 5.5 の独立レビューの PASS(高・中 0、low 2)
|
||||
- 通常の validation・review・integration・final の全 PASS
|
||||
- 実 DB と traceability の結合
|
||||
- S `db87270` → Q `9b5b09b`。
|
||||
- 原本への加算は 0 で、正式の数は 24/88・4/30 のまま。部分点なし。
|
||||
- 原 P07-01 は未達のまま。霧の境界のマナ炉の産出 120 と `AR.weather.fog_magic` が残っている。
|
||||
|
||||
| 名前 | commit | 備考 |
|
||||
|---|---|---|
|
||||
| 提出 S | `db87270972c9b243e3a1fccdf2c1fa96975dffc1` | 修正 2 |
|
||||
| 統合・最終検証 Q | `9b5b09b35e888e5ac1a49174b8a7f73e51c502d2` | Q の tree は S の tree と同じ(`86fa8fdd…`) |
|
||||
| 基準 M | `7ac2224e4705d06aab28803592a68b31d60841ed` | |
|
||||
|
||||
- 正式証拠: `WX_FORMAL_COMPLETION_9B5B09B.json`
|
||||
- 独立レビューの結果(実結果の bytes の写し): `WX_INDEPENDENT_REVIEW_DB87270.json`(SHA256 `1fd2c8bbe0f9d6b165d5f993e86002b94ead97674c27ed9270a223c929c1aaca`)
|
||||
|
||||
## 位置づけと原本の条件
|
||||
|
||||
- この補助の作業は、Root の作業の範囲の分割(提案 B、`ROOT_AUXILIARY_SCOPE_ORDER_R01.json`)である。
|
||||
- 人の新しいゲームの仕様の承認ではない。
|
||||
- 原 P07-01 の受入を消したものでもない。
|
||||
- 人の決定 A13(天候の初期仕様、答え「この初期仕様で進める」)は、`../PROVENANCE.json` のとおり。
|
||||
- 原 work: 雨の来客減・滞在延長、マナ霧、自然変化を実営業へ接続する。
|
||||
- 原 acceptance: 天候切替直前後、途中保存、自然来客の差を同条件で説明できる。
|
||||
- 原の横断条件(P07、7 項目):
|
||||
- 原本条件とUI操作が一致
|
||||
- Runtimeの実利用・会計・行動へ接続
|
||||
- 途中保存・再起動で再現
|
||||
- 異常/境界/中断を検証
|
||||
- 関連回帰PASS
|
||||
- 要件別ログとソースハッシュを保存
|
||||
- 既存代表負荷の性能回帰を計測
|
||||
- 受入 8 件(WX1〜WX8)は `SPEC.md` §4 と、登録の `acceptance_baseline.json` の本文のとおり。原 F1〜F8 との対応は `SPEC.md` §5 にある。
|
||||
|
||||
## 4つの通常のゲート(Runner の the-tree-p07-01-wx、task P07-01-WX、completed、修正 2)
|
||||
|
||||
| ゲート | commit | Runner の証跡 | 結果 |
|
||||
|---|---|---|---|
|
||||
| validation | S | evidence 5 | `p07-weather` exit 0、verified true、14 本(16 段)・1,159 checks、engines_started 0 |
|
||||
| review | S | evidence 6 | 実結果 `1fd2c8bb…`、pass、WX1〜WX8、low 2 |
|
||||
| integration | Q | evidence 7 | `p07-weather` exit 0、verified true、engines_started 0 |
|
||||
| final | Q | evidence 8 | `9145ecb2…-final.json` SHA `61d87d35…`、verified true、14 本・1,159 checks・parse 350、engines_started 0 |
|
||||
|
||||
- owner の承認:
|
||||
- Root が、main の統合と push についての既存の人の許可を、正確な Q `9b5b09b` と main_base `7ac2224` に正直に当てはめた。新しい人の発言は作っていない。
|
||||
- 記録は、Runner の approvals の行と `ROOT_OWNER_APPROVAL_9B5B09B_R01.json`。
|
||||
- integrate の job は `d166ed1445074b71bdae495edf5a6880`。
|
||||
- 測定は全て S で行った。
|
||||
- native: 14 本・1,159 checks
|
||||
- S の全 roster: 198 本・22,959 checks。既存の代表負荷の性能の suite 10 本を含む。
|
||||
- Q での integration と final について:
|
||||
- S で測った証拠を、中身が同じ Q の新しい checkout で、中身ベースの verifier が照合したものである。
|
||||
- Q で測ったとは書かない。
|
||||
|
||||
## 独立レビュー(S db87270)
|
||||
|
||||
- レビュー担当: 別の fresh な Claude Desktop Opus 5.5 の session(`local_fc6760c6`)。実装担当とも、前の S の2回のレビューとも別である。
|
||||
- 凍結の入力の SHA を確かめ、読むだけで判定した。凍結の入力は packet・schema・材料 7,174 ファイル。
|
||||
- 判定の範囲:
|
||||
- 受入 WX1〜WX8 の全部
|
||||
- 原の横断条件の7項目の全部
|
||||
- U1(未達の開示)と U2(証拠の適用)
|
||||
- 前の2つのレビューの findings の解消
|
||||
- 配送の前に、会話の記録の全体を確かめた。
|
||||
- tool の呼び出しは 77 件(重複を除く)。
|
||||
- 書き込みは結果のファイルだけ(作成1回と、同じファイルの書き直し3回)。
|
||||
- 補助のファイルは、その session の scratchpad だけにあった。
|
||||
- engine・試験・Runner のコマンド・git の書き換えは無い。
|
||||
- low 2 件。原文は独立レビューの結果のファイルにあり、どちらも試験の不足で、製品は正しいと判定された。
|
||||
1. 延長が二重に効かないことと延長の時刻の帰りを、まだ判断中の延長された客で動かして確かめていない。
|
||||
- 施設の無い試験の世界では、延びた客は延長の時刻より前に帰る。
|
||||
- 延長の時刻の帰りは、決定の処理を直接呼んで確かめている。
|
||||
- この点は `IMPLEMENTATION_NOTES.md` §7 で開示した。
|
||||
- 後の提案: 施設を使う世界で、延長を持つ保存を読み直して確かめる。または check の文言を実態に合わせる。
|
||||
2. 移行した save で、「読み込みの後の最初の 06:00 から抽選を始める」ことを直接確かめていない。
|
||||
- 製品の経路は、新しいゲームの毎日の抽選と同じで、そちらは確かめている。
|
||||
- 後の提案: 移行の試験で、次の 06:00 まで進めて抽選が1回出ることを確かめる。
|
||||
|
||||
## 修正の経過と保持した失敗
|
||||
|
||||
1. 原 P07-01 の S `dfacc83`
|
||||
- native では 14 本が PASS した。
|
||||
- しかし、写した照合の道具が old_bounded の後片付けの環境を知らず、exit 1 になった(`NATIVE_DFACC83_FAILURE_R01.json`)。
|
||||
- WX で照合を直した(ordinary と old_bounded の両方を厳密に照合する)。
|
||||
2. S `c5013601`
|
||||
- 全 roster が `test_required_kitchen.gd` で止まった。旧 save の比べが A13 の移行を知らなかったため(`FULL_ROSTER_C501360_FAILURE_R01.json`)。
|
||||
- レビューは changes_requested(high 1・medium 1・low 4)。
|
||||
- 修正 1 で、kitchen の試験への A13 の写し、霧の再開、low の試験、保存した抽選の照合、開示を入れた。
|
||||
3. S `f99c881b`
|
||||
- 全 roster は合格した。
|
||||
- レビューは changes_requested(medium 1・low 1)。別の process で雨の延長の保存が確かめられていなかった。
|
||||
- 診断で、製品の不具合も見つかった。延長の記録が客と一緒に片づかず、客が居なくなった分の保存が読めなかった(`RESTORE_FAILED`)。
|
||||
- 修正 2 で、記録を客と一緒に片づけるようにし、再開の雨の場面に 06:03・06:04 の保存を足した。
|
||||
- 全ての失敗の生の出力、診断の記録、前のレビューの結果は保持した。新しい S の証拠に流用していない。
|
||||
|
||||
## 未達として保つもの(主張しない)
|
||||
|
||||
- 原 P07-01(F1〜F8 の whole-unit の受入)は未達のまま。
|
||||
- 霧の境界のマナ炉の産出 120 と `AR.weather.fog_magic` が残っている。
|
||||
- 満たすには、P13 の炉の施設、D04 のユニットの消費、A06 の別承認が要る(`SPEC.md` §6)。
|
||||
- 出来事の一覧では、毎日の `WEATHER_FORECAST_DRAWN` が「未識別の記録」と出る。
|
||||
- `scripts/ui/event_log_dialog.gd` は範囲の外なので、別の単位として提案する。
|
||||
- 照合の ordinary の分岐は、狭い roster では実地に動かない。
|
||||
- 予報はその日の分だけを出す。
|
||||
- UI は今の部品のままで、完成の見た目ではない。
|
||||
- 同じ Windows の実機の受入、長い通常のプレイ、全ゲームの最終の受入は残っている。
|
||||
+744
@@ -0,0 +1,744 @@
|
||||
{
|
||||
"at_jst": "2026-10-24T10:43:31.780899+09:00",
|
||||
"auxiliary_unit": "P07-01-WX",
|
||||
"original_unit": "P07-01",
|
||||
"project": "the-tree-p07-01-wx",
|
||||
"task": "P07-01-WX",
|
||||
"state": "completed",
|
||||
"S": "db87270972c9b243e3a1fccdf2c1fa96975dffc1",
|
||||
"Q": "9b5b09b35e888e5ac1a49174b8a7f73e51c502d2",
|
||||
"M": "7ac2224e4705d06aab28803592a68b31d60841ed",
|
||||
"repairs": 2,
|
||||
"Q_tree_equals_S_tree": "86fa8fdd300f9146c6afaac9fddd2405a433718c",
|
||||
"all4_normal_gates_passed": true,
|
||||
"scope": "Auxiliary successor of the original P07-01 by Root work sequencing (option B, ROOT_AUXILIARY_SCOPE_ORDER_R01.json): the finished natural weather, rain arrivals and stay, storm, the world tree fog yield 24 and the adult fog intimacy, the HUD forecast and save/migration/rejection. Not a new human gameplay approval and not a deletion of the original P07-01 acceptance.",
|
||||
"original_work": "雨の来客減・滞在延長、マナ霧、自然変化を実営業へ接続する。",
|
||||
"original_acceptance": "天候切替直前後、途中保存、自然来客の差を同条件で説明できる。",
|
||||
"original_package_work": "雨/嵐/マナ霧、流行ニュース、ゾーンの採光/遮音/隠蔽、景観/清潔/騒音を段階導入。係数と乱数系列を保存しHUDと予測へ表示。",
|
||||
"original_package_acceptance": "嵐で新規入館0、雨で来客減/滞在延長。天候変化の瞬間と保存再開が一致。環境変更に応じて満足度と対象売上が変わる。",
|
||||
"original_cross_cutting_acceptance": [
|
||||
"原本条件とUI操作が一致",
|
||||
"Runtimeの実利用・会計・行動へ接続",
|
||||
"途中保存・再起動で再現",
|
||||
"異常/境界/中断を検証",
|
||||
"関連回帰PASS",
|
||||
"要件別ログとソースハッシュを保存",
|
||||
"既存代表負荷の性能回帰を計測"
|
||||
],
|
||||
"acceptance_ids": [
|
||||
{
|
||||
"id": "P07-01-WX1",
|
||||
"text": "自然天候: 毎日 06:00 に1回だけ、原 SPEC §4.1 の確率・開始の時刻・続く時間で天候が自然に予定され、同じ world_seed と同じ操作で同じ天候の列になる。新しいゲームの開始の日も抽選され、旧 save は原 SPEC §5.2 のとおり移行する。"
|
||||
},
|
||||
{
|
||||
"id": "P07-01-WX2",
|
||||
"text": "雨の来客: 雨の間の来客の計画の新規の人数が、同じ条件の晴れの半分(切り捨て)になり、嵐の 0 と定員・マナの上限を壊さない。"
|
||||
},
|
||||
{
|
||||
"id": "P07-01-WX3",
|
||||
"text": "雨の滞在: 雨の間に帰りの時刻になった客が、雨の終わりか元の予定 + 240 分の早い方まで1回だけ残り、その後は今の規則で帰る。嵐と霧では延びない。"
|
||||
},
|
||||
{
|
||||
"id": "P07-01-WX4",
|
||||
"text": "嵐: 自然の嵐を含め、嵐の間の新規入館が 0。"
|
||||
},
|
||||
{
|
||||
"id": "P07-01-WX5",
|
||||
"text": "マナ霧(炉を除く): 霧の間のマナの境界で世界樹 24(上限 300 のまま)、成人確認済みの客の性欲の増加が floor(×1.5) になり、霧の外と成人確認の無い客は変わらない。マナ炉の産出 120 はこの補助の作業では受け入れず、通常の営業での達成を主張しない(炉の産出の経路が無いので、貯蔵の規則の単体の確かめは開示として扱う)。"
|
||||
},
|
||||
{
|
||||
"id": "P07-01-WX6",
|
||||
"text": "表示と予報: ゲームの本物の Runtime と Main の上部の表示で、今の天候・次の切り替え(種類と時刻)・効果の案内が Runtime の値と一致する。"
|
||||
},
|
||||
{
|
||||
"id": "P07-01-WX7",
|
||||
"text": "保存と再開: 天候の切り替えの直前・直後・途中の保存を、同じ process と別の process で読み、天候・予定・系列・来客・滞在の延長・霧の効果が同じになり、二重に効かない。旧 save は原 SPEC §5.2 で読め、不正な状態は原 SPEC §5.3 で拒否して状態を保つ。"
|
||||
},
|
||||
{
|
||||
"id": "P07-01-WX8",
|
||||
"text": "回帰と照合: 関連の回帰(狭い roster と再開の2段)と後継の S の全 roster(既存の代表負荷の性能の suite を含む)が PASS。登録の照合が、`ordinary_cleanup_prepared` と `old_bounded_teardown` の両方の suite について、制御の環境と後片付けの証明・hash を厳密に照合する。既存の試験の期待値の変更は `EXPECTATION_CHANGES.json` に変更前後と根拠を一覧にする。元の失敗(S `dfacc83` の native の照合の失敗を含む)を保存し、未解決の試験を除いて PASS にしない。"
|
||||
}
|
||||
],
|
||||
"acceptance_baseline": {
|
||||
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/acceptance_baseline.json",
|
||||
"bytes": 3400,
|
||||
"sha256": "5207d0ac39538c7e639e04c66d73e6a316de17bf872f42eed718d3e0fc7bde7a"
|
||||
},
|
||||
"spec": {
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/SPEC.md",
|
||||
"bytes": 11023,
|
||||
"sha256": "d9bdb25710faf18f306107328e3277fe621ef060cd6e93f580889672f899e0ec",
|
||||
"contract_spec_sha256": "f1cd426202c605cc81727649283900438c6adb5d473670f5e9e6b01858fbd783"
|
||||
},
|
||||
"provenance": {
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/PROVENANCE.json",
|
||||
"bytes": 5834,
|
||||
"sha256": "84dc1d4bece6a06e95d4230eae050bd004b2f22a466b9bb595c2ee5d4f56cbd8"
|
||||
},
|
||||
"original_spec": {
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/SPEC.md",
|
||||
"bytes": 13238,
|
||||
"sha256": "7c715a1a4576a37e40f354d2e3c929f5be95c581713c7500984d2d073fe587dd"
|
||||
},
|
||||
"original_provenance_A13": {
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/PROVENANCE.json",
|
||||
"bytes": 3164,
|
||||
"sha256": "d9852735789d50ee57f6a35268d1fd9e30d4016082b219119def902abe20091c"
|
||||
},
|
||||
"unit_disclosures": [
|
||||
{
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/IMPLEMENTATION_NOTES.md",
|
||||
"bytes": 12625,
|
||||
"sha256": "194979ada4fd195ef5b29824dc6164123a9bed96609bb2e704ccaac397bd892e"
|
||||
},
|
||||
{
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/EXPECTATION_CHANGES.json",
|
||||
"bytes": 1871,
|
||||
"sha256": "28d5b13b011562771a8772f36c8c9fc1024ad551259b375bbf70b9cbb6ade0c3"
|
||||
},
|
||||
{
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/IMPLEMENTATION_NOTES.md",
|
||||
"bytes": 5463,
|
||||
"sha256": "d6876514ed93bf1d0c4db1b51c9af7d416a7c80cd24406a4523fdcf51e436908"
|
||||
},
|
||||
{
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/EXPECTATION_CHANGES.json",
|
||||
"bytes": 3131,
|
||||
"sha256": "51cff9ad1c1e8ef694fd1940fd1c242559ae981522fe8e1c48c87e5770d4a5ef"
|
||||
}
|
||||
],
|
||||
"runner_task": {
|
||||
"id": "P07-01-WX",
|
||||
"state": "completed",
|
||||
"submitted": "db87270972c9b243e3a1fccdf2c1fa96975dffc1",
|
||||
"candidate": "9b5b09b35e888e5ac1a49174b8a7f73e51c502d2",
|
||||
"main_base": "7ac2224e4705d06aab28803592a68b31d60841ed",
|
||||
"reason": null,
|
||||
"repairs": 2
|
||||
},
|
||||
"runner_events": [
|
||||
{
|
||||
"id": 1,
|
||||
"event": "registered",
|
||||
"created": 1792784865.483346
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"event": "owner_delegated_implementer",
|
||||
"created": 1792785023.35983
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"event": "commander_proposal",
|
||||
"created": 1792785130.728936
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"event": "submitted",
|
||||
"created": 1792785888.967114
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"event": "validating",
|
||||
"created": 1792787216.747516
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"event": "review_pending",
|
||||
"created": 1792787222.722386
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"event": "owner_prepared_actual_desktop_independent_review_input",
|
||||
"created": 1792790128.160841
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"event": "review_pending",
|
||||
"created": 1792791990.31691
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"event": "owner_actual_independent_Desktop_review_output",
|
||||
"created": 1792791999.8852632
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"event": "repair_pending",
|
||||
"created": 1792791999.887711
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"event": "commander_proposal",
|
||||
"created": 1792792080.606424
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"event": "repair_ready",
|
||||
"created": 1792792080.609395
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"event": "submitted",
|
||||
"created": 1792793303.897059
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"event": "validating",
|
||||
"created": 1792793918.329674
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"event": "review_pending",
|
||||
"created": 1792793925.8383539
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"event": "owner_prepared_actual_desktop_independent_review_input",
|
||||
"created": 1792797378.184551
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"event": "review_pending",
|
||||
"created": 1792799568.977567
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"event": "owner_actual_independent_Desktop_review_output",
|
||||
"created": 1792799578.633156
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"event": "repair_pending",
|
||||
"created": 1792799578.636506
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"event": "commander_proposal",
|
||||
"created": 1792799615.441633
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"event": "repair_ready",
|
||||
"created": 1792799615.4451442
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"event": "submitted",
|
||||
"created": 1792799976.661932
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"event": "validating",
|
||||
"created": 1792800668.8961751
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"event": "review_pending",
|
||||
"created": 1792800675.885521
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"event": "owner_prepared_actual_desktop_independent_review_input",
|
||||
"created": 1792804081.5048692
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"event": "review_pending",
|
||||
"created": 1792805107.4343512
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"event": "owner_actual_independent_Desktop_review_output",
|
||||
"created": 1792805117.083816
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"event": "reviewed",
|
||||
"created": 1792805117.0873659
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"event": "approval_pending",
|
||||
"created": 1792805138.5482361
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"event": "approved",
|
||||
"created": 1792805852.1386409
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"event": "completed",
|
||||
"created": 1792805862.032322
|
||||
}
|
||||
],
|
||||
"normal_evidence_join": [
|
||||
{
|
||||
"id": 5,
|
||||
"task_id": "P07-01-WX",
|
||||
"kind": "validation",
|
||||
"commit_sha": "db87270972c9b243e3a1fccdf2c1fa96975dffc1",
|
||||
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/f60b5a77b01a40fbbd4cdec52d7b3419-validation.json",
|
||||
"passed": 1,
|
||||
"artifact_sha256": "ed62d295182c53bc661cfeaa578e69af96d1e89e1ef5e5959a666700a5f7518c",
|
||||
"checks": [
|
||||
{
|
||||
"id": "p07-weather",
|
||||
"exit_code": 0,
|
||||
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/b6dad51a630149839962f304dc73df52",
|
||||
"cwd": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p07-01-wx/P07-01-WX-validation-704968e9",
|
||||
"stdout": {
|
||||
"verified": true,
|
||||
"actual_suites": 14,
|
||||
"parse_checks": 350,
|
||||
"actual_checks": 1159,
|
||||
"engines_started": 0,
|
||||
"formal_acceptance": false,
|
||||
"original_unit_complete": false
|
||||
},
|
||||
"raw": {
|
||||
"command.json": {
|
||||
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/b6dad51a630149839962f304dc73df52/command.json",
|
||||
"bytes": 729,
|
||||
"sha256": "f64b75d0614d9dc38b77ee6635bdf656c69f6415a1e60dc714eb5e7fac7bb15c"
|
||||
},
|
||||
"stdout.log": {
|
||||
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/b6dad51a630149839962f304dc73df52/stdout.log",
|
||||
"bytes": 167,
|
||||
"sha256": "7784c06ca2e44363e79c585aac0a01d01aeac250d5dd8b81fe60eae3961e60e1"
|
||||
},
|
||||
"stderr.log": {
|
||||
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/b6dad51a630149839962f304dc73df52/stderr.log",
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"sandbox.sb": {
|
||||
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/b6dad51a630149839962f304dc73df52/sandbox.sb",
|
||||
"bytes": 1944,
|
||||
"sha256": "07b5dcb0f19dcfbf238a9f4b7d9876c2eebf3e8d1ed3dc87dfa1488d9d3dca79"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"task_id": "P07-01-WX",
|
||||
"kind": "review",
|
||||
"commit_sha": "db87270972c9b243e3a1fccdf2c1fa96975dffc1",
|
||||
"artifact": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/p07-01-wx-independent-review-db87270-r01/actual-independent-desktop-output/result.json",
|
||||
"passed": 1,
|
||||
"artifact_sha256": "1fd2c8bbe0f9d6b165d5f993e86002b94ead97674c27ed9270a223c929c1aaca",
|
||||
"checks": []
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"task_id": "P07-01-WX",
|
||||
"kind": "integration",
|
||||
"commit_sha": "9b5b09b35e888e5ac1a49174b8a7f73e51c502d2",
|
||||
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/ba5835b26b554b20a5bc7ddb5a5351af-integration.json",
|
||||
"passed": 1,
|
||||
"artifact_sha256": "848717653b817734a9a6a66f3ae40534dc991c4a60bbdea097df69e73256aabb",
|
||||
"checks": [
|
||||
{
|
||||
"id": "p07-weather",
|
||||
"exit_code": 0,
|
||||
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/e44512c3202e4ef4bd22f7a8f601792b",
|
||||
"cwd": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p07-01-wx/P07-01-WX-integration-ed82b987",
|
||||
"stdout": {
|
||||
"verified": true,
|
||||
"actual_suites": 14,
|
||||
"parse_checks": 350,
|
||||
"actual_checks": 1159,
|
||||
"engines_started": 0,
|
||||
"formal_acceptance": false,
|
||||
"original_unit_complete": false
|
||||
},
|
||||
"raw": {
|
||||
"command.json": {
|
||||
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/e44512c3202e4ef4bd22f7a8f601792b/command.json",
|
||||
"bytes": 730,
|
||||
"sha256": "c808e064d90d60dc84e96e6cf1072263ad233f5bfb97997e92e221019fcd764f"
|
||||
},
|
||||
"stdout.log": {
|
||||
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/e44512c3202e4ef4bd22f7a8f601792b/stdout.log",
|
||||
"bytes": 167,
|
||||
"sha256": "7784c06ca2e44363e79c585aac0a01d01aeac250d5dd8b81fe60eae3961e60e1"
|
||||
},
|
||||
"stderr.log": {
|
||||
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/e44512c3202e4ef4bd22f7a8f601792b/stderr.log",
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"sandbox.sb": {
|
||||
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/e44512c3202e4ef4bd22f7a8f601792b/sandbox.sb",
|
||||
"bytes": 1945,
|
||||
"sha256": "a3494c5bdbb499f7304f635fba7e561b9a85590480a1eb1c8cffc49049637746"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"task_id": "P07-01-WX",
|
||||
"kind": "final",
|
||||
"commit_sha": "9b5b09b35e888e5ac1a49174b8a7f73e51c502d2",
|
||||
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/9145ecb2c8ed4ac5b224c6609181ad88-final.json",
|
||||
"passed": 1,
|
||||
"artifact_sha256": "61d87d352a67d43ed3c52f290a657d5d0f2c7531267fac40133cf44d631b28d2",
|
||||
"checks": [
|
||||
{
|
||||
"id": "p07-weather",
|
||||
"exit_code": 0,
|
||||
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/b8a8642f84df46a5b9b1a66a7c272350",
|
||||
"cwd": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p07-01-wx/P07-01-WX-final-9b2565c9",
|
||||
"stdout": {
|
||||
"verified": true,
|
||||
"actual_suites": 14,
|
||||
"parse_checks": 350,
|
||||
"actual_checks": 1159,
|
||||
"engines_started": 0,
|
||||
"formal_acceptance": false,
|
||||
"original_unit_complete": false
|
||||
},
|
||||
"raw": {
|
||||
"command.json": {
|
||||
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/b8a8642f84df46a5b9b1a66a7c272350/command.json",
|
||||
"bytes": 736,
|
||||
"sha256": "23902a90ad03f64994a05874d69ba00b3de5e7b6224a8eaf16cad07556720c73"
|
||||
},
|
||||
"stdout.log": {
|
||||
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/b8a8642f84df46a5b9b1a66a7c272350/stdout.log",
|
||||
"bytes": 167,
|
||||
"sha256": "7784c06ca2e44363e79c585aac0a01d01aeac250d5dd8b81fe60eae3961e60e1"
|
||||
},
|
||||
"stderr.log": {
|
||||
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/b8a8642f84df46a5b9b1a66a7c272350/stderr.log",
|
||||
"bytes": 0,
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
"sandbox.sb": {
|
||||
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/b8a8642f84df46a5b9b1a66a7c272350/sandbox.sb",
|
||||
"bytes": 1939,
|
||||
"sha256": "2e408ff831d67ac2703239e6212f8db86f45839ee8d685cd3f491b4cb1da897e"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"earlier_evidence_retained": [
|
||||
{
|
||||
"id": 1,
|
||||
"kind": "validation",
|
||||
"commit_sha": "c50136017b686994c79bdb3ecf49a9df5a465052",
|
||||
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/10a64c693929436d9ea374f1a520c965-validation.json",
|
||||
"passed": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"kind": "review",
|
||||
"commit_sha": "c50136017b686994c79bdb3ecf49a9df5a465052",
|
||||
"artifact": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/p07-01-wx-independent-review-c501360-r01/actual-independent-desktop-output/result.json",
|
||||
"passed": 0
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"kind": "validation",
|
||||
"commit_sha": "f99c881bca0399fec7a75a8715265fbcf7ff5b4c",
|
||||
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/runs/d456a391f0334ef4b02dcaf3e80de182-validation.json",
|
||||
"passed": 1
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"kind": "review",
|
||||
"commit_sha": "f99c881bca0399fec7a75a8715265fbcf7ff5b4c",
|
||||
"artifact": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/p07-01-wx-independent-review-f99c881-r01/actual-independent-desktop-output/result.json",
|
||||
"passed": 0
|
||||
}
|
||||
],
|
||||
"approval": {
|
||||
"task_id": "P07-01-WX",
|
||||
"candidate": "9b5b09b35e888e5ac1a49174b8a7f73e51c502d2",
|
||||
"main_base": "7ac2224e4705d06aab28803592a68b31d60841ed",
|
||||
"owner": "Root/Codex applying existing human authorization for exact-candidate main integration and push; no new human utterance",
|
||||
"created": 1792805852.13765
|
||||
},
|
||||
"owner_records": [
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/ROOT_OWNER_APPROVAL_9B5B09B_R01.json",
|
||||
"bytes": 5946,
|
||||
"sha256": "9b8547780b75a4ecfa645fc63c1d5334e50e297fb8fc8034a1bde6ff362cb6b0"
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/ROOT_WX_COMPLETED_DB_JOIN_9B5B09B_R01.json",
|
||||
"bytes": 8582,
|
||||
"sha256": "aeef2ac361363b23938992f36d58de12714620fea669f9ed2672ddcef764fe1c"
|
||||
}
|
||||
],
|
||||
"integrate_job_id": "d166ed1445074b71bdae495edf5a6880",
|
||||
"traceability": {
|
||||
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p07-01-wx/traceability/P07-01-WX.json",
|
||||
"bytes": 4746,
|
||||
"sha256": "68964ef40bb6dd033974e9721d6572f6056d80e1300e4721d1735ed7c3daaffc"
|
||||
},
|
||||
"independent_reviewer": {
|
||||
"task_id": "P07-01-WX",
|
||||
"commit": "db87270972c9b243e3a1fccdf2c1fa96975dffc1",
|
||||
"session_id": "local_fc6760c6-9acd-4614-950e-632b91d53f54",
|
||||
"model": "claude-opus-5-5",
|
||||
"context": "fresh",
|
||||
"read_only": true,
|
||||
"read_only_meaning": "The governor read every unique tool call of the reviewer transcript (77 unique ids): Bash read files, git objects and packet data (cat, sed, grep, find, ls, shasum, cmp, diff, git diff/log/show/ls-tree, python3 -I reading JSON, git blobs and the handoff); Write created DESKTOP_RESULT.json and three Edit calls revised that same result file before delivery; no test, engine, Runner or git-mutating command ran and no project, source or evidence file was changed (the frozen material shows no new file). Outside the project the reviewer wrote helper files in its own session scratchpad only (packet.json, diff.txt and gitdiff.txt extracted for comparison, lstree.txt, parse.py and gvar.py, read-only decoders, and __pycache__), as the brief allowed. It also read the previous review results, read-only (transcript 68151603-2f8e-40d1-aa98-3e3c77d33652.jsonl; tool calls {\"mcp__ccd_turn__update_plan\": 5, \"Bash\": 66, \"Read\": 1, \"Write\": 1, \"Edit\": 3, \"mcp__ccd_turn__wrap_up\": 1}; edit tools [\"Edit\", \"Write\"]; pattern matches read by the governor: 5)",
|
||||
"tests_or_engines_started": false,
|
||||
"packet_sha256": "7e0fe0882371d6b74b9a8a2c1c68df5ad1bbd6486bc288bf8ffdc8900d4bee19",
|
||||
"normal_packet_sha256": "6e0f8d299a195d02b5f0b3579b5b4e49a6ecd772c9bdab6985ef31ab3c9f910e",
|
||||
"schema_sha256": "a25f262ed5d1a5d36b5f8fca747d28621f39c11e664e058cdaf5f92297857d49",
|
||||
"material_manifest_sha256": "c3ef98337585388c4a624ad56d1cc4dbfe209cc7d73c2c68865b34e5865c8d54",
|
||||
"result_file": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/p07-01-wx-independent-review-db87270-r01/DESKTOP_RESULT.json",
|
||||
"result_sha256": "1fd2c8bbe0f9d6b165d5f993e86002b94ead97674c27ed9270a223c929c1aaca",
|
||||
"result_sha256_source": "computed by the governor from the actual DESKTOP_RESULT.json (not edited)",
|
||||
"status_reported": "pass",
|
||||
"delivered_by": "Desktop governor session local_a7f85421-3eed-4482-8030-c0dca669573a (implementer, commander and native executor of P07-01-WX; did not review or edit the result)",
|
||||
"session_started_by": "Root started the own_initiative card task_2fb30bba in the Claude Desktop UI (Root relay: actual UI confirmed); app session local_fc6760c6-9acd-4614-950e-632b91d53f54, fresh context, parent local_a7f85421 only as the card's origin",
|
||||
"cli_authentication_recovered": false,
|
||||
"cli_review_succeeded": false,
|
||||
"recorded_at": 1792805101.216085
|
||||
},
|
||||
"independent_result": {
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/WX_INDEPENDENT_REVIEW_DB87270.json",
|
||||
"bytes": 15057,
|
||||
"sha256": "1fd2c8bbe0f9d6b165d5f993e86002b94ead97674c27ed9270a223c929c1aaca",
|
||||
"copy_of": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/p07-01-wx-independent-review-db87270-r01/DESKTOP_RESULT.json"
|
||||
},
|
||||
"review_findings": [
|
||||
{
|
||||
"severity": "low",
|
||||
"message": "WX3・WX7 の試験の不足(S f99c881 の low の残り。一部は開示済み)。再読み込みの後に延長が二重に効かないことと、延長の時刻の帰りを、まだ判断中の延長された客で動かして確かめていない。\n\n根拠:\n- 363 分の保存では、延びた客はもう LEAVING である(tests/environment/test_p07_weather_restart_1_write.gd 36〜37・55〜56 行)。\n- _advance_guest_ai_decisions は、DECIDING の時だけ _hold_guest_for_rain を通す(autoload/simulation_runtime.gd 9062〜9063 行)。\n- そのため、「(no second extension)」と書いた1分後の check(writer 76〜78 行、test_p07_weather_restart_2_read.gd 77〜78 行)と、wx/IMPLEMENTATION_NOTES.md 116 行の「1分後の一致で、二度目の延長が無いことを示す」は、延長の記録が失われても二度目の延長を検出できない。\n\n二重に効かないことの実際の根拠は、次の3つで、間接である:\n- 延長の記録と plan 602 が、両方の process で正確に戻ること(確かめ済み。handoff の復号でも一致)\n- _hold_guest_for_rain の記録の判定(13640 行)\n- メモリの上の直接の呼び出し(test_p07_weather_runtime.gd 201 行、256〜258 行)\n\n延長の時刻の PLANNED_EXIT も、直接の呼び出しだけ。こちらは wx §7 119〜121 行で開示されている。\n\nsource を読んだ限りでは製品は正しく、合格を妨げない。後の提案: 施設を使って延長の時刻まで DECIDING で残る客の世界で、延長を持つ保存を再読み込みし、10:02 に新しい延長が無く PLANNED_EXIT で帰ることを確かめる。または、check の message と記録の文言を実態に合わせる。"
|
||||
},
|
||||
{
|
||||
"severity": "low",
|
||||
"message": "WX1・WX7 の試験の不足(§5.2 の4つの規則のうち1つ)。「読み込みの後の最初の 06:00 から抽選を始める」を、移行した save で直接確かめていない。\n\n根拠:\n- tests/environment/test_p07_weather_runtime.gd 318〜331 行は、v1 の save を 06:05 に読む。今の天候を保つこと、last_draw_day 0、その日の抽選が無いこと、系列 weather が world_seed から作られること、延長の記録が無いことは確かめる。しかし、移行した Runtime を次の 06:00(1800 分)まで進めて、day 1 の抽選が1回出ることは確かめていない。\n- tests/environment/test_runtime_weather.gd 22〜28 行(v10 の移行)と、kitchen の legacy14 の照合も、読んだ状態を比べるだけである。\n\nsource では、移行の last_draw_day(scripts/environment/weather_timeline.gd 216〜218 行の _days_before)と抽選の判定 day > last_draw_day(autoload/simulation_runtime.gd 13615〜13621 行)が、新しいゲームの毎日の抽選と同じ経路である。新しいゲームの毎日の抽選は、test_p07_weather_runtime.gd 146〜154 行で確かめている。そのため製品は正しいと判断し、合格を妨げない。後の提案: 移行の試験で、次の 06:00 まで進めて抽選が1回出ることの check を足す。"
|
||||
}
|
||||
],
|
||||
"review_high_medium": 0,
|
||||
"review_low": 2,
|
||||
"review_provenance_records": [
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/p07-01-wx-independent-review-db87270-r01/READY_FOR_DESKTOP_REVIEW.json",
|
||||
"bytes": 2785,
|
||||
"sha256": "903ad75cae4b685b57b8d06c70ae1cc6cd5cd40752fa742b245e6f0a2abe19f3"
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/p07-01-wx-independent-review-db87270-r01/DESKTOP_DELIVERY.json",
|
||||
"bytes": 2692,
|
||||
"sha256": "3e0960d863904ddb01c62d5a04f53161ebc8304676fd8b68e6ee9d4e0741322a"
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/p07-01-wx-independent-review-db87270-r01/INTAKE_STARTED.json",
|
||||
"bytes": 217,
|
||||
"sha256": "70c04498573812960b85f98efafa14cac5b79b29074ca2e02b09e677664c6b93"
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/p07-01-wx-independent-review-db87270-r01/INTAKE_FINISHED.json",
|
||||
"bytes": 288,
|
||||
"sha256": "ecc0d3c9462f2459ad0988ac54fe97279e0fdf277aafb02d0ebaf434f1032f24"
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/DESKTOP_DELIVERY_WRITE_DB87270.stdout",
|
||||
"bytes": 192,
|
||||
"sha256": "e7c8bab3ca2a266ec25dc63f0d5681d32e80e77163ab931c8c4556260dccf28a"
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/RECEIVE_REVIEW_DB87270.stdout",
|
||||
"bytes": 179,
|
||||
"sha256": "743e2c703ea9702e30e60a1592c28d2c5066b6bf1486e91c60352af7ecfdf748"
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/PREPARE_INTEGRATION_DB87270_SUMMARY.json",
|
||||
"bytes": 3177,
|
||||
"sha256": "7909b4f608d635168f648c8b6b9b6b4bd30b21291c3e3f0b8707df2e50bca926"
|
||||
}
|
||||
],
|
||||
"earlier_reviews_retained": [
|
||||
{
|
||||
"S": "c50136017b686994c79bdb3ecf49a9df5a465052",
|
||||
"status": "changes_requested",
|
||||
"result": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/p07-01-wx-independent-review-c501360-r01/DESKTOP_RESULT.json",
|
||||
"bytes": 12489,
|
||||
"sha256": "aea0cc4f2c00a6a093ca256154c62812a4d83749d075c6a4a9d36c1270026da7"
|
||||
},
|
||||
"repair": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/COMMANDER_REPAIR_PLAN_P07_01_WX_R01.md",
|
||||
"bytes": 4315,
|
||||
"sha256": "73fc02ad93ef1f000738d041abcd1b78a1aedb92d6beedda2621bd78aca33232"
|
||||
}
|
||||
},
|
||||
{
|
||||
"S": "f99c881bca0399fec7a75a8715265fbcf7ff5b4c",
|
||||
"status": "changes_requested",
|
||||
"result": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/p07-01-wx-independent-review-f99c881-r01/DESKTOP_RESULT.json",
|
||||
"bytes": 12831,
|
||||
"sha256": "59595592ad55a5e71c77ffc1fa5a4e5203bf8381af5dd72f3c506a3977c60269"
|
||||
},
|
||||
"repair": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/COMMANDER_REPAIR_PLAN_P07_01_WX_R02.md",
|
||||
"bytes": 3986,
|
||||
"sha256": "af76464dbafa880b4b803bb16e2be93ab73000e92507e62a2d5bf908ca0c6dc2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"measurement": {
|
||||
"measured_on": "db87270972c9b243e3a1fccdf2c1fa96975dffc1",
|
||||
"native": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/NATIVE_DB87270_START.json",
|
||||
"bytes": 937,
|
||||
"sha256": "c6c6b34d6c81b4af6c1bfcbe8d490e755e82c98532490c8c0b4e94ec7434cceb"
|
||||
},
|
||||
"native_bundle_transfer": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/NATIVE_DB87270_BUNDLE_TRANSFER.json",
|
||||
"bytes": 1333,
|
||||
"sha256": "3e8c1fe02567452eb2a5250b55cd464542cacb64e1db4e4c17a04e96ef650b32"
|
||||
},
|
||||
"formal_validation_summary": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/FORMAL_VALIDATE_DB87270_SUMMARY.json",
|
||||
"bytes": 3273,
|
||||
"sha256": "2db2d9cace4e26358557fc81d5c7631ec420c14df9a289f7498d752a94e2201b"
|
||||
},
|
||||
"full_roster_summary": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/FULL_ROSTER_DB87270_R01_SUMMARY.json",
|
||||
"bytes": 3815,
|
||||
"sha256": "7f9a042d39291de5730ae2747f4de2c96282cd6a9124268fa0020580e0fb14e5"
|
||||
},
|
||||
"full_roster_placement": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/FULL_ROSTER_DB87270_PLACEMENT.json",
|
||||
"bytes": 951,
|
||||
"sha256": "0a647732ac0689640eabb0bd21f732b3d7b7a74d6166be074cbf6c00c387b495"
|
||||
},
|
||||
"native_phases": 16,
|
||||
"native_suites": 14,
|
||||
"native_checks": 1159,
|
||||
"full_roster_suites": 198,
|
||||
"full_roster_checks": 22959,
|
||||
"measurement_not_relabelled_as_gate_Q": true,
|
||||
"Q_meaning": "the evidence measured on S was checked by the registered content-based verifier in fresh checkouts of Q (integration and final); nothing was measured on Q and no engine started"
|
||||
},
|
||||
"capacity_records_content_preserving": [
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/CLOSED_COPIES_ALIGN_R21.json",
|
||||
"bytes": 31543021,
|
||||
"sha256": "cfa4f6139cf6e7427896f1f057dcdf2ec920218e0b0a78cd99d27811a89fc035"
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/SCRATCH_STAGES_ALIGN_R23A.json",
|
||||
"bytes": 4544129,
|
||||
"sha256": "d9b762af5fc7bd7760a75592ef0bb5c3e41f9b34bd9cfa3825ac423c22a0468b"
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/C501360_CLOSED_MOVE_ALIGN_R23B.json",
|
||||
"bytes": 3046949,
|
||||
"sha256": "0c33d06226315af930c9d134ef80cc05b5c6b5f2d7d093bb0e29dec3591d9647"
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/F99C881_CLOSED_ALIGN_R25.json",
|
||||
"bytes": 1515211,
|
||||
"sha256": "a553519e47d45d518ce8dc1e9c241d53f7cd9ef69db0ba3bad808dfefab515bb"
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/F99C881_CLOSED_MOVE_ALIGN_R26.json",
|
||||
"bytes": 1531974,
|
||||
"sha256": "de5519ec42b29d9a08c9d86155f43c5e8d0b5fcd61cfc5040c443ec8d68d4e94"
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/DB87270_CLOSED_ALIGN_R27.json",
|
||||
"bytes": 1515187,
|
||||
"sha256": "f326f9d14f608a0a2eeef2212557ab0026403f6d73a268c5d4eb82df720d30be"
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/P07_01_WX_FULL_ROSTER_RAW_COMPRESSION_F99C881_R24.json",
|
||||
"bytes": 3076779,
|
||||
"sha256": "7dbb9b918829a2861648cfc1d8d25ee0236a1b83fee46f0e276d60943204a89e"
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/P07_01_WX_FULL_ROSTER_RAW_COMPRESSION_DB87270_R28.json",
|
||||
"bytes": 3113055,
|
||||
"sha256": "0622c636b62b222ddc6070cc623694210f1781f78a13d42c48067605bdf347db"
|
||||
}
|
||||
],
|
||||
"historical_evidence_retained": [
|
||||
{
|
||||
"S": "dfacc83d0afdd322a6389e131b87d3205e5ac9c8",
|
||||
"record": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-control-r01/NATIVE_DFACC83_FAILURE_R01.json",
|
||||
"bytes": 2582,
|
||||
"sha256": "af53fe3eeb58ff9a17274e8c17644407062aa75f1534a661d0dbdc1f7a4b4db5"
|
||||
},
|
||||
"meaning": "original P07-01 S native: 14 suites passed, the copied verifier exited 1 (old_bounded environment gap); fixed in WX"
|
||||
},
|
||||
{
|
||||
"S": "c50136017b686994c79bdb3ecf49a9df5a465052",
|
||||
"record": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/FULL_ROSTER_C501360_FAILURE_R01.json",
|
||||
"bytes": 3515,
|
||||
"sha256": "2b07e78e014aea37bad0fefb024d050e36fd4c0c94c8f4bcfa23499c73e7be26"
|
||||
},
|
||||
"meaning": "full roster stopped at test_required_kitchen.gd (A13 migration not mapped); fixed in repair 1"
|
||||
},
|
||||
{
|
||||
"record": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/P07_01_WX_DIAGNOSTIC_SWEEP_R01.json",
|
||||
"bytes": 39672,
|
||||
"sha256": "bd5efda6c32549a4a35829aa2e4d5c4f4d039f01202aea0f378781386da07c6e"
|
||||
},
|
||||
"meaning": "diagnostic sweep of all 198 suites on a scratch stage; not formal evidence"
|
||||
},
|
||||
{
|
||||
"record": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/P07_01_WX_REPAIR1_LOCAL_CHECKS_R01.json",
|
||||
"bytes": 10176,
|
||||
"sha256": "380a80c877d5992ae3f145a5fb41192ac25ee2504ad7fa4f1d33b5e07371bd6b"
|
||||
},
|
||||
"meaning": "repair 1 local checks with their failed first runs"
|
||||
},
|
||||
{
|
||||
"record": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/P07_01_WX_REPAIR2_LOCAL_CHECKS_R01.json",
|
||||
"bytes": 4959,
|
||||
"sha256": "9650d5050533edd9551d522d2288343dc01a78deadd474f0f9553affb4824eef"
|
||||
},
|
||||
"meaning": "repair 2 local checks; diag-4.log shows that before the fix a save in the minute a held guest left could not be loaded"
|
||||
},
|
||||
{
|
||||
"record": {
|
||||
"path": "artifacts/original-completion-2026-10-02/p07-01-wx-control-r01/FULL_ROSTER_F99C881_R01_START_CORRECTION_01.json",
|
||||
"bytes": 1463,
|
||||
"sha256": "748dc4b8efb81eb16efe84b3574029de3ded6d8dd247bce2770397cbb1aa29fd"
|
||||
},
|
||||
"meaning": "start-record reference name correction (bytes of the start record kept)"
|
||||
}
|
||||
],
|
||||
"remaining_not_claimed": [
|
||||
"Original P07-01 stays unmet: the furnace yield 120 at fog boundaries (original SPEC section 4.4, F5) and AR.weather.fog_magic need the P13 furnace facility, the D04 unit consumption and the A06 separate approval; the original F1-F8 whole-unit acceptance is not claimed",
|
||||
"The event log shows the daily WEATHER_FORECAST_DRAWN as 未識別の記録 because scripts/ui/event_log_dialog.gd has no Japanese name for it (outside allowed_paths; proposed as a separate unit)",
|
||||
"In the test worlds without a usable facility a held guest leaves before its extended time; the exit at the extended time is checked at the decision step only (review low 1, disclosed)",
|
||||
"A migrated save drawing again at the next 06:00 is not tested directly (review low 2); the product path is the daily draw path that is tested",
|
||||
"The ordinary-cleanup branch of the registered verifier is not exercised by the narrow roster (no ordinary suite); old_bounded and no-teardown branches ran",
|
||||
"Forecast shows only the current day; the UI uses the current parts and is not the finished look; Windows device acceptance, long normal play and whole-game final acceptance remain"
|
||||
],
|
||||
"implementation_provenance": "Contract worker field codex is compatibility only; no Codex or Astra run or dispatch. Carry-over of the original S dfacc83 (24 files byte for byte), the verifier fix, repairs 1 and 2, natives and full rosters by the registered Claude Opus 5.5 Desktop implementer and commander; commits made by Runner submit; independent reviews by separate fresh Opus 5.5 Desktop sessions",
|
||||
"formal_original_credit": 0,
|
||||
"original_completed_before": 24,
|
||||
"original_completed_after_ledger": 24,
|
||||
"original_delivery_total": 88,
|
||||
"packages_completed": 4,
|
||||
"packages_total": 30,
|
||||
"partial_credit": false,
|
||||
"new_engine_or_full_roster_runs_for_ledger": 0,
|
||||
"all_prior_failures_retained": true,
|
||||
"CLI_auth_recovered": false,
|
||||
"no_new_human_utterance": true
|
||||
}
|
||||
+26
File diff suppressed because one or more lines are too long
@@ -243,7 +243,7 @@
|
||||
"facility_value_contracts_validation.json": "bf490a9ca585c48fc318e76c4a069ef244827d828d46e6415399f7710590dd16",
|
||||
"performance_audit.json": "0b630b15a392c3e0366f1362bfe4ddacb968f915f54549f82e4d6fa315384762",
|
||||
"progress_contract.json": "ccbdfce8c8365d5c13f59531955708834cce45acd0e136561c95295dfdb5df77",
|
||||
"progress_snapshot.json": "38777ebda6729c8127fcca0eb6ecf133673445b8d66ebb51b44cad00e5457b1a",
|
||||
"progress_snapshot.json": "44ed0b19d042726d288ed2fc6749e2a368c344ca78d904007258ac4df879ecf8",
|
||||
"progress_validator_guard_checks.json": "72ee513fcf1cad85c24bfd0a4b472f425f6acf117928ece0f6b41e29018da627",
|
||||
"progress_validator_guard_checks.r01.json": "14c26adffa5a1938241db4912e50ea54b660641ee6fee2e412d467dce14d0519",
|
||||
"requirement_coverage.json": "03b10e4da367579db502d0671a6e0dda36fd385111c6f1c0084acaff7605f593",
|
||||
@@ -253,6 +253,6 @@
|
||||
"validate_facility_value_contracts.py": "91640e2a76cd6cc9186f7b85c0bf379b24b6cac0d48962815235ee628e7ae43e",
|
||||
"validate_plan.py": "2f698fbf38d3b16713835f99aaa391df9ebc30f44547f272644de19efb91f50a",
|
||||
"validator_update_checks.json": "f997d0f3de2cea29550840b1d5cb23647d64f81329de46758551e2b6c8fecc27",
|
||||
"work_packages.json": "f543f39842764076b4522cc818c476d18bdc8e09ac9d83e02dd84ccadc966496"
|
||||
"work_packages.json": "9faa85262ddc83fea7d4daeed8cdc3ee606a8cf6fce6ee9fff05ebb4d9b9a72d"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"integrated_at_jst": "2026-10-23T22:51:54.402572+09:00",
|
||||
"scope": "原30パッケージ/88成果、正式完了24成果・4パッケージ。部分点なし。P05-02全条件を独立レビュー/通常final/実DB結合で正式完了(P05 packageはP05-03未完)。ユーザー指示により機能実装の完了を優先し、P28-02の最適化と試験速度の調整は後段。全ゲーム/同一Windows最終受入は未完。",
|
||||
"source_head": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
|
||||
"source_authority_note": "台帳更新直前main Qcb183fd(S8954914とtree同一)。P05-02はRunner completed 8/8・repairs3、validation/review/integration/final全PASS、final ec32bfcd SHA f8d25e28 verified true/engines_started0。別fresh Desktop Opus5.5がF1〜F8 PASS(high/medium0・low3)。Rootが既存の人のmain/push許可を正確なQ/Mへ正直に適用(新しい人の発言は作らない)。測定はSでQへ再ラベルしない。原24/88・4/30。",
|
||||
"integrated_at_jst": "2026-10-24T10:45:02.441582+09:00",
|
||||
"scope": "原30パッケージ/88成果、正式完了24成果・4パッケージ。部分点なし。P07-01-WX補助(原加算0)を独立レビュー/通常final/実DB結合で正式完了し、原P07-01はマナ炉120とAR.weather.fog_magicの残件でin_progress。ユーザー指示により機能実装の完了を優先し、P28-02の最適化と試験速度の調整は後段。全ゲーム/同一Windows最終受入は未完。",
|
||||
"source_head": "9b5b09b35e888e5ac1a49174b8a7f73e51c502d2",
|
||||
"source_authority_note": "台帳更新直前main Q9b5b09b(S db87270とtree同一)。P07-01-WXはRunner completed 8/8・repairs2、validation/review/integration/final全PASS、final 9145ecb2 SHA 61d87d35 verified true/engines_started0。別fresh Desktop Opus5.5がWX1〜WX8・横断7・U1/U2 PASS(high/medium0・low2)。Rootが既存の人のmain/push許可を正確なQ/Mへ正直に適用(新しい人の発言は作らない)。測定はSでQへ再ラベルしない。原加算0、24/88・4/30。",
|
||||
"immutable_history": {
|
||||
"baseline.json": {
|
||||
"sha256": "750bb3d313cc5e2ce0d4c79e563a995abd7a78f991dee1dd75c627877b02cd7b",
|
||||
@@ -161,8 +161,8 @@
|
||||
},
|
||||
"current_delivery_state_counts": {
|
||||
"completed": 24,
|
||||
"in_progress": 17,
|
||||
"planned": 47
|
||||
"in_progress": 18,
|
||||
"planned": 46
|
||||
},
|
||||
"completed_current_deliveries": [
|
||||
"P00-01",
|
||||
@@ -9801,8 +9801,9 @@
|
||||
"formal_count": 24,
|
||||
"original_delivery_units": 88,
|
||||
"partial_credit": false,
|
||||
"selection_status": "P05-02正式完了。ユーザー指示により機能実装を優先し、回答待ちに依らない次の機能実装をRootの通常登録へ提案する。P28-02の記録は保持し後段。",
|
||||
"pending_decisions": "人への問いの回答待ち: P05-03の3件、P07-01の天候、P16-01-DISMISSの退職金。どれも推定しない。"
|
||||
"selection_status": "P07-01-WX補助(原加算0)正式完了。機能実装を優先し、WX統合後のmainで承認済みのP16-01-DISMISS(退職金を含む一般従業員の解雇、原加算0)をRootの通常登録へ渡す。P28-02の記録は保持し後段。",
|
||||
"pending_decisions": "人への問いの回答待ち: P05-03の3件(推定しない)。天候(A13)と退職金(A14)は回答済み。",
|
||||
"auxiliary_closed": "P07-01-WX"
|
||||
},
|
||||
"historical_regression_before_P08_02_formal_closure": {
|
||||
"path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-02-final-candidate-r32-combined-root-acceptance-r01.json",
|
||||
@@ -10600,7 +10601,7 @@
|
||||
},
|
||||
"whole_game_acceptance": false
|
||||
},
|
||||
"next_closure_note": "P05-02正式完了(24/88)。機能実装を優先:回答待ちに依らない次の機能の補助作業をRootが通常登録。P16-01-DISMISS r03は退職金の回答待ち。P28-02-OPTは後段(記録は保持)。",
|
||||
"next_closure_note": "P07-01-WX補助正式完了(原加算0、24/88)。次はP16-01-DISMISS r04をWX統合後のmainでRootが通常登録。原P07-01は炉の経路待ち。P28-02-OPTは後段(記録は保持)。",
|
||||
"current_p26_01_content_ledger": {
|
||||
"at": "2026-10-17T20:44:25.180745+09:00",
|
||||
"original_plan_id": "P26-01",
|
||||
@@ -10790,24 +10791,24 @@
|
||||
"current_workflow_handoff": {
|
||||
"controller_local_session": "local_a7f85421-3eed-4482-8030-c0dca669573a",
|
||||
"actual_model": "Opus5.5",
|
||||
"project": "the-tree-p05-02",
|
||||
"task": "P05-02",
|
||||
"project": "the-tree-p07-01-wx",
|
||||
"task": "P07-01-WX",
|
||||
"state": "completed",
|
||||
"submission": "895491456477b42a74e59fa239bea86c4fb9aa33",
|
||||
"candidate": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
|
||||
"main_base": "613b9886dae31b2578f6aa13b81712b68ac694bf",
|
||||
"submission": "db87270972c9b243e3a1fccdf2c1fa96975dffc1",
|
||||
"candidate": "9b5b09b35e888e5ac1a49174b8a7f73e51c502d2",
|
||||
"main_base": "7ac2224e4705d06aab28803592a68b31d60841ed",
|
||||
"evidence": {
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/P05_02_FORMAL_COMPLETION_CB183FD.json",
|
||||
"bytes": 30318,
|
||||
"sha256": "27e197df6c636126ad74f0d3a30041134c4bd4a1c94415f9ab4c380195bf100e"
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/WX_FORMAL_COMPLETION_9B5B09B.json",
|
||||
"bytes": 38051,
|
||||
"sha256": "57c2bf716126a823f3e735f50a7fa2ccb5b24a15be74d90cdc73031a506317ad"
|
||||
},
|
||||
"independent_reviewer_session": "local_cbdd640f-21ad-4f21-8d71-12b8debccc21",
|
||||
"independent_result_sha256": "fb3e5be2a15214a449da41b8a496de1b2f18ecbb1e8482c68ecd45999b9ad9a5",
|
||||
"independent_reviewer_session": "local_fc6760c6-9acd-4614-950e-632b91d53f54",
|
||||
"independent_result_sha256": "1fd2c8bbe0f9d6b165d5f993e86002b94ead97674c27ed9270a223c929c1aaca",
|
||||
"all4_normal_gates_passed": true,
|
||||
"original_credit": 1,
|
||||
"original_credit": 0,
|
||||
"original_completed": 24,
|
||||
"original_total": 88,
|
||||
"next": "Feature-first: next feature implementation that needs no pending human answer, proposed to Root for normal registration; P16-01-DISMISS r03 waits for the severance answer; P28-02-OPT deferred by the user instruction"
|
||||
"next": "P16-01-DISMISS r04 normal registration on the main after this ledger commit (Root registers and prepares; the same actual Opus continues as commander and implementer); original P07-01 waits for the furnace path; P28-02-OPT deferred by the user instruction"
|
||||
},
|
||||
"historical_workflow_before_desktop_handoff_20261019": {
|
||||
"source_thread": "01a14f6a-b6a1-7fd3-bd45-0f84bbef77be",
|
||||
@@ -14311,5 +14312,83 @@
|
||||
"AR.staff.dismissal_severance (P05, D04) not addressed; human severance answer pending",
|
||||
"Same Windows distribution device acceptance, long normal play and whole-game final acceptance remain"
|
||||
]
|
||||
},
|
||||
"historical_before_P07_01_WX_formal_9b5b09b": {
|
||||
"integrated_at_jst": "2026-10-23T22:51:54.402572+09:00",
|
||||
"scope": "原30パッケージ/88成果、正式完了24成果・4パッケージ。部分点なし。P05-02全条件を独立レビュー/通常final/実DB結合で正式完了(P05 packageはP05-03未完)。ユーザー指示により機能実装の完了を優先し、P28-02の最適化と試験速度の調整は後段。全ゲーム/同一Windows最終受入は未完。",
|
||||
"source_head": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
|
||||
"source_authority_note": "台帳更新直前main Qcb183fd(S8954914とtree同一)。P05-02はRunner completed 8/8・repairs3、validation/review/integration/final全PASS、final ec32bfcd SHA f8d25e28 verified true/engines_started0。別fresh Desktop Opus5.5がF1〜F8 PASS(high/medium0・low3)。Rootが既存の人のmain/push許可を正確なQ/Mへ正直に適用(新しい人の発言は作らない)。測定はSでQへ再ラベルしない。原24/88・4/30。",
|
||||
"current_delivery_state_counts": {
|
||||
"completed": 24,
|
||||
"in_progress": 17,
|
||||
"planned": 47
|
||||
},
|
||||
"current_closure_targets": {
|
||||
"formally_closed": "P05-02",
|
||||
"next_candidate": "P28-02",
|
||||
"formal_count": 24,
|
||||
"original_delivery_units": 88,
|
||||
"partial_credit": false,
|
||||
"selection_status": "P05-02正式完了。ユーザー指示により機能実装を優先し、回答待ちに依らない次の機能実装をRootの通常登録へ提案する。P28-02の記録は保持し後段。",
|
||||
"pending_decisions": "人への問いの回答待ち: P05-03の3件、P07-01の天候、P16-01-DISMISSの退職金。どれも推定しない。"
|
||||
},
|
||||
"current_workflow_handoff": {
|
||||
"controller_local_session": "local_a7f85421-3eed-4482-8030-c0dca669573a",
|
||||
"actual_model": "Opus5.5",
|
||||
"project": "the-tree-p05-02",
|
||||
"task": "P05-02",
|
||||
"state": "completed",
|
||||
"submission": "895491456477b42a74e59fa239bea86c4fb9aa33",
|
||||
"candidate": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
|
||||
"main_base": "613b9886dae31b2578f6aa13b81712b68ac694bf",
|
||||
"evidence": {
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p05-02/P05_02_FORMAL_COMPLETION_CB183FD.json",
|
||||
"bytes": 30318,
|
||||
"sha256": "27e197df6c636126ad74f0d3a30041134c4bd4a1c94415f9ab4c380195bf100e"
|
||||
},
|
||||
"independent_reviewer_session": "local_cbdd640f-21ad-4f21-8d71-12b8debccc21",
|
||||
"independent_result_sha256": "fb3e5be2a15214a449da41b8a496de1b2f18ecbb1e8482c68ecd45999b9ad9a5",
|
||||
"all4_normal_gates_passed": true,
|
||||
"original_credit": 1,
|
||||
"original_completed": 24,
|
||||
"original_total": 88,
|
||||
"next": "Feature-first: next feature implementation that needs no pending human answer, proposed to Root for normal registration; P16-01-DISMISS r03 waits for the severance answer; P28-02-OPT deferred by the user instruction"
|
||||
},
|
||||
"next_closure_note": "P05-02正式完了(24/88)。機能実装を優先:回答待ちに依らない次の機能の補助作業をRootが通常登録。P16-01-DISMISS r03は退職金の回答待ち。P28-02-OPTは後段(記録は保持)。"
|
||||
},
|
||||
"current_P07_01_WX_auxiliary_completion": {
|
||||
"state": "completed",
|
||||
"S": "db87270972c9b243e3a1fccdf2c1fa96975dffc1",
|
||||
"Q": "9b5b09b35e888e5ac1a49174b8a7f73e51c502d2",
|
||||
"M": "7ac2224e4705d06aab28803592a68b31d60841ed",
|
||||
"all4_normal_gates_passed": true,
|
||||
"proof": {
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/WX_FORMAL_COMPLETION_9B5B09B.json",
|
||||
"bytes": 38051,
|
||||
"sha256": "57c2bf716126a823f3e735f50a7fa2ccb5b24a15be74d90cdc73031a506317ad"
|
||||
},
|
||||
"independent_review": {
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/WX_INDEPENDENT_REVIEW_DB87270.json",
|
||||
"bytes": 15057,
|
||||
"sha256": "1fd2c8bbe0f9d6b165d5f993e86002b94ead97674c27ed9270a223c929c1aaca"
|
||||
},
|
||||
"acceptance_record": {
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/WX_ACCEPTANCE_9B5B09B.md",
|
||||
"bytes": 8201,
|
||||
"sha256": "df74dc4fc309e06671582747fd11c63390f3ab93c220f8ff9e5ef89bf1d4d253"
|
||||
},
|
||||
"original_credit": 0,
|
||||
"formal_original_completed": 24,
|
||||
"original_total": 88,
|
||||
"original_P07_01_state": "in_progress",
|
||||
"remaining_not_claimed": [
|
||||
"Furnace yield 120 at fog boundaries (original SPEC section 4.4, F5) and AR.weather.fog_magic: need the P13 furnace facility, the D04 unit consumption and the A06 separate approval",
|
||||
"Original P07-01 F1-F8 whole-unit independent review, normal integration and final after the furnace path exists",
|
||||
"Event log shows the daily WEATHER_FORECAST_DRAWN as 未識別の記録 (scripts/ui/event_log_dialog.gd outside the WX allowed_paths); proposed as a separate unit",
|
||||
"Review low 1: the exit at the extended time and no second extension after a reload are checked at the decision step and through the exact saved hold only, because a held guest leaves before its extended time in the test worlds without a usable facility (disclosed)",
|
||||
"Review low 2: a migrated save drawing at the next 06:00 is not tested directly; the product path is the tested daily draw path",
|
||||
"The ordinary-cleanup branch of the registered verifier is not exercised by the narrow roster",
|
||||
"Forecast covers the current day only; the UI uses the current parts; same Windows device acceptance, long normal play and whole-game final acceptance remain"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6882,15 +6882,93 @@
|
||||
},
|
||||
"legacy_state": "in_progress",
|
||||
"state_scope": "package-level; completed means whole package accepted; partial implementation is not completion",
|
||||
"current_summary": "嵐の境界と天候保存、清潔度基盤は部分接続済み。",
|
||||
"current_summary": "P07-01-WX補助(原加算0)を正式完了(S db87270→Q 9b5b09b、独立Opus PASS、通常4ゲートPASS)。自然天候・雨・嵐・世界樹と成人客のマナ霧・上部の予報・保存移行は通常営業へ接続済み。原P07-01は霧の境界のマナ炉120とAR.weather.fog_magic(P13炉施設・D04・A06別承認待ち)で未達、P07-02の環境・P07-03の流行ニュースは未着手。清潔度の基盤は既存のまま。",
|
||||
"remaining_deliverables": [
|
||||
{
|
||||
"id": "P07-01",
|
||||
"title": "雨と霧と自然天候",
|
||||
"work": "雨の来客減・滞在延長、マナ霧、自然変化を実営業へ接続する。",
|
||||
"acceptance": "天候切替直前後、途中保存、自然来客の差を同条件で説明できる。",
|
||||
"state": "planned",
|
||||
"evidence": []
|
||||
"state": "in_progress",
|
||||
"evidence": [
|
||||
{
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/WX_FORMAL_COMPLETION_9B5B09B.json",
|
||||
"bytes": 38051,
|
||||
"sha256": "57c2bf716126a823f3e735f50a7fa2ccb5b24a15be74d90cdc73031a506317ad",
|
||||
"scope": "P07-01-WX auxiliary eight conditions formally completed; original P07-01 remains incomplete (furnace 120, AR.weather.fog_magic); original credit zero",
|
||||
"linked_at_jst": "2026-10-24T10:45:02.441582+09:00"
|
||||
},
|
||||
{
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/WX_INDEPENDENT_REVIEW_DB87270.json",
|
||||
"bytes": 15057,
|
||||
"sha256": "1fd2c8bbe0f9d6b165d5f993e86002b94ead97674c27ed9270a223c929c1aaca",
|
||||
"scope": "Actual fresh independent Opus result for S db87270 (byte copy), pass, high/medium 0, low 2",
|
||||
"linked_at_jst": "2026-10-24T10:45:02.441582+09:00"
|
||||
},
|
||||
{
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/WX_ACCEPTANCE_9B5B09B.md",
|
||||
"bytes": 8201,
|
||||
"sha256": "df74dc4fc309e06671582747fd11c63390f3ab93c220f8ff9e5ef89bf1d4d253",
|
||||
"scope": "Readable acceptance record of the WX auxiliary completion with retained failures and open residuals",
|
||||
"linked_at_jst": "2026-10-24T10:45:02.441582+09:00"
|
||||
}
|
||||
],
|
||||
"historical_before_WX_formal_9b5b09b": {
|
||||
"state": "planned",
|
||||
"evidence_count": 0
|
||||
},
|
||||
"progress_summary": "P07-01-WX補助(原加算0)を受入8件(WX1〜WX8)の別fresh Desktop Opus5.5独立レビューPASS(高/中0・low2)、通常validation/review/integration/final全PASSと実DB/traceability結合で正式完了。S db87270→Q 9b5b09b(tree同一、修正2)。自然天候・雨の来客減と滞在延長・嵐・世界樹と成人客のマナ霧・上部の表示と予報・保存移行を通常営業へ接続。測定はS(native14本1159・全roster198/22959)、Qは中身同一の照合で測定と書かない。原P07-01は霧の境界のマナ炉120とAR.weather.fog_magicがP13炉施設・D04ユニット消費・A06別承認待ちのため未達、in_progress。正式24/88・4/30、部分点なし。",
|
||||
"remaining_gaps": [
|
||||
"Furnace yield 120 at fog boundaries (original SPEC section 4.4, F5) and AR.weather.fog_magic: need the P13 furnace facility, the D04 unit consumption and the A06 separate approval",
|
||||
"Original P07-01 F1-F8 whole-unit independent review, normal integration and final after the furnace path exists"
|
||||
],
|
||||
"updated_at_jst": "2026-10-24T10:45:02.441582+09:00",
|
||||
"auxiliary_completions": [
|
||||
{
|
||||
"id": "P07-01-WX",
|
||||
"S": "db87270972c9b243e3a1fccdf2c1fa96975dffc1",
|
||||
"Q": "9b5b09b35e888e5ac1a49174b8a7f73e51c502d2",
|
||||
"M": "7ac2224e4705d06aab28803592a68b31d60841ed",
|
||||
"repairs": 2,
|
||||
"all4_normal_gates_passed": true,
|
||||
"acceptance_ids": [
|
||||
"P07-01-WX1",
|
||||
"P07-01-WX2",
|
||||
"P07-01-WX3",
|
||||
"P07-01-WX4",
|
||||
"P07-01-WX5",
|
||||
"P07-01-WX6",
|
||||
"P07-01-WX7",
|
||||
"P07-01-WX8"
|
||||
],
|
||||
"independent_review": {
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/WX_INDEPENDENT_REVIEW_DB87270.json",
|
||||
"bytes": 15057,
|
||||
"sha256": "1fd2c8bbe0f9d6b165d5f993e86002b94ead97674c27ed9270a223c929c1aaca"
|
||||
},
|
||||
"formal_proof": {
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/WX_FORMAL_COMPLETION_9B5B09B.json",
|
||||
"bytes": 38051,
|
||||
"sha256": "57c2bf716126a823f3e735f50a7fa2ccb5b24a15be74d90cdc73031a506317ad"
|
||||
},
|
||||
"acceptance_record": {
|
||||
"path": "docs/plans/original-completion-2026-10-02/execution/p07-01/wx/WX_ACCEPTANCE_9B5B09B.md",
|
||||
"bytes": 8201,
|
||||
"sha256": "df74dc4fc309e06671582747fd11c63390f3ab93c220f8ff9e5ef89bf1d4d253"
|
||||
},
|
||||
"review_high_medium": 0,
|
||||
"review_low": 2,
|
||||
"original_credit": 0,
|
||||
"completed_at_jst": "2026-10-24T10:45:02.441582+09:00"
|
||||
}
|
||||
],
|
||||
"residuals_outside_unit_acceptance": [
|
||||
"Event log shows the daily WEATHER_FORECAST_DRAWN as 未識別の記録 (scripts/ui/event_log_dialog.gd outside the WX allowed_paths); proposed as a separate unit",
|
||||
"Review low 1: the exit at the extended time and no second extension after a reload are checked at the decision step and through the exact saved hold only, because a held guest leaves before its extended time in the test worlds without a usable facility (disclosed)",
|
||||
"Review low 2: a migrated save drawing at the next 06:00 is not tested directly; the product path is the tested daily draw path",
|
||||
"The ordinary-cleanup branch of the registered verifier is not exercised by the narrow roster",
|
||||
"Forecast covers the current day only; the UI uses the current parts; same Windows device acceptance, long normal play and whole-game final acceptance remain"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "P07-02",
|
||||
@@ -6944,7 +7022,9 @@
|
||||
"既存代表負荷の性能回帰を計測"
|
||||
],
|
||||
"cross_cutting_acceptance_scope": "Completion requirements only, not achieved results. Apply each to relevant slices; non-applicable views require a recorded reason.",
|
||||
"initial_revision_state": "in_progress"
|
||||
"initial_revision_state": "in_progress",
|
||||
"historical_current_summary_before_P07_01_WX_formal_9b5b09b": "嵐の境界と天候保存、清潔度基盤は部分接続済み。",
|
||||
"updated_at_jst": "2026-10-24T10:45:02.441582+09:00"
|
||||
},
|
||||
{
|
||||
"id": "P08",
|
||||
|
||||
@@ -136,6 +136,7 @@ var rank_button: Button
|
||||
var rank_dialog: ConfirmationDialog
|
||||
var event_log_dialog: AcceptDialog
|
||||
var weather_label: Label
|
||||
var weather_forecast_label: Label
|
||||
var guest_count_label: Button
|
||||
var dimension_migration_dialog: ConfirmationDialog
|
||||
var _dimension_migration_digest := ""
|
||||
@@ -467,6 +468,11 @@ func _create_top_bar() -> Control:
|
||||
weather_label.tooltip_text = "嵐の間は新規入館が停止します。現在のお客様は館内に残ります。"
|
||||
weather_label.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
row.add_child(weather_label)
|
||||
# A13 forecast: the next change of today's weather (kind and time).
|
||||
weather_forecast_label = _hud_value_label("", 72)
|
||||
weather_forecast_label.name = "WeatherForecastLabel"
|
||||
weather_forecast_label.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
row.add_child(weather_forecast_label)
|
||||
|
||||
guest_count_label = Button.new()
|
||||
guest_count_label.text = "来客 0人"
|
||||
@@ -1550,6 +1556,16 @@ func _refresh_dashboard() -> void:
|
||||
var weather_report: Dictionary = _runtime.get_weather_report()
|
||||
var weather_names := {&"clear": "晴れ", &"rain": "雨", &"storm": "嵐", &"mana_fog": "マナ霧"}
|
||||
weather_label.text = weather_names.get(weather_report.get(&"kind", &""), "天候 —")
|
||||
var next_change: Dictionary = weather_report.get(&"next_change", {})
|
||||
var forecast := "次: 予定なし"
|
||||
if not next_change.is_empty():
|
||||
var change_minute := posmod(int(next_change.get(&"minute", 0)), 1_440)
|
||||
forecast = "次: %s %02d:%02d" % [weather_names.get(StringName(next_change.get(&"kind", &"")), "天候 —"), change_minute / 60, change_minute % 60]
|
||||
var effect := String(weather_report.get(&"effect_text", ""))
|
||||
weather_label.tooltip_text = "%s\n毎朝6時にその日の天候が決まります。" % effect if not effect.is_empty() else weather_label.tooltip_text
|
||||
if weather_forecast_label != null:
|
||||
weather_forecast_label.text = forecast
|
||||
weather_forecast_label.tooltip_text = "%s\n%s" % [forecast, effect]
|
||||
var guests := _call_array(_service(&"guest_service"), &"get_guests")
|
||||
var queued_guests := 0
|
||||
var served_guests := 0
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
extends RefCounted
|
||||
|
||||
## Approved initial weather values A13 (P07-01 SPEC sections 2 and 4). The
|
||||
## timeline stays a pure state model; this file owns the draw and the effects.
|
||||
const DAY_MINUTES := 1440
|
||||
const DRAW_MINUTE_OF_DAY := 360
|
||||
const FIRST_START_MINUTE_OF_DAY := 360
|
||||
const START_SLOT_MINUTES := 30
|
||||
const START_SLOTS := 29
|
||||
## Rolls 0..99: clear 0-59, rain 60-84, mana fog 85-94, storm 95-99.
|
||||
const CLEAR_BELOW := 60
|
||||
const RAIN_BELOW := 85
|
||||
const FOG_BELOW := 95
|
||||
const DURATION_MINUTES := {&"rain": 360, &"mana_fog": 240, &"storm": 120}
|
||||
const RAIN_ARRIVAL_PERCENT := 50
|
||||
const RAIN_HOLD_MAX_MINUTES := 240
|
||||
const FOG_MANA_NUMERATOR := 12
|
||||
const FOG_MANA_DENOMINATOR := 10
|
||||
const FOG_INTIMACY_NUMERATOR := 3
|
||||
const FOG_INTIMACY_DENOMINATOR := 2
|
||||
|
||||
static func is_draw_minute(absolute_minute: int) -> bool:
|
||||
return absolute_minute >= 0 and posmod(absolute_minute, DAY_MINUTES) == DRAW_MINUTE_OF_DAY
|
||||
|
||||
static func day_of(absolute_minute: int) -> int:
|
||||
return int(floor(float(absolute_minute) / DAY_MINUTES)) if absolute_minute >= 0 else -1
|
||||
|
||||
## One draw per day from the saved weather stream. A clear day consumes one
|
||||
## roll; other days also consume one start-slot roll.
|
||||
static func draw(rng: DeterministicRng, day: int) -> Dictionary:
|
||||
var roll := rng.randi_range(0, 99)
|
||||
var kind: StringName = &"clear"
|
||||
if roll >= FOG_BELOW:
|
||||
kind = &"storm"
|
||||
elif roll >= RAIN_BELOW:
|
||||
kind = &"mana_fog"
|
||||
elif roll >= CLEAR_BELOW:
|
||||
kind = &"rain"
|
||||
if kind == &"clear":
|
||||
return {&"day": day, &"kind": kind, &"roll": roll, &"slot": -1, &"start": -1, &"end": -1}
|
||||
var slot := rng.randi_range(0, START_SLOTS - 1)
|
||||
var start := day * DAY_MINUTES + FIRST_START_MINUTE_OF_DAY + slot * START_SLOT_MINUTES
|
||||
return {&"day": day, &"kind": kind, &"roll": roll, &"slot": slot,
|
||||
&"start": start, &"end": start + int(DURATION_MINUTES[kind])}
|
||||
|
||||
## A saved day's draw must be this rule's own result (SPEC section 5.3): the roll
|
||||
## picks the kind, a clear day has no slot, and any other day starts on its slot
|
||||
## and lasts its kind's duration. An empty draw (no day drawn yet) is consistent.
|
||||
static func is_consistent_draw(draw: Dictionary) -> bool:
|
||||
if draw.is_empty():
|
||||
return true
|
||||
var roll := int(draw[&"roll"])
|
||||
var kind := StringName(draw[&"kind"])
|
||||
var expected: StringName = &"clear"
|
||||
if roll >= FOG_BELOW:
|
||||
expected = &"storm"
|
||||
elif roll >= RAIN_BELOW:
|
||||
expected = &"mana_fog"
|
||||
elif roll >= CLEAR_BELOW:
|
||||
expected = &"rain"
|
||||
if kind != expected:
|
||||
return false
|
||||
if kind == &"clear":
|
||||
return int(draw[&"slot"]) == -1 and int(draw[&"start"]) == -1 and int(draw[&"end"]) == -1
|
||||
var slot := int(draw[&"slot"])
|
||||
var start := int(draw[&"day"]) * DAY_MINUTES + FIRST_START_MINUTE_OF_DAY + slot * START_SLOT_MINUTES
|
||||
return slot >= 0 and slot < START_SLOTS and int(draw[&"start"]) == start \
|
||||
and int(draw[&"end"]) == start + int(DURATION_MINUTES[kind])
|
||||
|
||||
static func arrival_percent(kind: StringName) -> int:
|
||||
return RAIN_ARRIVAL_PERCENT if kind == &"rain" else 100
|
||||
|
||||
static func mana_yield(base_amount: int, kind: StringName) -> int:
|
||||
if kind != &"mana_fog":
|
||||
return base_amount
|
||||
return int(floor(float(base_amount * FOG_MANA_NUMERATOR) / FOG_MANA_DENOMINATOR))
|
||||
|
||||
static func fog_intimacy_increase(increase: int) -> int:
|
||||
if increase <= 0:
|
||||
return increase
|
||||
return int(floor(float(increase * FOG_INTIMACY_NUMERATOR) / FOG_INTIMACY_DENOMINATOR))
|
||||
|
||||
## Japanese HUD guidance for the current kind (SPEC section 4.6).
|
||||
static func effect_text(kind: StringName) -> String:
|
||||
match kind:
|
||||
&"rain":
|
||||
return "雨: 新規来客が半分になり、帰る予定の客は雨が止むまで(最大4時間)残ります。"
|
||||
&"storm":
|
||||
return "嵐: 新規来客はありません。館内の客は残ります。"
|
||||
&"mana_fog":
|
||||
return "マナ霧: 世界樹のマナ産出が1.2倍、成人確認済みの客の性欲の増え方が1.5倍になります。"
|
||||
return "晴れ: 天候の影響はありません。"
|
||||
@@ -0,0 +1 @@
|
||||
uid://bkr55x5to4cgi
|
||||
@@ -2,14 +2,25 @@ extends RefCounted
|
||||
|
||||
## Weather state and scheduled transitions. The caller owns generation policy;
|
||||
## this model invents neither probabilities nor rain/fog balance multipliers.
|
||||
const VERSION := 1
|
||||
## Version 2 also keeps the natural-weather bookkeeping (last daily draw, the
|
||||
## current day's draw) and the once-per-guest rain holds; version 1 snapshots
|
||||
## migrate with no draw for the past and no holds (P07-01 SPEC section 5).
|
||||
const VERSION := 2
|
||||
const LEGACY_VERSION := 1
|
||||
const KINDS := [&"clear", &"rain", &"storm", &"mana_fog"]
|
||||
const MAX_MINUTE := 2147483647
|
||||
const MAX_PENDING := 4096
|
||||
const MAX_RAIN_HOLDS := 100000
|
||||
const DAY_MINUTES := 1440
|
||||
const DRAW_MINUTE_OF_DAY := 360
|
||||
const RAIN_HOLD_MAX_MINUTES := 240
|
||||
var _minute := -1
|
||||
var _kind: StringName = &""
|
||||
var _since := -1
|
||||
var _pending: Array[Dictionary] = []
|
||||
var _last_draw_day := -1
|
||||
var _today_draw: Dictionary = {}
|
||||
var _rain_holds: Dictionary = {}
|
||||
|
||||
func initialize(kind: Variant, minute: Variant) -> bool:
|
||||
if _minute >= 0 or not _valid_kind(kind) or not _valid_minute(minute):
|
||||
@@ -17,6 +28,7 @@ func initialize(kind: Variant, minute: Variant) -> bool:
|
||||
_minute = minute
|
||||
_since = minute
|
||||
_kind = StringName(kind)
|
||||
_last_draw_day = _days_before(minute)
|
||||
return true
|
||||
|
||||
func get_kind() -> StringName:
|
||||
@@ -25,6 +37,9 @@ func get_kind() -> StringName:
|
||||
func get_minute() -> int:
|
||||
return _minute
|
||||
|
||||
func get_since() -> int:
|
||||
return _since
|
||||
|
||||
func permits_new_arrivals() -> bool:
|
||||
return _minute >= 0 and _kind != &"storm"
|
||||
|
||||
@@ -55,13 +70,100 @@ func advance_to(minute: Variant) -> Dictionary:
|
||||
_minute = minute
|
||||
return {&"ok": true, &"replayed": replayed, &"changes": changes}
|
||||
|
||||
## Natural weather (A13): the caller draws once per day at 06:00 and applies
|
||||
## the result here. A start at the current minute switches now; later starts
|
||||
## and the return to clear use the ordinary schedule rules, applied together.
|
||||
func get_last_draw_day() -> int:
|
||||
return _last_draw_day
|
||||
|
||||
func get_today_draw() -> Dictionary:
|
||||
return _today_draw.duplicate(true)
|
||||
|
||||
func apply_draw(draw: Dictionary) -> Dictionary:
|
||||
if _minute < 0 or not _valid_draw(draw):
|
||||
return {&"ok": false, &"code": &"INVALID_DRAW"}
|
||||
var day: int = draw[&"day"]
|
||||
if day <= _last_draw_day or _minute != day * DAY_MINUTES + DRAW_MINUTE_OF_DAY:
|
||||
return {&"ok": false, &"code": &"DRAW_NOT_DUE"}
|
||||
var kind := StringName(draw[&"kind"])
|
||||
var start: int = draw[&"start"]
|
||||
var end: int = draw[&"end"]
|
||||
var changes: Array[Dictionary] = []
|
||||
if kind != &"clear":
|
||||
for row: Dictionary in _pending:
|
||||
if int(row[&"minute"]) == start or int(row[&"minute"]) == end:
|
||||
return {&"ok": false, &"code": &"SCHEDULE_CONFLICT"}
|
||||
if _pending.size() + 2 > MAX_PENDING:
|
||||
return {&"ok": false, &"code": &"SCHEDULE_FULL"}
|
||||
if start == _minute:
|
||||
if kind != _kind:
|
||||
changes.append({&"from": _kind, &"to": kind, &"minute": start})
|
||||
_kind = kind
|
||||
_since = start
|
||||
else:
|
||||
_pending.append({&"kind": kind, &"minute": start})
|
||||
_pending.append({&"kind": &"clear", &"minute": end})
|
||||
_pending.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a[&"minute"] < b[&"minute"])
|
||||
_last_draw_day = day
|
||||
_today_draw = _copy_draw(draw)
|
||||
return {&"ok": true, &"changes": changes}
|
||||
|
||||
## The next scheduled change away from the current kind, for the forecast.
|
||||
func next_change() -> Dictionary:
|
||||
for row: Dictionary in _pending:
|
||||
if row[&"kind"] != _kind:
|
||||
return {&"kind": row[&"kind"], &"minute": row[&"minute"]}
|
||||
return {}
|
||||
|
||||
## When the current rain ends (-1 when it is not raining or no end is known).
|
||||
func rain_end_minute() -> int:
|
||||
if _kind != &"rain":
|
||||
return -1
|
||||
for row: Dictionary in _pending:
|
||||
if row[&"kind"] != &"rain":
|
||||
return int(row[&"minute"])
|
||||
return -1
|
||||
|
||||
func get_rain_hold(guest_id: StringName) -> Dictionary:
|
||||
var row: Variant = _rain_holds.get(String(guest_id), null)
|
||||
return row.duplicate(true) if row is Dictionary else {}
|
||||
|
||||
func get_rain_holds() -> Dictionary:
|
||||
return _rain_holds.duplicate(true)
|
||||
|
||||
func add_rain_hold(guest_id: StringName, original_exit: int, extended_to: int) -> bool:
|
||||
var key := String(guest_id)
|
||||
if key.is_empty() or _rain_holds.has(key) or _rain_holds.size() >= MAX_RAIN_HOLDS \
|
||||
or not _valid_hold({"original_exit": original_exit, "extended_to": extended_to}):
|
||||
return false
|
||||
_rain_holds[key] = {"original_exit": original_exit, "extended_to": extended_to}
|
||||
return true
|
||||
|
||||
## Holds of guests that have left are history; the caller passes who remains.
|
||||
func prune_rain_holds(present_guest_ids: Dictionary) -> void:
|
||||
for key: String in _rain_holds.keys():
|
||||
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)}
|
||||
&"since": _since, &"pending": _pending.duplicate(true),
|
||||
&"natural": {&"last_draw_day": _last_draw_day, &"today": _today_draw.duplicate(true)},
|
||||
&"rain_holds": _rain_holds.duplicate(true)}
|
||||
|
||||
func restore_snapshot(source: Dictionary) -> bool:
|
||||
if not _keys(source, ["version", "minute", "kind", "since", "pending"]) \
|
||||
or typeof(source["version"]) != TYPE_INT or source["version"] != VERSION \
|
||||
var version: Variant = source.get("version", source.get(&"version", null))
|
||||
if typeof(version) != TYPE_INT or version not in [LEGACY_VERSION, VERSION]:
|
||||
return false
|
||||
var expected := ["version", "minute", "kind", "since", "pending"]
|
||||
if version == VERSION:
|
||||
expected = expected + ["natural", "rain_holds"]
|
||||
if not _keys(source, expected) \
|
||||
or not _valid_minute(source["minute"]) or not _valid_minute(source["since"]) \
|
||||
or source["since"] > source["minute"] or not _valid_kind(source["kind"]) \
|
||||
or not source["pending"] is Array or source["pending"].size() > MAX_PENDING:
|
||||
@@ -75,12 +177,75 @@ func restore_snapshot(source: Dictionary) -> bool:
|
||||
return false
|
||||
previous = value["minute"]
|
||||
pending.append({&"kind": StringName(value["kind"]), &"minute": value["minute"]})
|
||||
var last_draw_day := _days_before(source["minute"])
|
||||
var today: Dictionary = {}
|
||||
var holds: Dictionary = {}
|
||||
if version == VERSION:
|
||||
var natural: Variant = source["natural"]
|
||||
if not natural is Dictionary or not _keys(natural, ["last_draw_day", "today"]) \
|
||||
or typeof(natural["last_draw_day"]) != TYPE_INT or natural["last_draw_day"] < -1 \
|
||||
or natural["last_draw_day"] > _day_of(source["minute"]) or not natural["today"] is Dictionary:
|
||||
return false
|
||||
last_draw_day = natural["last_draw_day"]
|
||||
if not natural["today"].is_empty():
|
||||
if not _valid_draw(natural["today"]) or int(natural["today"]["day"]) != last_draw_day:
|
||||
return false
|
||||
today = _copy_draw(natural["today"])
|
||||
var raw_holds: Variant = source["rain_holds"]
|
||||
if not raw_holds is Dictionary or raw_holds.size() > MAX_RAIN_HOLDS:
|
||||
return false
|
||||
for key: Variant in raw_holds:
|
||||
if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME] or String(key).is_empty() \
|
||||
or not raw_holds[key] is Dictionary or not _keys(raw_holds[key], ["original_exit", "extended_to"]) \
|
||||
or not _valid_hold(raw_holds[key]):
|
||||
return false
|
||||
holds[String(key)] = {"original_exit": int(raw_holds[key]["original_exit"]),
|
||||
"extended_to": int(raw_holds[key]["extended_to"])}
|
||||
_minute = source["minute"]
|
||||
_kind = StringName(source["kind"])
|
||||
_since = source["since"]
|
||||
_pending = pending
|
||||
_last_draw_day = last_draw_day
|
||||
_today_draw = today
|
||||
_rain_holds = holds
|
||||
return true
|
||||
|
||||
## A day whose 06:00 is before the minute counts as drawn, so a new or migrated
|
||||
## timeline never draws for the past (SPEC section 5.2). At exactly 06:00 the
|
||||
## day is still due, which lets a new game draw its opening day.
|
||||
static func _days_before(minute: int) -> int:
|
||||
var day := _day_of(minute)
|
||||
return day if posmod(minute, DAY_MINUTES) > DRAW_MINUTE_OF_DAY else day - 1
|
||||
|
||||
static func _day_of(minute: int) -> int:
|
||||
return int(floor(float(minute) / DAY_MINUTES))
|
||||
|
||||
static func _valid_draw(draw: Variant) -> bool:
|
||||
if not draw is Dictionary or not _keys(draw, ["day", "kind", "roll", "slot", "start", "end"]):
|
||||
return false
|
||||
for key: String in ["day", "roll", "slot", "start", "end"]:
|
||||
if typeof(draw[key]) != TYPE_INT:
|
||||
return false
|
||||
if not _valid_kind(draw["kind"]) or draw["day"] < 0 or draw["roll"] < 0 or draw["roll"] > 99:
|
||||
return false
|
||||
if StringName(draw["kind"]) == &"clear":
|
||||
return draw["slot"] == -1 and draw["start"] == -1 and draw["end"] == -1
|
||||
var day_start: int = draw["day"] * DAY_MINUTES + DRAW_MINUTE_OF_DAY
|
||||
return draw["slot"] >= 0 and draw["start"] >= day_start and draw["start"] <= day_start + 14 * 60 \
|
||||
and draw["end"] > draw["start"] and draw["end"] < day_start + DAY_MINUTES
|
||||
|
||||
static func _copy_draw(draw: Dictionary) -> Dictionary:
|
||||
var result := {}
|
||||
for key: String in ["day", "kind", "roll", "slot", "start", "end"]:
|
||||
result[key] = draw[key] if key != "kind" else String(draw[key])
|
||||
return result
|
||||
|
||||
static func _valid_hold(row: Dictionary) -> bool:
|
||||
var original: Variant = row.get("original_exit", null)
|
||||
var extended: Variant = row.get("extended_to", null)
|
||||
return typeof(original) == TYPE_INT and typeof(extended) == TYPE_INT and original >= 0 \
|
||||
and extended > original and extended <= original + RAIN_HOLD_MAX_MINUTES
|
||||
|
||||
static func _valid_kind(value: Variant) -> bool:
|
||||
return typeof(value) in [TYPE_STRING, TYPE_STRING_NAME] and StringName(value) in KINDS
|
||||
|
||||
|
||||
@@ -365,6 +365,10 @@ func _update_needs_runtime(
|
||||
delta_medical += int(normalized_effects.get(&"medical", 0))
|
||||
delta_leisure += int(normalized_effects.get(&"leisure", 0))
|
||||
delta_intimacy += int(normalized_effects.get(&"intimacy", 0))
|
||||
## A13: mana fog raises an adult-verified guest's positive intimacy increase
|
||||
## to floor(x1.5) = n + floor(n / 2); decreases and other guests are unchanged.
|
||||
if definition.adult_verified and delta_intimacy > 0 and bool(_value(context, &"mana_fog", false)):
|
||||
delta_intimacy += delta_intimacy >> 1
|
||||
|
||||
var after_hunger := clampi(before_hunger + delta_hunger, 0, 100)
|
||||
var after_sleep := clampi(before_sleep + delta_sleep, 0, 100)
|
||||
@@ -1157,7 +1161,7 @@ func _need_context_error(context: Dictionary) -> String:
|
||||
var raw_value: Variant = _value(context, field_id, null)
|
||||
if not raw_value is int or int(raw_value) < 0 or int(raw_value) > 100:
|
||||
return "%s must be an integer from 0 through 100." % field_id
|
||||
for field_id: StringName in [&"is_night", &"work_like_activity", &"unstimulated"]:
|
||||
for field_id: StringName in [&"is_night", &"work_like_activity", &"unstimulated", &"mana_fog"]:
|
||||
if _has_value(context, field_id) and not _value(context, field_id, null) is bool:
|
||||
return "%s must be boolean." % field_id
|
||||
if _has_value(context, &"need_effects"):
|
||||
|
||||
@@ -11,6 +11,10 @@ const CAPACITY := 300
|
||||
const WORLD_TREE_GENERATION := 20
|
||||
const INTERVAL_MINUTES := 10
|
||||
const FURNACE_YIELD := 100
|
||||
## A13 mana fog: world tree and furnace yields x1.2 (floor) at fog boundaries.
|
||||
const FOG_WORLD_TREE_GENERATION := 24
|
||||
const FOG_FURNACE_YIELD := 120
|
||||
const GENERATIONS: Array[int] = [WORLD_TREE_GENERATION, FOG_WORLD_TREE_GENERATION]
|
||||
const LIMIT := 1000000000
|
||||
const FLOW_KEYS: Array[String] = [
|
||||
"from_minute", "to_minute", "before", "generation", "furnace", "demand",
|
||||
@@ -35,12 +39,14 @@ func get_last_flow() -> Dictionary:
|
||||
## Processes every boundary in (last boundary, minute]. Boundaries already
|
||||
## processed are no-ops, so per-minute callers and same-minute retries are safe.
|
||||
## Mana may rise between boundaries (event refunds, commands); anything above
|
||||
## capacity is discarded and recorded as overflow at the next boundary.
|
||||
func advance_to(minute: Variant, before: Variant, demand: Variant) -> Dictionary:
|
||||
## capacity is discarded and recorded as overflow at the next boundary. The
|
||||
## caller passes the world tree yield of the boundary (A13 fog: 24, else 20).
|
||||
func advance_to(minute: Variant, before: Variant, demand: Variant, generation: Variant = WORLD_TREE_GENERATION) -> Dictionary:
|
||||
if _minute < 0:
|
||||
return _failure(&"NOT_STARTED")
|
||||
if typeof(minute) != TYPE_INT or not _amount(before) \
|
||||
or typeof(demand) != TYPE_INT or demand < 0 or demand > LIMIT or minute < _minute:
|
||||
or typeof(demand) != TYPE_INT or demand < 0 or demand > LIMIT or minute < _minute \
|
||||
or typeof(generation) != TYPE_INT or generation not in GENERATIONS:
|
||||
return _failure(&"INVALID_FLOW")
|
||||
var boundary: int = minute - minute % INTERVAL_MINUTES
|
||||
if boundary <= _minute:
|
||||
@@ -49,7 +55,7 @@ func advance_to(minute: Variant, before: Variant, demand: Variant) -> Dictionary
|
||||
var flow := {}
|
||||
var count := 0
|
||||
for to_minute: int in range(_minute + INTERVAL_MINUTES, boundary + 1, INTERVAL_MINUTES):
|
||||
flow = plan(amount, demand, 0, to_minute - INTERVAL_MINUTES, to_minute)
|
||||
flow = plan(amount, demand, 0, to_minute - INTERVAL_MINUTES, to_minute, generation)
|
||||
amount = flow[&"after"]
|
||||
count += 1
|
||||
_minute = boundary
|
||||
@@ -57,17 +63,18 @@ func advance_to(minute: Variant, before: Variant, demand: Variant) -> Dictionary
|
||||
return {&"ok": true, &"processed": true, &"boundaries": count, &"after": amount, &"flow": flow.duplicate(true)}
|
||||
|
||||
## Furnace offers are judged after natural generation and upkeep demand are
|
||||
## paid: base_after = min(C, max(0, B + G - D)); accept only with 100 free.
|
||||
static func furnace_offer_accepted(before: int, demand: int) -> bool:
|
||||
var base_after := mini(CAPACITY, maxi(0, before + WORLD_TREE_GENERATION - demand))
|
||||
return CAPACITY - base_after >= FURNACE_YIELD
|
||||
## paid: base_after = min(C, max(0, B + G - D)); accept only with the yield free
|
||||
## (100, or 120 at an A13 fog boundary where G is 24).
|
||||
static func furnace_offer_accepted(before: int, demand: int, generation: int = WORLD_TREE_GENERATION, furnace_yield: int = FURNACE_YIELD) -> bool:
|
||||
var base_after := mini(CAPACITY, maxi(0, before + generation - demand))
|
||||
return CAPACITY - base_after >= furnace_yield
|
||||
|
||||
static func plan(before: int, demand: int, furnace: int, start: int, end: int) -> Dictionary:
|
||||
var available := before + WORLD_TREE_GENERATION + furnace
|
||||
static func plan(before: int, demand: int, furnace: int, start: int, end: int, generation: int = WORLD_TREE_GENERATION) -> Dictionary:
|
||||
var available := before + generation + furnace
|
||||
var consumed := mini(available, demand)
|
||||
var remaining := available - consumed
|
||||
return {&"from_minute": start, &"to_minute": end, &"before": before,
|
||||
&"generation": WORLD_TREE_GENERATION, &"furnace": furnace, &"demand": demand,
|
||||
&"generation": generation, &"furnace": furnace, &"demand": demand,
|
||||
&"consumed": consumed, &"shortage": demand - consumed,
|
||||
&"overflow": maxi(remaining - CAPACITY, 0), &"after": mini(remaining, CAPACITY)}
|
||||
|
||||
@@ -105,10 +112,10 @@ func restore_snapshot(source: Variant) -> bool:
|
||||
if not _amount(flow[key]):
|
||||
return false
|
||||
if flow["to_minute"] != source["minute"] \
|
||||
or flow["generation"] != WORLD_TREE_GENERATION \
|
||||
or flow["generation"] not in GENERATIONS \
|
||||
or flow["from_minute"] + INTERVAL_MINUTES != flow["to_minute"]:
|
||||
return false
|
||||
var expected := plan(flow["before"], flow["demand"], flow["furnace"], flow["from_minute"], flow["to_minute"])
|
||||
var expected := plan(flow["before"], flow["demand"], flow["furnace"], flow["from_minute"], flow["to_minute"], flow["generation"])
|
||||
if not _same_flow(expected, flow):
|
||||
return false
|
||||
_minute = source["minute"]
|
||||
|
||||
@@ -1013,6 +1013,15 @@ func _process_arrival_boundary(minute: int) -> CommandResult:
|
||||
&"ARRIVAL_PLAN", minute, PHASE_CAPTURE, &"ARRIVAL_CAPTURE_INVALID"
|
||||
)
|
||||
var external_capacity := int(capacity_value)
|
||||
# A13: the arrival boundary reports the weather share of planned arrivals
|
||||
# (rain 50, otherwise 100). Absent means 100 for callers without weather.
|
||||
var percent_value: Variant = capture.get(&"arrival_percent", 100)
|
||||
if not _is_integral_value(percent_value) or int(percent_value) < 0 or int(percent_value) > 100:
|
||||
return _failed_boundary_after_capture(
|
||||
_arrival_boundary, capture_payload, token,
|
||||
&"ARRIVAL_PLAN", minute, PHASE_CAPTURE, &"ARRIVAL_CAPTURE_INVALID"
|
||||
)
|
||||
var arrival_percent := int(percent_value)
|
||||
var remaining := mini(
|
||||
mini(local_capacity, external_capacity),
|
||||
int(_config[&"max_arrivals_per_interval"])
|
||||
@@ -1022,6 +1031,8 @@ func _process_arrival_boundary(minute: int) -> CommandResult:
|
||||
var plans: Array[Dictionary] = []
|
||||
var total_count := 0
|
||||
var capped_rows: Array[Dictionary] = []
|
||||
var accepted_by_row: Array[int] = []
|
||||
var planned_total := 0
|
||||
for row: Dictionary in intent_rows:
|
||||
var accepted := mini(int(row[&"desired_count"]), remaining)
|
||||
var key: String = row[&"key"]
|
||||
@@ -1034,6 +1045,21 @@ func _process_arrival_boundary(minute: int) -> CommandResult:
|
||||
&"guest_class": String(row[&"guest_class"]),
|
||||
&"discarded_numerator": retained - capped_retained,
|
||||
})
|
||||
accepted_by_row.append(accepted)
|
||||
planned_total += accepted
|
||||
remaining -= accepted
|
||||
# Rain keeps floor(planned * percent / 100) arrivals; the dropped arrivals
|
||||
# are lost to the weather and are not carried to the next boundary.
|
||||
var dropped := planned_total - planned_total * arrival_percent / 100
|
||||
var drop_index := accepted_by_row.size() - 1
|
||||
while dropped > 0 and drop_index >= 0:
|
||||
var cut := mini(dropped, accepted_by_row[drop_index])
|
||||
accepted_by_row[drop_index] -= cut
|
||||
dropped -= cut
|
||||
drop_index -= 1
|
||||
for row_index in intent_rows.size():
|
||||
var row: Dictionary = intent_rows[row_index]
|
||||
var accepted := accepted_by_row[row_index]
|
||||
if accepted <= 0:
|
||||
continue
|
||||
if plan_sequence > MAX_SEQUENCE_VALUE \
|
||||
@@ -1058,7 +1084,6 @@ func _process_arrival_boundary(minute: int) -> CommandResult:
|
||||
})
|
||||
plan_sequence += 1
|
||||
total_count += accepted
|
||||
remaining -= accepted
|
||||
|
||||
var payload := capture_payload.duplicate(true)
|
||||
payload[&"plans"] = plans
|
||||
|
||||
@@ -276,8 +276,9 @@ func _legacy_controls(writer: Dictionary, reader: Dictionary) -> void:
|
||||
|
||||
## Approved later save migrations applied when an older save is loaded, each
|
||||
## checked against its own contract and then mapped into `expected` in place:
|
||||
## A06 starts a fresh finite Mana reserve at the restored boundary, and P14-03
|
||||
## raises the elevator snapshot from v1 to v2 with every other field unchanged.
|
||||
## A06 starts a fresh finite Mana reserve at the restored boundary, P14-03
|
||||
## raises the elevator snapshot from v1 to v2 with every other field unchanged,
|
||||
## and P07-01 A13 migrates the weather snapshot and adds the weather stream.
|
||||
func _approved_runtime_migrations(expected: Dictionary, reader: Dictionary) -> Dictionary:
|
||||
var old: Dictionary = expected[&"systems"][&"runtime"]
|
||||
var now: Dictionary = reader[&"systems"][&"runtime"]
|
||||
@@ -322,6 +323,35 @@ func _approved_runtime_migrations(expected: Dictionary, reader: Dictionary) -> D
|
||||
old[&"economy"] = economy
|
||||
fields.append("economy_A12_empty_fund_accounts")
|
||||
proof["economy_A12"] = {"added_accounts": added.keys(), "only_accounts_added": only_accounts_added}
|
||||
# P07-01 A13 (SPEC section 5.2): a save from before natural weather keeps its
|
||||
# weather and pending schedule, counts every day whose 06:00 has passed as
|
||||
# drawn (no past draw), holds no rain extension, and starts the weather
|
||||
# stream from the world seed; every other field stays byte-identical.
|
||||
var old_weather: Variant = old.get(&"weather")
|
||||
var now_weather: Variant = now.get(&"weather")
|
||||
if old_weather is Dictionary and int(old_weather.get(&"version", 0)) == 1 and now_weather is Dictionary and int(now_weather.get(&"version", 0)) == 2:
|
||||
var weather_minute := int(old_weather[&"minute"])
|
||||
var last_draw_day := floori(weather_minute / 1440.0) - (0 if posmod(weather_minute, 1440) > 360 else 1)
|
||||
var migrated: Dictionary = (old_weather as Dictionary).duplicate(true)
|
||||
migrated[&"version"] = 2
|
||||
migrated[&"natural"] = {&"last_draw_day": last_draw_day, &"today": {}}
|
||||
migrated[&"rain_holds"] = {}
|
||||
var only_natural_state_added: bool = migrated == now_weather
|
||||
ok = ok and only_natural_state_added
|
||||
old[&"weather"] = (now_weather as Dictionary).duplicate(true)
|
||||
fields.append("weather_v1_to_v2_A13")
|
||||
proof["weather_A13"] = {"last_draw_day": last_draw_day, "only_natural_state_added": only_natural_state_added}
|
||||
var old_streams: Variant = old.get(&"rng_streams")
|
||||
var now_streams: Variant = now.get(&"rng_streams")
|
||||
if old_streams is Dictionary and now_streams is Dictionary and not (old_streams as Dictionary).has("weather") and (now_streams as Dictionary).has("weather"):
|
||||
var seeded: Dictionary = DeterministicRng.from_root_seed(int(old[&"world_seed"]), &"weather").snapshot()
|
||||
var others: Dictionary = (now_streams as Dictionary).duplicate(true)
|
||||
others.erase("weather")
|
||||
var only_weather_stream_added: bool = now_streams["weather"] == seeded and var_to_bytes(others) == var_to_bytes(old_streams)
|
||||
ok = ok and only_weather_stream_added
|
||||
old[&"rng_streams"] = (now_streams as Dictionary).duplicate(true)
|
||||
fields.append("rng_streams_weather_from_world_seed_A13")
|
||||
proof["rng_streams_A13"] = {"only_weather_stream_added": only_weather_stream_added}
|
||||
if not fields.is_empty():
|
||||
# Saves keep the Runtime's own key order, so the mapped tree follows it.
|
||||
ok = ok and old.size() == now.size()
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
extends SceneTree
|
||||
|
||||
## P07-01 F7, writer phase. Plays the minute script, writes ordinary save files
|
||||
## mid-rain, just before and after the rain end and just before and after the
|
||||
## day 1 draw, and hands the exact save packages to the reader suite
|
||||
## (test_p07_weather_restart_2_read.gd), which runs as a separate engine process.
|
||||
## The fog scene (WX7) writes saves around the 07:00 fog mana boundary and the
|
||||
## fog end, reloads each one in this process and continues it, and hands those
|
||||
## packages to the reader too.
|
||||
## The handoff is the declared generated report res://artifacts/p07-01/restart-handoff.json
|
||||
## (tools/regression/suite-policy.json fixed_generated_report_routes).
|
||||
|
||||
const Fixture := preload("res://tests/helpers/p07_weather_restart_fixture.gd")
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("P07-01: restart writer")
|
||||
var fixtures := Fixture.new(_expect)
|
||||
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": &"", "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
|
||||
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"] == 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()
|
||||
var saving_runtime: Variant = saving[&"runtime"]
|
||||
var saved_states := {}
|
||||
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:
|
||||
_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 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,
|
||||
"writer_pid": OS.get_process_id(),
|
||||
"writer_source_sha256": FileAccess.get_sha256(Fixture.WRITER_SOURCE),
|
||||
"seed": Fixture.SEED,
|
||||
"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,
|
||||
}
|
||||
DirAccess.make_dir_recursive_absolute(Fixture.HANDOFF_PATH.get_base_dir())
|
||||
var file := FileAccess.open(Fixture.HANDOFF_PATH, FileAccess.WRITE)
|
||||
_expect(file != null, "handoff file opens for writing")
|
||||
if file != null:
|
||||
file.store_string(JSON.stringify(handoff, "\t") + "\n")
|
||||
file.close()
|
||||
print("P07_RESTART_WRITER pid=%d handoff_sha256=%s" % [OS.get_process_id(), FileAccess.get_sha256(Fixture.HANDOFF_PATH)])
|
||||
fixtures.free_fixture(straight)
|
||||
fixtures.free_fixture(saving)
|
||||
await process_frame
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
else:
|
||||
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
||||
quit(_failures)
|
||||
|
||||
|
||||
## WX7 fog scene: the uninterrupted run, the run that writes the save files, and
|
||||
## a same-process reload of every save file continued to the end.
|
||||
func _fog_scene(fixtures: Variant) -> Dictionary:
|
||||
var straight: Dictionary = fixtures.new_fixture(false, Fixture.FOG_SEED)
|
||||
var runtime: Variant = straight[&"runtime"]
|
||||
_expect(runtime.weather.get_kind() == &"mana_fog" and int(runtime.weather.get_today_draw().get("end", -1)) == 600, "seed 1592 opens the fog scene with mana fog 06:00-10:00")
|
||||
var seen := {"intimacy": {}, "rises": 0, "fog_rises": 0, "rises_ok": true, "flows": 0, "fog_flows": 0, "flows_ok": true, "after": {}}
|
||||
fixtures.play(straight, Fixture.FIRST_MINUTE, Fixture.FOG_LAST_MINUTE, func(minute: int) -> void:
|
||||
var fog_now: bool = runtime.weather.get_kind() == &"mana_fog"
|
||||
var last: Dictionary = seen["intimacy"]
|
||||
for guest_id: StringName in Fixture.FOG_ADULTS.values():
|
||||
var guest: Variant = runtime.guest_service.get_guest(guest_id)
|
||||
if guest == null:
|
||||
continue
|
||||
var now := int(guest.needs[&"intimacy"])
|
||||
if last.has(guest_id) and now < 100:
|
||||
seen["rises"] += 1
|
||||
seen["rises_ok"] = seen["rises_ok"] and now - int(last[guest_id]) == (4 if fog_now else 3)
|
||||
if fog_now:
|
||||
seen["fog_rises"] += 1
|
||||
last[guest_id] = now
|
||||
if minute % 10 == 0:
|
||||
seen["flows"] += 1
|
||||
seen["flows_ok"] = seen["flows_ok"] and int(runtime.mana_reserve.get_last_flow().get(&"generation", -1)) == (24 if fog_now else 20)
|
||||
if fog_now:
|
||||
seen["fog_flows"] += 1
|
||||
if minute - 1 in Fixture.FOG_SAVE_MINUTES:
|
||||
seen["after"][minute] = fixtures.state_bytes(runtime))
|
||||
_expect(seen["rises_ok"] and int(seen["fog_rises"]) >= 3 and int(seen["rises"]) - int(seen["fog_rises"]) >= 2,
|
||||
"adult-verified intimacy rises by 4 a minute in fog and 3 after it (%d rises, %d in fog)" % [int(seen["rises"]), int(seen["fog_rises"])])
|
||||
_expect(seen["flows_ok"] and int(seen["fog_flows"]) >= 20 and int(seen["flows"]) > int(seen["fog_flows"]),
|
||||
"each 10-minute mana boundary adds 24 in fog and 20 after it (%d boundaries, %d in fog)" % [int(seen["flows"]), int(seen["fog_flows"])])
|
||||
var saving: Dictionary = fixtures.new_fixture(false, Fixture.FOG_SEED)
|
||||
var saving_runtime: Variant = saving[&"runtime"]
|
||||
var states := {}
|
||||
var paths := {}
|
||||
fixtures.play(saving, Fixture.FIRST_MINUTE, Fixture.FOG_LAST_MINUTE, func(minute: int) -> void:
|
||||
if minute in Fixture.FOG_SAVE_MINUTES:
|
||||
var path := Fixture.FOG_WRITER_SAVE % minute
|
||||
states[minute] = fixtures.state_bytes(saving_runtime)
|
||||
_expect(bool(saving_runtime.save_to_path(path).get(&"ok", false)), "fog save file at minute %d" % minute)
|
||||
paths[minute] = path)
|
||||
var final_state: PackedByteArray = fixtures.state_bytes(runtime)
|
||||
_expect(fixtures.state_bytes(saving_runtime) == final_state, "writing the fog save files leaves the state as without saving")
|
||||
for minute: int in Fixture.FOG_SAVE_MINUTES:
|
||||
var reload: Dictionary = fixtures.new_fixture(false, Fixture.FOG_SEED)
|
||||
var reload_runtime: Variant = reload[&"runtime"]
|
||||
_expect(bool(reload_runtime.load_from_path(str(paths.get(minute, Fixture.FOG_WRITER_SAVE % minute))).get(&"ok", false))
|
||||
and fixtures.state_bytes(reload_runtime) == states.get(minute, PackedByteArray()) and reload[&"clock"].absolute_minute == minute,
|
||||
"this process reloads the fog save file of minute %d exactly" % minute)
|
||||
fixtures.play(reload, minute, minute + 1, Callable())
|
||||
_expect(fixtures.state_bytes(reload_runtime) == seen["after"].get(minute + 1, PackedByteArray()),
|
||||
"one minute after the minute %d fog reload equals the uninterrupted run (the fog yield and intimacy apply once)" % minute)
|
||||
fixtures.play(reload, minute + 1, Fixture.FOG_LAST_MINUTE, Callable())
|
||||
_expect(fixtures.state_bytes(reload_runtime) == final_state, "the minute %d fog reload continues to the uninterrupted final state" % minute)
|
||||
fixtures.free_fixture(reload)
|
||||
var after := {}
|
||||
for minute: int in seen["after"]:
|
||||
var bytes: PackedByteArray = seen["after"][minute]
|
||||
after[str(minute)] = {"sha256": Fixture.sha256(bytes), "base64": Marshalls.raw_to_base64(bytes)}
|
||||
var saves := _packages(Fixture.FOG_SAVE_MINUTES, paths, states, Fixture.FOG_WRITER_SAVE)
|
||||
fixtures.free_fixture(straight)
|
||||
fixtures.free_fixture(saving)
|
||||
return {
|
||||
"seed": Fixture.FOG_SEED,
|
||||
"last_minute": Fixture.FOG_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,
|
||||
}
|
||||
|
||||
|
||||
## The exact save packages and the compared state at each save minute.
|
||||
func _packages(minutes: Array[int], paths: Dictionary, states: Dictionary, path_format: String) -> Array:
|
||||
var saves: Array = []
|
||||
for minute: int in minutes:
|
||||
var directory := ProjectSettings.globalize_path(str(paths.get(minute, path_format % minute)))
|
||||
_expect(DirAccess.dir_exists_absolute(directory) and DirAccess.get_directories_at(directory).is_empty(), "save package %s holds only files" % (path_format % minute))
|
||||
var files := {}
|
||||
for name: String in DirAccess.get_files_at(directory):
|
||||
var bytes := FileAccess.get_file_as_bytes(directory.path_join(name))
|
||||
files[name] = {"sha256": Fixture.sha256(bytes), "bytes": bytes.size(), "base64": Marshalls.raw_to_base64(bytes)}
|
||||
_expect(files.has("manifest.json") and files.has("world.bin"), "save package %s has its manifest and world" % (path_format % minute))
|
||||
var state: PackedByteArray = states.get(minute, PackedByteArray())
|
||||
saves.append({"minute": minute, "files": files, "state_sha256": Fixture.sha256(state), "state_base64": Marshalls.raw_to_base64(state)})
|
||||
return saves
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
_checks += 1
|
||||
if not condition:
|
||||
_failures += 1
|
||||
push_error(message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bz9x67zdqdrc6
|
||||
@@ -0,0 +1,138 @@
|
||||
extends SceneTree
|
||||
|
||||
## P07-01 F7, reader phase (a separate engine process after the writer). Each
|
||||
## handed-off save package is loaded into a fresh Runtime, must equal the writer's
|
||||
## state at that minute, and must continue to the writer's uninterrupted final
|
||||
## state, with every remaining 06:00 draw happening exactly once. The fog scene
|
||||
## (WX7) packages must also equal the writer's state one minute after each save,
|
||||
## so the fog mana yield and the adult intimacy apply exactly once across the
|
||||
## process boundary.
|
||||
|
||||
const Fixture := preload("res://tests/helpers/p07_weather_restart_fixture.gd")
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("P07-01: restart reader")
|
||||
var handoff := _read_handoff()
|
||||
if not handoff.is_empty():
|
||||
_check_restarts(handoff)
|
||||
_check_fog(handoff.get("fog", {}))
|
||||
await process_frame
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
else:
|
||||
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
||||
quit(_failures)
|
||||
|
||||
|
||||
func _read_handoff() -> Dictionary:
|
||||
_expect(FileAccess.file_exists(Fixture.HANDOFF_PATH), "the writer phase left its handoff (run test_p07_weather_restart_1_write.gd first)")
|
||||
if not FileAccess.file_exists(Fixture.HANDOFF_PATH):
|
||||
return {}
|
||||
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(Fixture.HANDOFF_PATH))
|
||||
_expect(parsed is Dictionary and str((parsed as Dictionary).get("schema", "")) == Fixture.HANDOFF_SCHEMA, "handoff schema")
|
||||
if not parsed is Dictionary:
|
||||
return {}
|
||||
var handoff: Dictionary = parsed
|
||||
var writer_pid := int(handoff.get("writer_pid", 0))
|
||||
_expect(writer_pid > 0 and writer_pid != OS.get_process_id(), "the writer ran in another engine process")
|
||||
_expect(str(handoff.get("writer_source_sha256", "")) == FileAccess.get_sha256(Fixture.WRITER_SOURCE), "the handoff comes from the current writer suite")
|
||||
_expect(int(handoff.get("seed", -1)) == Fixture.SEED and int(handoff.get("last_minute", -1)) == Fixture.LAST_MINUTE, "same fixture and minute script")
|
||||
print("P07_RESTART_READER pid=%d writer_pid=%d handoff_sha256=%s" % [OS.get_process_id(), writer_pid, FileAccess.get_sha256(Fixture.HANDOFF_PATH)])
|
||||
return handoff
|
||||
|
||||
|
||||
func _check_restarts(handoff: Dictionary) -> void:
|
||||
var fixtures := Fixture.new(_expect)
|
||||
var final_state := Marshalls.base64_to_raw(str(handoff.get("straight_final_state_base64", "")))
|
||||
_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, 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:
|
||||
expected_draws += 1
|
||||
_expect(fixture[&"draws"][&"count"] == expected_draws, "after the minute %d save the remaining 06:00 draws happen once (%d)" % [minute, expected_draws])
|
||||
_expect(fixtures.state_bytes(runtime) == final_state, "continuing from minute %d reaches the uninterrupted final state" % minute)
|
||||
fixtures.free_fixture(fixture)
|
||||
SaveService.delete_save(path)
|
||||
|
||||
|
||||
func _check_fog(fog: Dictionary) -> void:
|
||||
var fixtures := Fixture.new(_expect)
|
||||
_expect(int(fog.get("seed", -1)) == Fixture.FOG_SEED and int(fog.get("last_minute", -1)) == Fixture.FOG_LAST_MINUTE, "same fog scene and minute script")
|
||||
var final_state := Marshalls.base64_to_raw(str(fog.get("straight_final_state_base64", "")))
|
||||
_expect(not final_state.is_empty() and Fixture.sha256(final_state) == str(fog.get("straight_final_state_sha256", "")), "the writer's fog final state arrived intact")
|
||||
var saves: Array = fog.get("saves", [])
|
||||
_expect(saves.size() == Fixture.FOG_SAVE_MINUTES.size(), "every fog save package was handed off")
|
||||
var after: Dictionary = fog.get("after_states", {})
|
||||
for save: Variant in saves:
|
||||
var minute := int((save as Dictionary).get("minute", -1))
|
||||
var path := Fixture.FOG_READER_SAVE % minute
|
||||
_expect(_write_package(save, path), "fog 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, Fixture.FOG_SEED)
|
||||
var runtime: Variant = fixture[&"runtime"]
|
||||
_expect(bool(runtime.load_from_path(path).get(&"ok", false)) and fixtures.state_bytes(runtime) == expected_state and fixture[&"clock"].absolute_minute == minute,
|
||||
"the reader resumes the minute %d fog save exactly" % minute)
|
||||
fixtures.play(fixture, minute, minute + 1, Callable())
|
||||
_expect(fixtures.state_bytes(runtime) == next_state, "one minute after the minute %d fog save the reader equals the uninterrupted run (no double fog yield or intimacy)" % minute)
|
||||
fixtures.play(fixture, minute + 1, Fixture.FOG_LAST_MINUTE, Callable())
|
||||
_expect(fixtures.state_bytes(runtime) == final_state, "continuing the minute %d fog save reaches the uninterrupted final state" % minute)
|
||||
fixtures.free_fixture(fixture)
|
||||
SaveService.delete_save(path)
|
||||
|
||||
|
||||
## Writes one handed-off save package byte for byte; true when every file matches its pin.
|
||||
func _write_package(save: Dictionary, path: String) -> bool:
|
||||
var directory := ProjectSettings.globalize_path(path)
|
||||
DirAccess.make_dir_recursive_absolute(directory)
|
||||
var files: Dictionary = save.get("files", {})
|
||||
var intact := not files.is_empty()
|
||||
for name: String in files:
|
||||
var bytes := Marshalls.base64_to_raw(str(files[name].get("base64", "")))
|
||||
intact = intact and Fixture.sha256(bytes) == str(files[name].get("sha256", "")) and bytes.size() == int(files[name].get("bytes", -1))
|
||||
var file := FileAccess.open(directory.path_join(name), FileAccess.WRITE)
|
||||
if file != null:
|
||||
file.store_buffer(bytes)
|
||||
file.close()
|
||||
return intact
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
_checks += 1
|
||||
if not condition:
|
||||
_failures += 1
|
||||
push_error(message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bni9w8axk6f97
|
||||
@@ -0,0 +1,403 @@
|
||||
extends SceneTree
|
||||
|
||||
## P07-01 (rain, mana fog and natural weather) F1-F7 on the real Runtime with the
|
||||
## approved initial values A13 (SPEC sections 4 and 5). F6 reads the ordinary Main
|
||||
## HUD. The seeds open day 0 with clear (1000), rain (1106), mana fog (1592) and
|
||||
## storm (2501) starting at 06:00; the draws are asserted, not assumed.
|
||||
|
||||
const NaturalWeather := preload("res://scripts/environment/natural_weather_policy.gd")
|
||||
const WeatherTimeline := preload("res://scripts/environment/weather_timeline.gd")
|
||||
const ManaReserveCycle := preload("res://scripts/mana/mana_reserve_cycle.gd")
|
||||
const AudioTeardown := preload("res://tests/helpers/ui_audio_teardown.gd")
|
||||
const SEED_CLEAR := 1000
|
||||
const SEED_RAIN := 1106
|
||||
const SEED_FOG := 1592
|
||||
const SEED_STORM := 2501
|
||||
|
||||
var _checks := 0
|
||||
var _failures := 0
|
||||
var _fixtures: Array[Dictionary] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
print("P07-01: natural weather runtime")
|
||||
_test_policy_draws()
|
||||
_test_daily_natural_draw()
|
||||
_test_rain_arrivals()
|
||||
_test_rain_stay_and_saves()
|
||||
_test_rain_hold_exit()
|
||||
_test_storm()
|
||||
_test_mana_fog()
|
||||
_test_migration_and_rejection()
|
||||
await _test_hud_forecast()
|
||||
_release_fixtures()
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
else:
|
||||
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
|
||||
quit(_failures)
|
||||
|
||||
|
||||
func _new_runtime(seed: int, hour := 6, minute := 0) -> Dictionary:
|
||||
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
|
||||
var clock: Variant = load("res://autoload/game_clock.gd").new()
|
||||
var state: Variant = load("res://autoload/game_state.gd").new()
|
||||
clock.reset(1, hour, minute)
|
||||
state.reset()
|
||||
_expect(runtime.initialize(clock, state, seed)[&"valid"], "initialize Runtime with seed %d" % seed)
|
||||
clock.set_paused(true)
|
||||
var fixture := {&"runtime": runtime, &"clock": clock, &"state": state}
|
||||
_fixtures.append(fixture)
|
||||
return fixture
|
||||
|
||||
|
||||
func _release_fixtures() -> void:
|
||||
for fixture: Dictionary in _fixtures:
|
||||
for key: StringName in [&"runtime", &"clock", &"state"]:
|
||||
if is_instance_valid(fixture[key]):
|
||||
fixture[key].free()
|
||||
_fixtures.clear()
|
||||
|
||||
|
||||
func _drawn_events(runtime: Variant) -> Array:
|
||||
var found: Array = []
|
||||
for event: SimulationDomainEvent in runtime.domain_events.recent_events():
|
||||
if event.event_type == &"WEATHER_FORECAST_DRAWN":
|
||||
found.append(event.payload.duplicate(true))
|
||||
return found
|
||||
|
||||
|
||||
func _spawn(runtime: Variant, guest_id: StringName, guest_type: StringName, planned_exit_at: int) -> Variant:
|
||||
var result: CommandResult = runtime.submit_command(&"spawn_guest", {
|
||||
&"guest_id": guest_id,
|
||||
&"guest_type_id": guest_type,
|
||||
&"gold": 100,
|
||||
&"options": {
|
||||
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
|
||||
&"needs": {&"hunger": 10, &"sleep": 10, &"hygiene": 10, &"medical": 0, &"leisure": 10, &"intimacy": 0},
|
||||
&"planned_exit_at": planned_exit_at,
|
||||
},
|
||||
}, &"world", guest_id, 10, -1, StringName("p07." + String(guest_id)))
|
||||
_expect(result.is_success(), "spawn %s" % guest_id)
|
||||
return runtime.guest_service.get_guest(guest_id)
|
||||
|
||||
|
||||
## F1: the A13 draw rule, determinism and the once-per-day timeline rule.
|
||||
func _test_policy_draws() -> void:
|
||||
var rng := DeterministicRng.from_root_seed(77, &"weather") as DeterministicRng
|
||||
var counts := {&"clear": 0, &"rain": 0, &"mana_fog": 0, &"storm": 0}
|
||||
var rule_ok := true
|
||||
for day in range(2000):
|
||||
var draw: Dictionary = NaturalWeather.draw(rng, day)
|
||||
var kind: StringName = draw[&"kind"]
|
||||
counts[kind] += 1
|
||||
var roll: int = draw[&"roll"]
|
||||
var expected_kind: StringName = &"clear" if roll < 60 else (&"rain" if roll < 85 else (&"mana_fog" if roll < 95 else &"storm"))
|
||||
rule_ok = rule_ok and kind == expected_kind and roll >= 0 and roll <= 99
|
||||
if kind == &"clear":
|
||||
rule_ok = rule_ok and draw[&"slot"] == -1 and draw[&"start"] == -1 and draw[&"end"] == -1
|
||||
else:
|
||||
var offset: int = int(draw[&"start"]) - day * 1440 - 360
|
||||
rule_ok = rule_ok and offset >= 0 and offset <= 14 * 60 and offset % 30 == 0 and offset / 30 == int(draw[&"slot"]) \
|
||||
and int(draw[&"end"]) - int(draw[&"start"]) == int(NaturalWeather.DURATION_MINUTES[kind])
|
||||
_expect(rule_ok, "every draw maps rolls 0-59/60-84/85-94/95-99 to clear/rain/fog/storm, starts 06:00-20:00 in 30-minute steps and lasts 6/4/2 hours")
|
||||
_expect(counts[&"clear"] > 1100 and counts[&"clear"] < 1300 and counts[&"rain"] > 400 and counts[&"rain"] < 600
|
||||
and counts[&"mana_fog"] > 140 and counts[&"mana_fog"] < 260 and counts[&"storm"] > 60 and counts[&"storm"] < 140,
|
||||
"2000 draws follow 60/25/10/5 percent (%s)" % str(counts))
|
||||
var left := DeterministicRng.from_root_seed(5, &"weather") as DeterministicRng
|
||||
var right := DeterministicRng.from_root_seed(5, &"weather") as DeterministicRng
|
||||
var same := true
|
||||
for day in range(60):
|
||||
same = same and NaturalWeather.draw(left, day) == NaturalWeather.draw(right, day)
|
||||
_expect(same, "the same seed draws the same weather list")
|
||||
var timeline := WeatherTimeline.new()
|
||||
_expect(timeline.initialize(&"clear", 360) and timeline.get_last_draw_day() == -1, "a timeline opened at 06:00 still owes that day's draw")
|
||||
var rain := {&"day": 0, &"kind": &"rain", &"roll": 70, &"slot": 2, &"start": 420, &"end": 780}
|
||||
_expect(timeline.apply_draw(rain)[&"ok"] and timeline.get_last_draw_day() == 0 and timeline.next_change() == {&"kind": &"rain", &"minute": 420}, "a drawn rain is scheduled with its return to clear")
|
||||
_expect(timeline.apply_draw(rain)[&"code"] == &"DRAW_NOT_DUE", "a second draw for the same day is refused")
|
||||
var later := WeatherTimeline.new()
|
||||
later.initialize(&"clear", 361)
|
||||
_expect(later.apply_draw(rain)[&"code"] == &"DRAW_NOT_DUE", "a draw outside its 06:00 is refused")
|
||||
|
||||
|
||||
## F1: one draw at 06:00 on the saved stream, recorded as WEATHER_FORECAST_DRAWN.
|
||||
func _test_daily_natural_draw() -> void:
|
||||
var opening := _new_runtime(SEED_RAIN)
|
||||
var runtime: Variant = opening[&"runtime"]
|
||||
var today: Dictionary = runtime.weather.get_today_draw()
|
||||
_expect(String(today.get("kind", "")) == "rain" and int(today.get("start", -1)) == 360 and int(today.get("end", -1)) == 720, "seed 1106 opens day 0 with rain 06:00-12:00 (%s)" % str(today))
|
||||
_expect(runtime.weather.get_kind() == &"rain" and runtime.weather.get_last_draw_day() == 0, "a start at the draw minute switches at once")
|
||||
var events := _drawn_events(runtime)
|
||||
_expect(events.size() == 1 and int(events[0][&"day"]) == 0 and String(events[0][&"kind"]) == "rain" and bool(events[0][&"applied"]), "the opening draw is recorded once")
|
||||
runtime.call("_draw_natural_weather_if_due", 360)
|
||||
_expect(_drawn_events(runtime).size() == 1, "the same minute does not draw again")
|
||||
var twin: Variant = _new_runtime(SEED_RAIN)[&"runtime"]
|
||||
_expect(twin.weather.create_snapshot() == runtime.weather.create_snapshot()
|
||||
and twin.get_rng_stream(&"weather").snapshot() == runtime.get_rng_stream(&"weather").snapshot(), "the same seed gives the same weather and stream")
|
||||
var early := _new_runtime(SEED_RAIN, 5, 50)
|
||||
var early_runtime: Variant = early[&"runtime"]
|
||||
_expect(early_runtime.weather.get_today_draw().is_empty() and _drawn_events(early_runtime).is_empty(), "a game opened before 06:00 waits for the boundary")
|
||||
early[&"clock"].advance_minutes(10)
|
||||
_expect(early_runtime.weather.get_today_draw() == today and early_runtime.weather.get_kind() == &"rain", "the 06:00 boundary draws the same day from the same stream")
|
||||
var daily := _new_runtime(SEED_CLEAR)
|
||||
var daily_runtime: Variant = daily[&"runtime"]
|
||||
_expect(String(daily_runtime.weather.get_today_draw().get("kind", "")) == "clear" and daily_runtime.weather.get_kind() == &"clear", "seed 1000 opens day 0 clear")
|
||||
var seen := {&"count": 0}
|
||||
daily_runtime.domain_events.subscribe(&"WEATHER_FORECAST_DRAWN", &"p07_test", func(_event: SimulationDomainEvent) -> void: seen[&"count"] += 1)
|
||||
daily[&"clock"].advance_minutes(1439)
|
||||
_expect(seen[&"count"] == 0 and daily_runtime.weather.get_last_draw_day() == 0, "no draw before the next 06:00")
|
||||
daily[&"clock"].advance_minutes(1)
|
||||
_expect(seen[&"count"] == 1 and daily_runtime.weather.get_last_draw_day() == 1 and int(daily_runtime.weather.get_today_draw().get("day", -1)) == 1, "the next 06:00 draws day 1 once")
|
||||
|
||||
|
||||
## F2: rain halves the planned new arrivals of a boundary; the dropped ones are not carried.
|
||||
func _test_rain_arrivals() -> void:
|
||||
var clear_side := _new_runtime(SEED_CLEAR)
|
||||
var rain_side := _new_runtime(SEED_CLEAR)
|
||||
var clear_runtime: Variant = clear_side[&"runtime"]
|
||||
var rain_runtime: Variant = rain_side[&"runtime"]
|
||||
_expect(clear_runtime.enable_production_operations()[&"ok"] and rain_runtime.enable_production_operations()[&"ok"], "enable production arrivals on both sides")
|
||||
_expect(rain_runtime.weather.schedule(&"rain", 361)[&"ok"] and rain_runtime.weather.schedule(&"clear", 1200)[&"ok"], "rain from 06:01 on one side only")
|
||||
var compared := false
|
||||
for _minute in range(300):
|
||||
clear_side[&"clock"].advance_minutes(1)
|
||||
rain_side[&"clock"].advance_minutes(1)
|
||||
var clear_reports: Array = clear_runtime.operations_coordinator.get_arrival_reports()
|
||||
var rain_reports: Array = rain_runtime.operations_coordinator.get_arrival_reports()
|
||||
# Only boundaries inside the rain (from 06:01) are compared.
|
||||
if clear_reports.is_empty() or int(clear_reports[-1][&"minute"]) <= 360 or int(clear_reports[-1][&"total_count"]) < 2:
|
||||
continue
|
||||
var minute: int = clear_reports[-1][&"minute"]
|
||||
var clear_total: int = clear_reports[-1][&"total_count"]
|
||||
_expect(not rain_reports.is_empty() and int(rain_reports[-1][&"minute"]) == minute
|
||||
and int(rain_reports[-1][&"total_count"]) == clear_total / 2, "rain plans floor(%d / 2) arrivals at minute %d" % [clear_total, minute])
|
||||
var carry_same := true
|
||||
for profile: Dictionary in clear_runtime.operations_coordinator.get_demand_profiles():
|
||||
carry_same = carry_same and clear_runtime.operations_coordinator.get_arrival_carry(profile[&"facility_tag"], profile[&"guest_class"], profile[&"time_band"]) \
|
||||
== rain_runtime.operations_coordinator.get_arrival_carry(profile[&"facility_tag"], profile[&"guest_class"], profile[&"time_band"])
|
||||
_expect(carry_same, "arrivals lost to rain are not carried to the next boundary")
|
||||
compared = true
|
||||
break
|
||||
_expect(compared, "an arrival boundary with at least two arrivals was compared")
|
||||
var rain_capture: Dictionary = rain_runtime.call("_operations_arrival_boundary", {}, OperationsCoordinator.PHASE_CAPTURE, null)
|
||||
var clear_capture: Dictionary = clear_runtime.call("_operations_arrival_boundary", {}, OperationsCoordinator.PHASE_CAPTURE, null)
|
||||
_expect(int(rain_capture[&"arrival_percent"]) == 50 and int(clear_capture[&"arrival_percent"]) == 100, "the arrival boundary reports the rain share")
|
||||
|
||||
|
||||
## F3 and F7 (same process): a guest due to leave in rain stays once, up to the rain end or 240 minutes.
|
||||
func _test_rain_stay_and_saves() -> void:
|
||||
var rainy := _new_runtime(SEED_RAIN)
|
||||
var runtime: Variant = rainy[&"runtime"]
|
||||
var guest: Variant = _spawn(runtime, &"p07_rain_guest", &"traveler", 362)
|
||||
# Spawned 06:00, entering 06:01, deciding 06:02, first exit check at 06:03.
|
||||
rainy[&"clock"].advance_minutes(3)
|
||||
var hold: Dictionary = runtime.weather.get_rain_hold(&"p07_rain_guest")
|
||||
_expect(hold == {"original_exit": 362, "extended_to": 602} and guest.planned_exit_at == 602, "a guest due at 06:02 in rain stays to 10:02 (+240, before the rain end 12:00) (%s)" % str(hold))
|
||||
_expect(guest.exit_reason != &"PLANNED_EXIT", "the held guest did not leave at its first plan")
|
||||
_expect(not runtime.call("_hold_guest_for_rain", guest, 400), "a guest is held only once")
|
||||
var late: Variant = _spawn(runtime, &"p07_late_guest", &"traveler", 650)
|
||||
_expect(runtime.call("_hold_guest_for_rain", late, 650) and late.planned_exit_at == 720, "a guest due at 10:50 stays to the rain end 12:00")
|
||||
var saved: Dictionary = runtime.create_snapshot()
|
||||
_expect(runtime.restore_snapshot(saved) and runtime.create_snapshot() == saved, "rain, holds and the stream restore exactly in the same process")
|
||||
var path := "user://tests/p07_weather_%d" % OS.get_process_id()
|
||||
var weather_before: Dictionary = runtime.weather.create_snapshot()
|
||||
var stream_before: Dictionary = runtime.get_rng_stream(&"weather").snapshot()
|
||||
_expect(bool(runtime.save_to_path(path).get(&"ok", false)) and bool(runtime.load_from_path(path).get(&"ok", false))
|
||||
and runtime.weather.create_snapshot() == weather_before and runtime.get_rng_stream(&"weather").snapshot() == stream_before
|
||||
and runtime.guest_service.get_guest(&"p07_late_guest").planned_exit_at == 720, "an ordinary save file restores rain, holds and the weather stream")
|
||||
SaveService.delete_save(path)
|
||||
# The ordinary save operation records its own lifecycle; later checks compare with the loaded state.
|
||||
saved = runtime.create_snapshot()
|
||||
for bad_hold: Dictionary in [{"original_exit": 362, "extended_to": 603}, {"original_exit": 362, "extended_to": 362}]:
|
||||
var malformed := saved.duplicate(true)
|
||||
malformed[&"systems"][&"runtime"][&"weather"][&"rain_holds"]["p07_rain_guest"] = bad_hold
|
||||
_expect(not runtime.restore_snapshot(malformed) and runtime.create_snapshot() == saved, "a hold beyond 240 minutes or not extending is refused atomically (%s)" % str(bad_hold))
|
||||
var ghost := saved.duplicate(true)
|
||||
ghost[&"systems"][&"runtime"][&"weather"][&"rain_holds"]["nobody"] = {"original_exit": 400, "extended_to": 500}
|
||||
_expect(not runtime.restore_snapshot(ghost) and runtime.create_snapshot() == saved, "a hold of a missing guest is refused atomically")
|
||||
var unmatched := saved.duplicate(true)
|
||||
unmatched[&"systems"][&"runtime"][&"weather"][&"rain_holds"]["p07_late_guest"] = {"original_exit": 650, "extended_to": 700}
|
||||
_expect(not runtime.restore_snapshot(unmatched) and runtime.create_snapshot() == saved, "a hold that differs from the guest plan is refused atomically")
|
||||
var clear_runtime: Variant = _new_runtime(SEED_CLEAR)[&"runtime"]
|
||||
var dry: Variant = _spawn(clear_runtime, &"p07_dry_guest", &"traveler", 361)
|
||||
_expect(not clear_runtime.call("_hold_guest_for_rain", dry, 361) and dry.planned_exit_at == 361, "no hold without rain")
|
||||
_expect(clear_runtime.weather.schedule(&"storm", 361)[&"ok"], "schedule a storm")
|
||||
clear_runtime.weather.advance_to(361)
|
||||
_expect(not clear_runtime.call("_hold_guest_for_rain", dry, 361), "no hold in a storm")
|
||||
var fog_runtime: Variant = _new_runtime(SEED_FOG)[&"runtime"]
|
||||
var misty: Variant = _spawn(fog_runtime, &"p07_fog_due_guest", &"traveler", 361)
|
||||
_expect(fog_runtime.weather.get_kind() == &"mana_fog" and not fog_runtime.call("_hold_guest_for_rain", misty, 361) and misty.planned_exit_at == 361, "no hold in mana fog")
|
||||
|
||||
|
||||
## F3: after its hold the guest leaves by the current rule. A guest without a
|
||||
## suitable facility leaves at its next decision, so the ordinary decision step
|
||||
## 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)
|
||||
rainy[&"clock"].advance_minutes(2)
|
||||
_expect(guest.state == GuestRuntime.State.DECIDING and runtime.weather.get_kind() == &"rain", "the guest decides at 06:02 in rain")
|
||||
_expect(runtime.call("_hold_guest_for_rain", guest, 362) and guest.planned_exit_at == 602 and runtime.weather.get_rain_hold(&"p07_exit_guest") == {"original_exit": 362, "extended_to": 602}, "the rain holds the guest due at 06:02 to 10:02")
|
||||
runtime.call("_advance_guest_ai_decisions", 602)
|
||||
_expect(guest.exit_reason == &"PLANNED_EXIT" and guest.state == GuestRuntime.State.LEAVING and guest.planned_exit_at == 602, "at 10:02 the held guest's decision begins leaving with PLANNED_EXIT (%s)" % String(guest.exit_reason))
|
||||
|
||||
|
||||
## F4: natural storm keeps zero new arrivals.
|
||||
func _test_storm() -> void:
|
||||
var stormy := _new_runtime(SEED_STORM)
|
||||
var runtime: Variant = stormy[&"runtime"]
|
||||
_expect(runtime.weather.get_kind() == &"storm" and String(runtime.weather.get_today_draw().get("kind", "")) == "storm" and int(runtime.weather.get_today_draw().get("end", -1)) == 480, "seed 2501 opens day 0 with a natural storm 06:00-08:00")
|
||||
_expect(runtime.enable_production_operations()[&"ok"], "enable production arrivals in the storm")
|
||||
var capture: Dictionary = runtime.call("_operations_arrival_boundary", {}, OperationsCoordinator.PHASE_CAPTURE, null)
|
||||
_expect(int(capture[&"capacity_remaining"]) == 0, "a natural storm exposes zero admission capacity")
|
||||
var spawned := {&"count": 0}
|
||||
runtime.guest_service.guest_spawned.connect(func(_guest: GuestRuntime) -> void: spawned[&"count"] += 1)
|
||||
stormy[&"clock"].advance_minutes(119)
|
||||
_expect(spawned[&"count"] == 0 and runtime.weather.get_kind() == &"storm", "no new guest enters during the natural storm")
|
||||
stormy[&"clock"].advance_minutes(1)
|
||||
_expect(runtime.weather.get_kind() == &"clear", "the storm returns to clear at 08:00")
|
||||
|
||||
|
||||
## F5: mana fog x1.2 world tree and furnace yield (floor), adult intimacy increase x1.5 (floor).
|
||||
func _test_mana_fog() -> void:
|
||||
var foggy := _new_runtime(SEED_FOG)
|
||||
var fog_runtime: Variant = foggy[&"runtime"]
|
||||
var clear_side := _new_runtime(SEED_CLEAR)
|
||||
var clear_runtime: Variant = clear_side[&"runtime"]
|
||||
_expect(fog_runtime.weather.get_kind() == &"mana_fog" and int(fog_runtime.weather.get_today_draw().get("end", -1)) == 600, "seed 1592 opens day 0 with mana fog 06:00-10:00")
|
||||
_expect(int(fog_runtime.get_mana_reserve_report()[&"generation"]) == 24 and int(clear_runtime.get_mana_reserve_report()[&"generation"]) == 20, "the reserve report shows yield 24 in fog and 20 in clear")
|
||||
foggy[&"clock"].advance_minutes(10)
|
||||
clear_side[&"clock"].advance_minutes(10)
|
||||
_expect(int(fog_runtime.mana_reserve.get_last_flow().get(&"generation", -1)) == 24 and int(clear_runtime.mana_reserve.get_last_flow().get(&"generation", -1)) == 20, "the 06:10 boundary adds 24 in fog and 20 in clear")
|
||||
var fog_saved: Dictionary = fog_runtime.create_snapshot()
|
||||
_expect(fog_runtime.restore_snapshot(fog_saved) and fog_runtime.create_snapshot() == fog_saved, "a fog boundary flow restores")
|
||||
var reserve := ManaReserveCycle.new()
|
||||
var bad_reserve: Dictionary = fog_saved[&"systems"][&"runtime"][&"mana_reserve"].duplicate(true)
|
||||
bad_reserve[&"last_flow"][&"generation"] = 23
|
||||
_expect(not reserve.restore_snapshot(bad_reserve), "a yield that is neither 20 nor 24 is refused")
|
||||
_expect(ManaReserveCycle.furnace_offer_accepted(160, 0) and not ManaReserveCycle.furnace_offer_accepted(160, 0, ManaReserveCycle.FOG_WORLD_TREE_GENERATION, ManaReserveCycle.FOG_FURNACE_YIELD), "a fog boundary judges the furnace with yield 24 and 120")
|
||||
_expect(int(ManaReserveCycle.plan(100, 0, ManaReserveCycle.FOG_FURNACE_YIELD, 0, 10, ManaReserveCycle.FOG_WORLD_TREE_GENERATION)[&"after"]) == 244 and int(ManaReserveCycle.plan(200, 0, 120, 0, 10, 24)[&"after"]) == 300, "a fog furnace adds 120 and capacity 300 stays")
|
||||
foggy[&"clock"].advance_minutes(230)
|
||||
_expect(fog_runtime.weather.get_kind() == &"clear" and int(fog_runtime.mana_reserve.get_last_flow().get(&"generation", -1)) == 20, "after the fog the yield is 20 again")
|
||||
var adult: Variant = _spawn(fog_runtime, &"p07_adult", &"adventurer", 2000)
|
||||
var minor: Variant = _spawn(fog_runtime, &"p07_other", &"traveler", 2000)
|
||||
var fog_report: Dictionary = fog_runtime.guest_service.update_needs(&"p07_adult", {&"intimacy_day": 3, &"mana_fog": true})
|
||||
var plain_report: Dictionary = fog_runtime.guest_service.update_needs(&"p07_adult", {&"intimacy_day": 3, &"mana_fog": false})
|
||||
var other_report: Dictionary = fog_runtime.guest_service.update_needs(&"p07_other", {&"intimacy_day": 3, &"mana_fog": true})
|
||||
var down_report: Dictionary = fog_runtime.guest_service.update_needs(&"p07_adult", {&"need_effects": {&"intimacy": -4}, &"mana_fog": true})
|
||||
_expect(int(fog_report[&"deltas"]["intimacy"]) == 4 and int(plain_report[&"deltas"]["intimacy"]) == 3, "fog raises an adult-verified increase of 3 to floor(4.5) = 4")
|
||||
_expect(int(other_report[&"deltas"]["intimacy"]) == 0 and int(down_report[&"deltas"]["intimacy"]) == -4, "fog leaves guests without adult verification and decreases unchanged")
|
||||
_expect(not fog_runtime.guest_service.update_needs(&"p07_adult", {&"mana_fog": 1})[&"ok"], "a non-boolean fog context is refused")
|
||||
var vital_fog := _new_runtime(SEED_FOG)
|
||||
var vital_clear := _new_runtime(SEED_CLEAR)
|
||||
var fog_guest: Variant = _spawn(vital_fog[&"runtime"], &"p07_fog_adult", &"adventurer", 2000)
|
||||
var clear_guest: Variant = _spawn(vital_clear[&"runtime"], &"p07_clear_adult", &"adventurer", 2000)
|
||||
vital_fog[&"clock"].advance_minutes(1)
|
||||
vital_clear[&"clock"].advance_minutes(1)
|
||||
_expect(int(fog_guest.needs[&"intimacy"]) == 4 and int(clear_guest.needs[&"intimacy"]) == 3, "the ordinary minute raises intimacy by 4 in fog and 3 in clear (day)")
|
||||
_expect(adult != null and minor != null, "fog guests exist")
|
||||
|
||||
|
||||
## F1 and F7: a pre-natural save migrates without a past draw; malformed natural state is refused.
|
||||
func _test_migration_and_rejection() -> void:
|
||||
var opened := _new_runtime(SEED_RAIN)
|
||||
var runtime: Variant = opened[&"runtime"]
|
||||
opened[&"clock"].advance_minutes(5)
|
||||
var saved: Dictionary = runtime.create_snapshot()
|
||||
var legacy := saved.duplicate(true)
|
||||
var legacy_weather: Dictionary = legacy[&"systems"][&"runtime"][&"weather"]
|
||||
legacy_weather.erase(&"natural")
|
||||
legacy_weather.erase(&"rain_holds")
|
||||
legacy_weather[&"version"] = 1
|
||||
legacy[&"systems"][&"runtime"][&"rng_streams"].erase("weather")
|
||||
_expect(runtime.restore_snapshot(legacy), "a version 1 weather save without the weather stream loads")
|
||||
_expect(runtime.weather.get_kind() == &"rain" and runtime.weather.get_last_draw_day() == 0 and runtime.weather.get_today_draw().is_empty() and runtime.weather.get_rain_holds().is_empty(), "the migration keeps the current weather and does not draw for the past")
|
||||
_expect(runtime.get_rng_stream(&"weather").snapshot() == DeterministicRng.from_root_seed(SEED_RAIN, &"weather").snapshot(), "the migrated weather stream starts from the world seed")
|
||||
var migrated: Dictionary = runtime.create_snapshot()
|
||||
var missing_stream := saved.duplicate(true)
|
||||
missing_stream[&"systems"][&"runtime"][&"rng_streams"].erase("weather")
|
||||
_expect(not runtime.restore_snapshot(missing_stream) and runtime.create_snapshot() == migrated, "a natural-weather save without its stream is refused atomically")
|
||||
var wrong_day := saved.duplicate(true)
|
||||
wrong_day[&"systems"][&"runtime"][&"weather"][&"natural"][&"last_draw_day"] = 1
|
||||
_expect(not runtime.restore_snapshot(wrong_day) and runtime.create_snapshot() == migrated, "a draw day after the saved minute is refused atomically")
|
||||
var unknown_kind := saved.duplicate(true)
|
||||
unknown_kind[&"systems"][&"runtime"][&"weather"][&"natural"][&"today"]["kind"] = "snow"
|
||||
_expect(not runtime.restore_snapshot(unknown_kind) and runtime.create_snapshot() == migrated, "an unknown drawn kind is refused atomically")
|
||||
var overlap := saved.duplicate(true)
|
||||
overlap[&"systems"][&"runtime"][&"weather"][&"pending"] = [{"kind": "storm", "minute": 700}, {"kind": "clear", "minute": 700}]
|
||||
_expect(not runtime.restore_snapshot(overlap) and runtime.create_snapshot() == migrated, "overlapping weather changes are refused atomically")
|
||||
# The saved rain draw of seed 1106 (roll 60-84, slot 0, 06:00-12:00) changed in one field
|
||||
# each: a roll of a clear day, a slot that does not match the start, a wrong duration.
|
||||
var today: Dictionary = saved[&"systems"][&"runtime"][&"weather"][&"natural"][&"today"]
|
||||
_expect(NaturalWeather.is_consistent_draw(today) and String(today["kind"]) == "rain" and int(today["slot"]) == 0, "the saved opening draw follows the draw rule")
|
||||
for change: Dictionary in [{"roll": 10}, {"slot": 1}, {"end": 700}]:
|
||||
var inconsistent := saved.duplicate(true)
|
||||
var drawn: Dictionary = inconsistent[&"systems"][&"runtime"][&"weather"][&"natural"][&"today"]
|
||||
for key: String in change:
|
||||
drawn[key] = change[key]
|
||||
_expect(not runtime.restore_snapshot(inconsistent) and runtime.create_snapshot() == migrated, "an internally inconsistent saved draw is refused atomically (%s)" % str(change))
|
||||
|
||||
|
||||
## F6: the ordinary Main HUD shows the weather, the forecast and the effect guidance.
|
||||
func _test_hud_forecast() -> void:
|
||||
var clock: Variant = root.get_node("GameClock")
|
||||
var state: Variant = root.get_node("GameState")
|
||||
var runtime: Variant = root.get_node("SimulationRuntime")
|
||||
clock.reset(1, 6, 0)
|
||||
state.reset()
|
||||
_expect(runtime.initialize(clock, state, SEED_RAIN)[&"valid"], "initialize the ordinary Runtime with seed 1106")
|
||||
clock.set_paused(true)
|
||||
var main := load("res://scenes/main/main.tscn").instantiate() as Control
|
||||
main.set("manual_save_path", "user://tests/p07_weather_main_%d/manual_ui" % OS.get_process_id())
|
||||
root.add_child(main)
|
||||
await process_frame
|
||||
await process_frame
|
||||
main.call("_refresh_dashboard")
|
||||
var weather_label := main.get("weather_label") as Label
|
||||
var forecast_label := main.get("weather_forecast_label") as Label
|
||||
var report: Dictionary = runtime.get_weather_report()
|
||||
_expect(weather_label.text == "雨" and report[&"kind"] == &"rain", "HUD shows the Runtime rain")
|
||||
_expect(forecast_label != null and forecast_label.text == "次: 晴れ 12:00" and report[&"next_change"] == {&"kind": &"clear", &"minute": 720}, "HUD forecast shows the next change from the Runtime (%s)" % (forecast_label.text if forecast_label != null else "none"))
|
||||
_expect(String(report[&"effect_text"]) in weather_label.tooltip_text and "新規来客が半分" in weather_label.tooltip_text, "HUD guidance explains the rain effect")
|
||||
clock.advance_minutes(360)
|
||||
main.call("_refresh_dashboard")
|
||||
_expect(weather_label.text == "晴れ" and forecast_label.text == "次: 予定なし" and "影響はありません" in weather_label.tooltip_text, "HUD follows the end of the rain")
|
||||
var now: int = clock.absolute_minute
|
||||
_expect(runtime.weather.schedule(&"storm", now + 1)[&"ok"] and runtime.weather.schedule(&"mana_fog", now + 2)[&"ok"], "schedule a storm and then mana fog on the ordinary Runtime")
|
||||
clock.advance_minutes(1)
|
||||
main.call("_refresh_dashboard")
|
||||
report = runtime.get_weather_report()
|
||||
var fog_minute := posmod(now + 2, 1440)
|
||||
_expect(weather_label.text == "嵐" and report[&"kind"] == &"storm" and String(report[&"effect_text"]) in weather_label.tooltip_text and "新規来客はありません" in weather_label.tooltip_text, "HUD shows the storm and its guidance")
|
||||
_expect(forecast_label.text == "次: マナ霧 %02d:%02d" % [fog_minute / 60, fog_minute % 60], "HUD forecast shows the next change to mana fog (%s)" % forecast_label.text)
|
||||
clock.advance_minutes(1)
|
||||
main.call("_refresh_dashboard")
|
||||
report = runtime.get_weather_report()
|
||||
_expect(weather_label.text == "マナ霧" and report[&"kind"] == &"mana_fog" and String(report[&"effect_text"]) in weather_label.tooltip_text
|
||||
and "1.2倍" in weather_label.tooltip_text and "1.5倍" in weather_label.tooltip_text and not "炉" in weather_label.tooltip_text, "HUD shows the mana fog and its guidance without a furnace claim")
|
||||
var teardown: Dictionary = await AudioTeardown.free_owner_and_wait(self, main)
|
||||
_expect(bool(teardown.get(&"passed", false)) and not bool(teardown.get(&"deadline_reached", true)), "owned audio references release before bounded teardown deadline with whole Runtime unchanged")
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
return
|
||||
_failures += 1
|
||||
push_error(message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bsaoksmz5s26w
|
||||
@@ -20,10 +20,15 @@ func run() -> void:
|
||||
legacy[&"systems"][&"runtime"].erase(&"weather")
|
||||
legacy[&"systems"][&"runtime"].erase(&"facility_cleanliness")
|
||||
expect(runtime.restore_snapshot(legacy) and runtime.weather.get_kind() == &"clear" and runtime.weather.get_minute() == 360, "v10 migrates to neutral weather at saved minute")
|
||||
expect(runtime.create_snapshot() == original, "old neutral save migration preserves other state")
|
||||
# P07-01 (A13): the original carries the opening day's natural draw, which a
|
||||
# pre-weather save never had; every other system must stay byte-equal.
|
||||
var migrated: Dictionary = runtime.create_snapshot()
|
||||
var migrated_rest: Dictionary = migrated.duplicate(true); migrated_rest[&"systems"][&"runtime"].erase(&"weather")
|
||||
var original_rest: Dictionary = original.duplicate(true); original_rest[&"systems"][&"runtime"].erase(&"weather")
|
||||
expect(migrated_rest == original_rest and runtime.weather.get_last_draw_day() == -1 and runtime.weather.get_today_draw().is_empty(), "old neutral save migration preserves other state")
|
||||
var malformed := original.duplicate(true)
|
||||
malformed[&"systems"][&"runtime"][&"weather"][&"minute"] = 361
|
||||
expect(not runtime.restore_snapshot(malformed) and runtime.create_snapshot() == original, "future weather clock rejects atomically")
|
||||
expect(not runtime.restore_snapshot(malformed) and runtime.create_snapshot() == migrated, "future weather clock rejects atomically")
|
||||
malformed = original.duplicate(true)
|
||||
malformed[&"systems"][&"runtime"].erase(&"weather")
|
||||
expect(not runtime.restore_snapshot(malformed), "new schema cannot silently lose weather")
|
||||
|
||||
@@ -32,7 +32,11 @@ func run() -> void:
|
||||
var completed: Dictionary = runtime.create_snapshot()
|
||||
expect(runtime.restore_snapshot(completed) and runtime.create_snapshot() == completed, "completed retry restores exactly")
|
||||
clock.advance_minutes(30)
|
||||
expect(runtime.weather.get_kind() == &"clear" and runtime.weather.create_snapshot()[&"pending"].is_empty(), "disk-restored recovery occurs once at scheduled minute")
|
||||
# P07-01 (A13): only the opening day's natural draw may remain scheduled.
|
||||
var today: Dictionary = runtime.weather.get_today_draw()
|
||||
var natural_pending: Array = [] if String(today.get("kind", "clear")) == "clear" else [
|
||||
{&"kind": StringName(today["kind"]), &"minute": int(today["start"])}, {&"kind": &"clear", &"minute": int(today["end"])}]
|
||||
expect(runtime.weather.get_kind() == &"clear" and runtime.weather.create_snapshot()[&"pending"] == natural_pending, "disk-restored recovery occurs once at scheduled minute")
|
||||
expect(not runtime.get_operations_report()[&"runtime_failure_latched"], "recovery resumes ordinary operations")
|
||||
expect(SaveService.delete_save(path), "delete isolated test save")
|
||||
runtime.free(); clock.free(); state.free()
|
||||
|
||||
@@ -32,7 +32,7 @@ func _initialize() -> void:
|
||||
for field: String in ["version", "minute", "kind", "since", "pending"]:
|
||||
var bad := baseline.duplicate(true); bad.erase(field)
|
||||
expect(not weather.restore_snapshot(bad) and weather.create_snapshot() == baseline, "missing field rejects atomically: " + field)
|
||||
for change: Dictionary in [{"version": 2}, {"minute": 380.0}, {"kind": "snow"}, {"since": 381}, {"pending": [{"kind":"clear","minute":380}]}, {"pending": [{"kind":"clear","minute":400},{"kind":"rain","minute":390}]}, {"extra":true}]:
|
||||
for change: Dictionary in [{"version": 3}, {"minute": 380.0}, {"kind": "snow"}, {"since": 381}, {"pending": [{"kind":"clear","minute":380}]}, {"pending": [{"kind":"clear","minute":400},{"kind":"rain","minute":390}]}, {"extra":true}]:
|
||||
var bad := baseline.duplicate(true)
|
||||
bad.merge(change, true)
|
||||
expect(not weather.restore_snapshot(bad) and weather.create_snapshot() == baseline, "malformed or contradictory state rejects atomically")
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
extends RefCounted
|
||||
|
||||
## P07-01 F7: the fixture and minute script shared by the restart writer
|
||||
## (tests/environment/test_p07_weather_restart_1_write.gd) and reader
|
||||
## (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
|
||||
## saves), and the saves bracket the 07:00 mana boundary and the fog end.
|
||||
const HANDOFF_PATH := "res://artifacts/p07-01/restart-handoff.json"
|
||||
const HANDOFF_SCHEMA := "p07-01-restart-handoff-v2"
|
||||
const WRITER_SOURCE := "res://tests/environment/test_p07_weather_restart_1_write.gd"
|
||||
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
|
||||
# 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"
|
||||
const FOG_SEED := 1592
|
||||
const FOG_WRITER_SAVE := "user://p07_restart_write/fog_minute_%d"
|
||||
const FOG_READER_SAVE := "user://p07_restart_read/fog_minute_%d"
|
||||
# Just before, at and after the 07:00 mana boundary in fog; just before, at and after the fog end.
|
||||
const FOG_SAVE_MINUTES: Array[int] = [419, 420, 421, 599, 600, 601]
|
||||
const FOG_LAST_MINUTE := 630
|
||||
const FOG_ADULTS := {418: &"p07_restart_fog_adult_a", 598: &"p07_restart_fog_adult_b"}
|
||||
|
||||
var _expect: Callable
|
||||
|
||||
|
||||
func _init(expect: Callable) -> void:
|
||||
_expect = expect
|
||||
|
||||
|
||||
func new_fixture(with_guest := true, seed := SEED) -> Dictionary:
|
||||
var clock: Variant = load("res://autoload/game_clock.gd").new()
|
||||
var state: Variant = load("res://autoload/game_state.gd").new()
|
||||
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
|
||||
clock.reset(1, 6, 0)
|
||||
state.reset()
|
||||
_expect.call(bool(runtime.initialize(clock, state, seed).get(&"valid", false)), "fixture Runtime initializes")
|
||||
clock.set_paused(true)
|
||||
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
||||
_expect.call(bool(runtime.enable_production_operations().get(&"ok", false)), "fixture enables production Operations")
|
||||
var fixture := {&"clock": clock, &"state": state, &"runtime": runtime, &"draws": {&"count": 0}, &"seed": seed}
|
||||
if with_guest and seed == SEED:
|
||||
var result: CommandResult = runtime.submit_command(&"spawn_guest", {
|
||||
&"guest_id": HELD_GUEST,
|
||||
&"guest_type_id": &"traveler",
|
||||
&"gold": 100,
|
||||
&"options": {
|
||||
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
|
||||
&"needs": {&"hunger": 10, &"sleep": 10, &"hygiene": 10, &"medical": 0, &"leisure": 10, &"intimacy": 0},
|
||||
&"planned_exit_at": 362,
|
||||
},
|
||||
}, &"world", HELD_GUEST, 10, -1, &"p07.restart.guest")
|
||||
_expect.call(result.is_success(), "fixture spawns the guest due at 06:02")
|
||||
watch_draws(fixture)
|
||||
return fixture
|
||||
|
||||
|
||||
func watch_draws(fixture: Dictionary) -> void:
|
||||
var draws: Dictionary = fixture[&"draws"]
|
||||
fixture[&"runtime"].domain_events.subscribe(&"WEATHER_FORECAST_DRAWN", &"p07_restart",
|
||||
func(_event: SimulationDomainEvent) -> void: draws[&"count"] += 1)
|
||||
|
||||
|
||||
func free_fixture(fixture: Dictionary) -> void:
|
||||
for key: StringName in [&"runtime", &"clock", &"state"]:
|
||||
if fixture.get(key) != null and is_instance_valid(fixture[key]):
|
||||
fixture[key].free()
|
||||
|
||||
|
||||
## Advances one minute at a time after `after_minute` through `through_minute`
|
||||
## and calls `on_minute` after each minute (the writer saves there). In the fog
|
||||
## scene the adult-verified guests arrive at their minutes in every run.
|
||||
func play(fixture: Dictionary, after_minute: int, through_minute: int, on_minute: Callable) -> void:
|
||||
var clock: Variant = fixture[&"clock"]
|
||||
_expect.call(clock.absolute_minute == after_minute, "play starts at minute %d" % after_minute)
|
||||
for minute: int in range(after_minute + 1, through_minute + 1):
|
||||
clock.advance_minutes(1)
|
||||
if fixture[&"seed"] == FOG_SEED and FOG_ADULTS.has(minute):
|
||||
_spawn_adult(fixture[&"runtime"], FOG_ADULTS[minute])
|
||||
if on_minute.is_valid():
|
||||
on_minute.call(minute)
|
||||
|
||||
|
||||
func _spawn_adult(runtime: Variant, guest_id: StringName) -> void:
|
||||
var result: CommandResult = runtime.submit_command(&"spawn_guest", {
|
||||
&"guest_id": guest_id,
|
||||
&"guest_type_id": &"adventurer",
|
||||
&"gold": 100,
|
||||
&"options": {
|
||||
&"current_cell": Vector3i(0, 0, WorldService.LAYER_FRONT),
|
||||
&"needs": {&"hunger": 10, &"sleep": 10, &"hygiene": 10, &"medical": 0, &"leisure": 10, &"intimacy": 0},
|
||||
&"planned_exit_at": 2000,
|
||||
},
|
||||
}, &"world", guest_id, 10, -1, StringName("p07.restart." + String(guest_id)))
|
||||
_expect.call(result.is_success(), "the fog scene spawns the adult-verified guest %s" % guest_id)
|
||||
|
||||
|
||||
## The weather-facing state compared across processes: weather and its stream,
|
||||
## guest plans and needs (the fog intimacy), the whole reserve with its last
|
||||
## flow (the fog yield) and the arrival totals. Needs are listed in text order of
|
||||
## their names, because a loaded guest may keep its needs in another key order
|
||||
## (StringName ordering is not text ordering and differs between processes).
|
||||
func state_value(runtime: Variant) -> Dictionary:
|
||||
var plans := {}
|
||||
var needs := {}
|
||||
for guest: GuestRuntime in runtime.guest_service.get_guests():
|
||||
plans[String(guest.guest_id)] = guest.planned_exit_at
|
||||
var names: Array[String] = []
|
||||
for name: Variant in guest.needs:
|
||||
names.append(String(name))
|
||||
names.sort()
|
||||
var row := {}
|
||||
for name: String in names:
|
||||
row[name] = guest.needs[name]
|
||||
needs[String(guest.guest_id)] = row
|
||||
var arrivals := 0
|
||||
for report: Dictionary in runtime.operations_coordinator.get_arrival_reports():
|
||||
arrivals += int(report[&"total_count"])
|
||||
return {
|
||||
"minute": runtime.weather.get_minute(),
|
||||
"weather": runtime.weather.create_snapshot(),
|
||||
"stream": runtime.get_rng_stream(&"weather").snapshot(),
|
||||
"plans": plans,
|
||||
"needs": needs,
|
||||
"reserve": runtime.mana_reserve.create_snapshot(),
|
||||
"reserve_flow": runtime.mana_reserve.get_last_flow(),
|
||||
"arrivals": arrivals,
|
||||
}
|
||||
|
||||
|
||||
func state_bytes(runtime: Variant) -> PackedByteArray:
|
||||
return var_to_bytes(state_value(runtime))
|
||||
|
||||
|
||||
static func sha256(bytes: PackedByteArray) -> String:
|
||||
var context := HashingContext.new()
|
||||
context.start(HashingContext.HASH_SHA256)
|
||||
context.update(bytes)
|
||||
return context.finish().hex_encode()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bczrqi1qiv5zk
|
||||
@@ -438,6 +438,28 @@
|
||||
"old_bounded_teardown": false,
|
||||
"new_audio_QA": false
|
||||
}
|
||||
,
|
||||
"tests/environment/test_p07_weather_runtime.gd": {
|
||||
"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": "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": "e5c2376e9cb3197d6a0a8d99c9dea020cad2648328178949cd188d999e72071a",
|
||||
"ordinary_cleanup_prepared": false,
|
||||
"ordinary_control_labels": [],
|
||||
"old_bounded_teardown": false,
|
||||
"new_audio_QA": false
|
||||
}
|
||||
},
|
||||
"outside_routes": {
|
||||
"tests/supply/test_normal_auto_purchase_chain.gd": {
|
||||
@@ -474,6 +496,9 @@
|
||||
"tests/economy/test_p05_funds_restart_1_write.gd": [
|
||||
"artifacts/p05-02/restart-handoff.json"
|
||||
],
|
||||
"tests/environment/test_p07_weather_restart_1_write.gd": [
|
||||
"artifacts/p07-01/restart-handoff.json"
|
||||
],
|
||||
"tests/mvp/test_runtime_upkeep.gd": [
|
||||
"artifacts/upkeep-2026-09-29/preclose-snapshot.txt",
|
||||
"artifacts/upkeep-2026-09-29/closed-snapshot.txt"
|
||||
|
||||
@@ -0,0 +1,632 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Root-owned narrow P07-01 (natural weather, rain, mana fog, storm, HUD forecast; A13) collection using the
|
||||
unchanged guarded phase protocol of tools/run_p05_funds_tests.py (itself the protocol of
|
||||
tools/run_p18_relationship_tests.py). After the roster, the F7 restart writer and reader suites run as two more
|
||||
phases (separate engine processes); the writer's only declared generated report is the save handoff the reader loads."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import types
|
||||
import uuid
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
import run_all_tests as shared
|
||||
|
||||
ROSTER = [
|
||||
'tests/environment/test_p07_weather_runtime.gd',
|
||||
'tests/environment/test_runtime_weather.gd',
|
||||
'tests/environment/test_weather_timeline.gd',
|
||||
'tests/environment/test_weather_recovery.gd',
|
||||
'tests/mvp/test_runtime_operations_integration.gd',
|
||||
'tests/s2/test_runtime_operations_cycle.gd',
|
||||
'tests/s2/test_runtime_mana_reserve.gd',
|
||||
'tests/s2/test_mana_reserve_cycle.gd',
|
||||
'tests/s3/test_guest_service.gd',
|
||||
'tests/s6/test_runtime_save_lifecycle.gd',
|
||||
'tests/s6/test_runtime_snapshot_serialization.gd',
|
||||
'tests/ui/test_main_dashboard.gd',
|
||||
]
|
||||
# P07-01 F7 (SPEC section 8): the writer saves, the reader in the next phase's engine resumes.
|
||||
RESTART = [
|
||||
'tests/environment/test_p07_weather_restart_1_write.gd',
|
||||
'tests/environment/test_p07_weather_restart_2_read.gd',
|
||||
]
|
||||
HANDOFF = 'artifacts/p07-01/restart-handoff.json'
|
||||
SUITES = ROSTER + RESTART
|
||||
WRITER_LINE = r'(?m)^P07_RESTART_WRITER pid=([0-9]+) handoff_sha256=([0-9a-f]{64})$'
|
||||
READER_LINE = r'(?m)^P07_RESTART_READER pid=([0-9]+) writer_pid=([0-9]+) handoff_sha256=([0-9a-f]{64})$'
|
||||
|
||||
|
||||
def collect(args, root, out, refs, markers, contracts, policy, contract_path, contract_sha, parse_paths):
|
||||
source_bytes = sum(pin['bytes'] for pin in refs.values())
|
||||
free = shutil.disk_usage(out.parent).free
|
||||
needed = source_bytes + shared.MIN_SPACE
|
||||
shared.require(free >= needed, 'fresh source and phase disk admission')
|
||||
outer = shared.module('p07_owned_outer', shared.SUPPORT / 'owned_outer.py')
|
||||
shared.require(not outer.native.helper['live_engines'](), 'another Godot owns CPU')
|
||||
engine = shutil.which('godot')
|
||||
shared.require(engine and Path(engine).is_file(), 'native launcher exists')
|
||||
engine = str(Path(engine).resolve())
|
||||
out.mkdir(parents=True, exist_ok=False)
|
||||
stage = out / 'stage'
|
||||
stage.mkdir()
|
||||
(out / 'starters').mkdir()
|
||||
(out / 'bootstrap').mkdir()
|
||||
for name, pin in shared.read(shared.SUPPORT / 'bootstrap-scripts.json').items():
|
||||
data = pin['text'].encode('utf-8')
|
||||
shared.require(hashlib.sha256(data).hexdigest() == pin['sha256'], 'unchanged external bootstrap bytes')
|
||||
(out / 'bootstrap' / name).write_bytes(data)
|
||||
protected = shared.default_player(root)
|
||||
expected = {name: {'bytes': pin['bytes'], 'sha256': pin['sha256']} for name, pin in refs.items()}
|
||||
selected = {'root_before': refs, 'map': {name: pin['sha256'] for name, pin in refs.items()}, 'timeout': args.timeout}
|
||||
for name, pin in refs.items():
|
||||
target = stage / name
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shared.copy_source(Path(pin['path']), target)
|
||||
shared.require(shared.inventory(stage, stage=True) == expected, 'complete private source copy')
|
||||
public = (stage / 'project.godot').read_bytes()
|
||||
shared.write(out / 'parse-paths.json', parse_paths)
|
||||
run_id = uuid.uuid4().hex
|
||||
shared.write(out / 'runner-input.json', dict(protocol='guarded-p07-01-v1', run_id=run_id,
|
||||
owner_runner_pid=os.getpid(), project_root=str(root), stage=str(stage), roster=ROSTER,
|
||||
restart_phases=RESTART, restart_handoff=HANDOFF,
|
||||
selected_source_map=selected['map'], marker_contract_sha256=contract_sha,
|
||||
runner_sha256=shared.sha(Path(__file__)), shared_runner_sha256=shared.sha(shared.HERE / 'run_all_tests.py'),
|
||||
marker_module_sha256=shared.sha(shared.HERE / 'strict_test_markers.py'),
|
||||
engine=engine, engine_sha256=shared.sha(engine), default_player_before=protected,
|
||||
formal_acceptance=False, original_unit_complete=False))
|
||||
shared.write(out / 'source-start.json', refs)
|
||||
diagnostics = shared.module('p07_diagnostics', shared.SUPPORT / 'diagnostic_gate.py')
|
||||
owners = shared.module('p07_owners', shared.SUPPORT / 'ordinary_proof_gate.py')
|
||||
audio = shared.module('p07_audio', shared.SUPPORT / 'audio_evidence_gate.py')
|
||||
phases, rows, previous = [], [], {}
|
||||
error = None
|
||||
try:
|
||||
for phase, test in [('import', None), ('parse', None)] + [(str(i).zfill(3), name) for i, name in enumerate(SUITES)]:
|
||||
shared.require(shared.sha(contract_path) == contract_sha, 'marker contract unchanged')
|
||||
row = shared.execute_phase(engine, stage, out, phase, test, selected, expected, previous,
|
||||
policy, contracts, parse_paths, public, protected, root, outer, markers, diagnostics, owners, audio)
|
||||
archive_profiles(out, row)
|
||||
phases.append(row)
|
||||
if test:
|
||||
row.update(sequence=len(rows), godot=engine)
|
||||
rows.append(row)
|
||||
(out / 'all-tests.json').write_text(json.dumps(rows, ensure_ascii=False, indent=2) + '\n')
|
||||
print(('PASS ' if row['passed'] else 'FAIL ') + test, flush=True)
|
||||
shared.require(row['passed'], 'first failed phase stops without retry: ' + phase)
|
||||
except BaseException as caught:
|
||||
error = type(caught).__name__ + ': ' + str(caught)
|
||||
finally:
|
||||
cleanup, cleanup_errors = outer.finish_captured(outer.OWNED)
|
||||
guard_errors = []
|
||||
try:
|
||||
shared.require(shared.source_refs(root) == refs, 'whole source unchanged')
|
||||
shared.require(shared.sha(contract_path) == contract_sha, 'marker contract unchanged at end')
|
||||
shared.require(shared.default_player(root) == protected, 'default profile untouched')
|
||||
shared.require(shared.inventory(stage, stage=True) == dict(expected, **previous), 'whole stage exact')
|
||||
except BaseException as caught:
|
||||
guard_errors.append(type(caught).__name__ + ': ' + str(caught))
|
||||
ended = bool(outer.OWNED) and all(row['engine_ended'] is True for row in cleanup)
|
||||
passed = error is None and not cleanup_errors and not guard_errors and ended and len(phases) == len(SUITES) + 2 and all(row['passed'] for row in phases)
|
||||
context = shared.totals(rows, SUITES)
|
||||
shared.write(out / 'runner-ended.json', dict(context, protocol='guarded-p07-01-v1', run_id=run_id,
|
||||
owner_runner_pid=os.getpid(), passed=passed, engine_ended=ended,
|
||||
actual_captured_PIDs=[process.pid for process in outer.OWNED], final_owned_cleanup=cleanup,
|
||||
cleanup_errors=cleanup_errors, guard_errors=guard_errors, error=error, phases=phases,
|
||||
all_tests_sha256=shared.sha(out / 'all-tests.json') if (out / 'all-tests.json').exists() else None,
|
||||
default_player_after=shared.default_player(root), formal_acceptance=False,
|
||||
original_unit_complete=False, independent_review='pending', native_Windows='pending'))
|
||||
if passed:
|
||||
seal(out, refs, engine, contract_sha)
|
||||
# Root also exercises precisely the verifier that ordinary Runner uses.
|
||||
verify(out, refs, markers, contracts, policy, contract_sha, parse_paths)
|
||||
print(json.dumps(dict(context, passed=passed, error=error, cleanup_errors=cleanup_errors, guard_errors=guard_errors)), flush=True)
|
||||
return 0 if passed else 1
|
||||
|
||||
|
||||
|
||||
# Receipt file names are always relative; absolute paths in native receipts are
|
||||
# historical identity strings only and are never opened by the verifier.
|
||||
def require(ok, reason):
|
||||
shared.require(ok, reason)
|
||||
|
||||
|
||||
def digest(value):
|
||||
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'),
|
||||
ensure_ascii=False).encode()).hexdigest()
|
||||
|
||||
|
||||
def content_map(refs):
|
||||
return {name: {'bytes': pin['bytes'], 'sha256': pin['sha256']}
|
||||
for name, pin in refs.items()}
|
||||
|
||||
|
||||
def plain_path(path):
|
||||
path = Path(path)
|
||||
require('..' not in path.parts, 'parent traversal refused')
|
||||
absolute = Path(os.path.abspath(path))
|
||||
for part in [absolute, *absolute.parents]:
|
||||
require(not part.is_symlink(), 'symlink path refused: ' + str(part))
|
||||
return absolute
|
||||
|
||||
|
||||
def confined(out, name):
|
||||
require(type(name) is str and name and '\\' not in name, 'ordinary relative evidence name')
|
||||
path = Path(name)
|
||||
require(not path.is_absolute() and '..' not in path.parts
|
||||
and path.as_posix() == name and name != '.', 'evidence path escape or alias')
|
||||
result = plain_path(out / path)
|
||||
require(result.is_relative_to(out), 'evidence confined to output')
|
||||
return result
|
||||
|
||||
|
||||
def inventory(out):
|
||||
result = {}
|
||||
require(out.is_dir(), 'output directory required')
|
||||
for directory, dirs, files in os.walk(out, followlinks=False):
|
||||
for name in dirs + files:
|
||||
p = Path(directory) / name
|
||||
mode = p.lstat().st_mode
|
||||
require(stat.S_ISREG(mode) or stat.S_ISDIR(mode), 'nonordinary evidence refused')
|
||||
if stat.S_ISREG(mode):
|
||||
require(p.stat().st_nlink == 1, 'hardlinked evidence refused')
|
||||
result[p.relative_to(out).as_posix()] = {
|
||||
'bytes': p.stat().st_size, 'sha256': shared.sha(p)}
|
||||
return result
|
||||
|
||||
|
||||
def unique_object(pairs):
|
||||
result = {}
|
||||
for key, value in pairs:
|
||||
require(key not in result, 'duplicate JSON key')
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def read_json(path):
|
||||
return json.loads(path.read_text(encoding='utf-8'), object_pairs_hook=unique_object,
|
||||
parse_constant=lambda token: require(False, 'nonfinite JSON ' + token))
|
||||
|
||||
|
||||
def archive_profiles(out, row):
|
||||
for label in ('startup', 'runtime'):
|
||||
value = row.get('whole_' + label + '_profile_after')
|
||||
if value is None:
|
||||
continue
|
||||
dest = out / 'profiles' / row['phase'] / label
|
||||
dest.mkdir(parents=True, exist_ok=False)
|
||||
source = plain_path(value['path'])
|
||||
require(shared.inventory(source) == value['files'], 'whole owned profile before archive')
|
||||
for name in value['files']:
|
||||
target = confined(out, str((dest / name).relative_to(out)))
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(source / name, target)
|
||||
require(shared.inventory(dest) == value['files'] == shared.inventory(source),
|
||||
'whole owned profile archive exact')
|
||||
|
||||
|
||||
def seal(out, refs, engine, contract_sha):
|
||||
# Last file written: an interrupted collection has no complete receipt.
|
||||
shutil.copyfile(engine, out / 'engine.bin')
|
||||
require(shared.sha(out / 'engine.bin') == shared.sha(engine), 'engine archive exact')
|
||||
source = content_map(refs)
|
||||
shared.write(out / 'receipt.json', {
|
||||
'schema': 'guarded-p07-01-v1', 'source_map': source,
|
||||
'parse_checks': len([name for name in refs if name.endswith('.gd')]),
|
||||
'source_map_sha256': digest(source),
|
||||
'project_settings_sha256': source['project.godot']['sha256'],
|
||||
'marker_contract_sha256': contract_sha,
|
||||
'engine_sha256': shared.sha(engine),
|
||||
'files': inventory(out),
|
||||
})
|
||||
|
||||
|
||||
def cleanup_gate(value):
|
||||
require(value['owned_child_created'] is True and value['engine_ended'] is True
|
||||
and value['actual_process_exit_code'] == 0 and value['errors'] == [],
|
||||
'successful actual owned cleanup')
|
||||
attempts = value['attempts']
|
||||
require(1 <= len(attempts) <= 3, 'bounded cleanup attempts')
|
||||
for index, row in enumerate(attempts):
|
||||
require(row['attempt'] == index + 1 and row['errors'] == []
|
||||
and row['wait_timeout_seconds'] == 5 and row['wait_completed'] is True
|
||||
and row['wait_returned_code'] == 0 and row['poll_after'] == 0,
|
||||
'complete bounded owned wait')
|
||||
|
||||
|
||||
def outer_cleanup(rows, pids):
|
||||
require([r['pid'] for r in rows] == pids, 'exact captured cleanup PID order')
|
||||
for row in rows:
|
||||
require(row['engine_ended'] is True and row['outer_cleanup_errors'] == []
|
||||
and row['foreign_process_or_group_signals'] == 0
|
||||
and row['outer_maximum_helper_invocations'] == 3
|
||||
and row['helper_maximum_waits'] == 3 and row['per_wait_timeout_seconds'] == 5,
|
||||
'unchanged outer cleanup contract')
|
||||
cleanup_gate(row['cleanup'])
|
||||
|
||||
|
||||
def verify(out, refs, markers, contracts, policy, contract_sha, parse_paths):
|
||||
receipt_path = confined(out, 'receipt.json')
|
||||
require(receipt_path.is_file(), 'regular final receipt required')
|
||||
manifest = read_json(receipt_path)
|
||||
require(manifest['schema'] == 'guarded-p07-01-v1', 'current receipt schema required')
|
||||
files = manifest['files']
|
||||
require(type(files) is dict and files, 'complete raw manifest')
|
||||
for name, pin in files.items():
|
||||
p = confined(out, name)
|
||||
require(p.is_file() and set(pin) == {'bytes', 'sha256'}
|
||||
and type(pin['bytes']) is int and pin['bytes'] >= 0
|
||||
and re.fullmatch('[0-9a-f]{64}', pin['sha256']) is not None,
|
||||
'ordinary artifact pin')
|
||||
actual = inventory(out)
|
||||
receipt_pin = actual.pop('receipt.json')
|
||||
require(actual == files, 'complete raw file set and SHA-256 equality')
|
||||
source = content_map(refs)
|
||||
require(manifest['source_map'] == source and manifest['source_map_sha256'] == digest(source),
|
||||
'current whole source content map independent of checkout path')
|
||||
require(manifest['project_settings_sha256'] == source['project.godot']['sha256']
|
||||
and manifest['marker_contract_sha256'] == contract_sha, 'current project/marker pins')
|
||||
|
||||
def read(name):
|
||||
require(name in files, 'required raw file missing: ' + name)
|
||||
return read_json(confined(out, name))
|
||||
|
||||
def text(name):
|
||||
require(name in files, 'required raw transport missing: ' + name)
|
||||
return confined(out, name).read_text(encoding='utf-8')
|
||||
|
||||
start, end = read('runner-input.json'), read('runner-ended.json')
|
||||
require(start['protocol'] == end['protocol'] == 'guarded-p07-01-v1'
|
||||
and start['run_id'] == end['run_id']
|
||||
and re.fullmatch('[0-9a-f]{32}', start['run_id']) is not None,
|
||||
'same current run')
|
||||
require(type(start['owner_runner_pid']) is int and start['owner_runner_pid'] > 0
|
||||
and start['owner_runner_pid'] == end['owner_runner_pid'], 'same collector owner')
|
||||
require(start['roster'] == ROSTER and start['restart_phases'] == RESTART
|
||||
and start['restart_handoff'] == HANDOFF and len(set(SUITES)) == len(SUITES),
|
||||
'exact unique roster and restart tests')
|
||||
require(start['selected_source_map'] == {n: p['sha256'] for n, p in source.items()}
|
||||
and content_map(read('source-start.json')) == source, 'whole selected source pins')
|
||||
for field, name in [('runner_sha256', 'tools/run_p07_weather_tests.py'),
|
||||
('shared_runner_sha256', 'tools/run_all_tests.py'),
|
||||
('marker_module_sha256', 'tools/strict_test_markers.py')]:
|
||||
require(start[field] == source[name]['sha256'], 'current collector/shared/marker pin')
|
||||
require(start['marker_contract_sha256'] == contract_sha, 'marker contract pin')
|
||||
# Resolve only the locally configured launcher, never the receipt's engine path.
|
||||
engine = shutil.which('godot')
|
||||
require(engine and shared.sha(engine) == start['engine_sha256']
|
||||
== manifest['engine_sha256'] == files['engine.bin']['sha256'],
|
||||
'current native engine and retained bytes pinned')
|
||||
derived_paths = ['res://' + name for name in sorted(refs) if name.endswith('.gd')]
|
||||
require(read('parse-paths.json') == parse_paths == derived_paths
|
||||
and manifest['parse_checks'] == len(derived_paths),
|
||||
'all exact source parse paths and receipt count')
|
||||
require(end['passed'] is True and end['engine_ended'] is True and end['error'] is None
|
||||
and end['cleanup_errors'] == [] and end['guard_errors'] == [],
|
||||
'finished successful collection without guard/cleanup error')
|
||||
for receipt in (start, end):
|
||||
require(receipt['formal_acceptance'] is False and receipt['original_unit_complete'] is False,
|
||||
'backend evidence never completes original unit')
|
||||
before = start['default_player_before']
|
||||
require(before == end['default_player_after'] and before['logs_and_locks_included'] is True
|
||||
and type(before['existed']) is bool and type(before['files']) is dict,
|
||||
'complete default player before/after equality')
|
||||
for name, pin in before['files'].items():
|
||||
confined(out, name) # Validate the inventory name; never open the player.
|
||||
require(set(pin) == {'bytes', 'sha256'} and type(pin['bytes']) is int
|
||||
and pin['bytes'] >= 0 and re.fullmatch('[0-9a-f]{64}', pin['sha256']),
|
||||
'whole default player file pin')
|
||||
require(before['existed'] or before['files'] == {}, 'absent player has no files')
|
||||
|
||||
native = shared.module('p07_verify_native', shared.SUPPORT / 'owned_diagnostic_process.py')
|
||||
diagnostics = shared.module('p07_verify_diagnostics', shared.SUPPORT / 'diagnostic_gate.py')
|
||||
owners = shared.module('p07_verify_owners', shared.SUPPORT / 'ordinary_proof_gate.py')
|
||||
audio = shared.module('p07_verify_audio', shared.SUPPORT / 'audio_evidence_gate.py')
|
||||
# Helpers are loaded for their pure gates only. No run/probe/cleanup is called.
|
||||
origin = Path(start['stage']).parent
|
||||
require(Path(start['stage']) == origin / 'stage' and origin.is_absolute()
|
||||
and '..' not in origin.parts, 'original output identity')
|
||||
|
||||
def relocated(value):
|
||||
if isinstance(value, dict):
|
||||
return {relocated(k): relocated(v) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [relocated(v) for v in value]
|
||||
if isinstance(value, str) and value.startswith(str(origin) + '/'):
|
||||
return str(confined(out, value[len(str(origin)) + 1:]))
|
||||
return value
|
||||
|
||||
def proof_read(path):
|
||||
path = plain_path(path)
|
||||
require(path.is_relative_to(out), 'proof path stays in output')
|
||||
return relocated(read(path.relative_to(out).as_posix()))
|
||||
|
||||
owners.read = proof_read
|
||||
# The bounded teardown gate parses its owner proof itself and pins the snapshots by their recorded
|
||||
# absolute paths; this private module instance reads them relocated, as the ordinary gate does.
|
||||
audio.json = types.SimpleNamespace(loads=lambda data: relocated(json.loads(data)))
|
||||
phases = end['phases']
|
||||
require(len(phases) == len(SUITES) + 2, 'exact import, parse, roster and restart phases')
|
||||
tests = read('all-tests.json')
|
||||
stage_files = shared.inventory(confined(out, 'stage'), stage=True)
|
||||
require(set(stage_files) == set(source) | {HANDOFF}, 'retained stage is the source and the restart handoff')
|
||||
handoff = stage_files[HANDOFF]
|
||||
require(len(tests) == len(SUITES) and end['all_tests_sha256'] == files['all-tests.json']['sha256'],
|
||||
'complete test rows')
|
||||
pids = []
|
||||
for index, (phase, test) in enumerate([('import', None), ('parse', None)]
|
||||
+ [(str(i).zfill(3), n) for i, n in enumerate(SUITES)]):
|
||||
row = read(phase + '-receipt.json')
|
||||
expected_row = dict(row)
|
||||
if test:
|
||||
expected_row.update(sequence=index - 2, godot=start['engine'])
|
||||
require(tests[index - 2] == expected_row, 'test row matches raw phase receipt')
|
||||
require(phases[index] == expected_row and row['phase'] == phase and row['test'] == test
|
||||
and row['passed'] is True and row['failed'] is False
|
||||
and row['validation_error'] is None and not row.get('guard_error')
|
||||
and row['default_player_entire_profile_unchanged'] is True,
|
||||
'exact successful phase')
|
||||
# The same proof classification as the shared collector (tools/run_all_tests.py).
|
||||
p = shared.phase_policy(test, Path(refs[test]['path']), policy['suite_policies']) if test else None
|
||||
ordinary = bool(p) and p['ordinary_cleanup_prepared'] is True
|
||||
bounded = bool(p) and p['old_bounded_teardown'] is True
|
||||
pid = row['actual_pid']
|
||||
require(type(pid) is int and pid > 0 and pid not in pids, 'distinct actual PIDs per phase')
|
||||
pids.append(pid)
|
||||
process = read(phase + '/process.json')
|
||||
require(process == row['native_process'] and process['pid'] == pid
|
||||
and process['owned_child_process_helper_sha256'] == native.HELPER_SHA
|
||||
== source['tools/regression/p09_owned_engine_process.py']['sha256'],
|
||||
'actual phase/process/helper agreement')
|
||||
stdout, stderr, logger = (text(phase + '/' + n) for n in
|
||||
('stdout.log', 'stderr.log', 'engine-native.log'))
|
||||
raw = stdout + stderr
|
||||
require(text(row['log']) == raw and row['log_sha256'] == files[row['log']]['sha256'],
|
||||
'literal complete raw stream')
|
||||
require(process['stdout_sha256'] == files[phase + '/stdout.log']['sha256']
|
||||
and process['stderr_sha256'] == files[phase + '/stderr.log']['sha256'],
|
||||
'process transport SHA')
|
||||
require(row['raw_native_log'] == dict(files[phase + '/engine-native.log'],
|
||||
path=str(origin / phase / 'engine-native.log')), 'native logger pin')
|
||||
review = diagnostics.review_streams(native, {
|
||||
'stdout': stdout, 'stderr': stderr, 'native_file': logger}, test)
|
||||
require(row['finite_original_diagnostic_review'] == review
|
||||
and diagnostics.diagnostic_result_gate(process, review)
|
||||
and shared.process_gate(process, review['allowed_e31_error_lines']),
|
||||
'strict diagnostics/process gates')
|
||||
require(native.native_diagnostics(stdout, stderr, row['command'])
|
||||
== process['strict_native_diagnostics'], 'raw banner/diagnostics recomputed')
|
||||
command = [start['engine'], '--headless', '--path', start['stage']]
|
||||
if phase == 'import':
|
||||
command += ['--editor', '--import', '--quit', '--verbose']
|
||||
else:
|
||||
script = ('bootstrap/parse_full_dependencies.gd' if phase == 'parse'
|
||||
else 'starters/' + phase + '.gd')
|
||||
command += ['--script', str(origin / script), '--verbose']
|
||||
require(row['starter_sha256'] == files[script]['sha256'], 'exact starter pin')
|
||||
if test:
|
||||
expected_starter = ('extends ' + json.dumps('res://' + test) + '\nconst Guard := preload('
|
||||
+ json.dumps(str(origin / 'bootstrap/profile_guard.gd'))
|
||||
+ ')\nfunc _initialize() -> void:\n\tif not Guard.activate():\n\t\tquit(97)\n\t\treturn\n\tsuper._initialize()\n')
|
||||
require(text(script) == expected_starter, 'unchanged guarded starter bytes')
|
||||
command += ['--log-file', str(origin / phase / 'engine-native.log')]
|
||||
require(row['command'] == process['argv'] == command, 'exact native argv/logger')
|
||||
for filename, field, event in [('started.json', 'actual_start_binding', 'ENGINE_STARTED'),
|
||||
('ended.json', 'actual_end_binding', 'ENGINE_ENDED')]:
|
||||
binding = read(phase + '/' + filename)
|
||||
require(binding == process[field] and binding['event'] == event
|
||||
and binding['actual_pid'] == binding['pid'] == pid
|
||||
and binding['owner_pid'] == start['owner_runner_pid']
|
||||
and binding['command'] == binding['argv'] == command, 'actual START/END ownership')
|
||||
binding = process['actual_start_binding']
|
||||
identity = binding['current_process_identity']
|
||||
if identity is None:
|
||||
require(binding['tracked_child_ended_before_initial_probe'] is True, 'early ended tracked child')
|
||||
else:
|
||||
require(binding['live_identity_exact'] is True and identity == {
|
||||
'pid': pid, 'ppid': start['owner_runner_pid'], 'pgid': pid, 'args': ' '.join(command)},
|
||||
'recorded live child PID/PPID/PGID/argv')
|
||||
finish = process['actual_end_binding']
|
||||
require(finish['engine_ended'] is True and finish['timed_out'] is False
|
||||
and finish['lifecycle_error'] is None and finish['exit_code'] == 0
|
||||
and finish['actual_process_exit_code'] == 0
|
||||
and finish['start_receipt_written'] is True
|
||||
and finish['monotonic_ns'] >= binding['monotonic_ns'], 'complete native END')
|
||||
cleanup = read(phase + '/adapter-owned-cleanup.json')
|
||||
require(cleanup == process['adapter_owned_cleanup'], 'raw adapter cleanup')
|
||||
cleanup_gate(cleanup)
|
||||
capture = row['outer_actual_capture']
|
||||
require(read(phase + '/outer-owned-finally.json') == {
|
||||
k: v for k, v in capture.items()
|
||||
if k not in ('outer_receipt_written', 'outer_receipt_error', 'passed')},
|
||||
'whole raw outer receipt')
|
||||
require(capture['actual_captured_pids'] == [pid] and capture['passed'] is True
|
||||
and capture['all_captured_ended'] is True and capture['adapter_exception'] is None
|
||||
and capture['outer_cleanup_errors'] == [] and capture['outer_receipt_error'] is None
|
||||
and capture['outer_receipt_written'] is True
|
||||
and capture['all_cleanup_before_outer_receipt'] is True
|
||||
and capture['foreign_process_or_group_signals'] == 0
|
||||
and row['engine_ended'] is True and row['exit_code'] == 0, 'owned outer capture')
|
||||
outer_cleanup(capture['cleanup'], [pid])
|
||||
startup = read(phase + '-startup-profile-before.json')
|
||||
require(startup == {'path': row['whole_startup_profile_after']['path'], 'existed': False,
|
||||
'files': {}, 'before_native_spawn': True}, 'fresh startup profile')
|
||||
require(row['whole_startup_profile_after']['fresh_before_engine'] is True,
|
||||
'startup fresh before engine')
|
||||
name = row['profile_startup_name']
|
||||
require(re.fullmatch('TheTree-PCK-tests-[0-9a-f]{32}-standard-startup-' + phase, name)
|
||||
and Path(startup['path']).name == name and row['private_project_fields'] == 2
|
||||
and row['private_project_sha256'] == hashlib.sha256(shared.private_project(
|
||||
(shared.HERE.parent / 'project.godot').read_bytes(), name)).hexdigest(),
|
||||
'exact two-field startup isolation')
|
||||
for label in ('startup', 'runtime'):
|
||||
if label == 'runtime' and phase == 'import':
|
||||
continue
|
||||
whole = row['whole_' + label + '_profile_after']
|
||||
archive = confined(out, 'profiles/' + phase + '/' + label)
|
||||
require(archive.is_dir() and shared.inventory(archive)
|
||||
== whole['files'], 'complete retained private profile')
|
||||
if phase != 'import':
|
||||
guard = read(phase + '-profile.json')
|
||||
runtime = Path(row['whole_runtime_profile_after']['path'])
|
||||
name = guard['custom_user_dir_name']
|
||||
require(re.fullmatch('TheTree-PCK-tests-[0-9a-f]{32}-standard-runtime-' + phase, name)
|
||||
and runtime.name == name and guard['verified'] is True
|
||||
and guard['conditions'] == [True] * 5 and guard['conditions_checked'] == 5
|
||||
and guard['fresh_before_parent_initialize'] is True
|
||||
and guard['profile_writes_before_guard'] == 0
|
||||
and guard['actual_user_data_dir'] == guard['expected_user_data_dir'] == str(runtime)
|
||||
and guard['globalized_probe'] == str(runtime / 'guard-probe')
|
||||
and guard['project_settings_persisted'] is False, 'all unchanged Guard conditions')
|
||||
require(row['actual_private_profile'] == {
|
||||
'report': guard, 'report_sha256': files[phase + '-profile.json']['sha256'],
|
||||
'whole_files': row['whole_runtime_profile_after']['files']}, 'whole raw Guard report')
|
||||
require(row['controlled_environment'] == {
|
||||
'THE_TREE_PACK_PROFILE_NAME': name, 'THE_TREE_PACK_PROFILE_EXPECTED': str(runtime),
|
||||
'THE_TREE_PACK_PROFILE_REPORT': str(origin / (phase + '-profile.json')),
|
||||
'THE_TREE_FULL_PARSE_PATHS': str(origin / 'parse-paths.json'),
|
||||
**({'THE_TREE_AUDIO_TEARDOWN_PROOF_DIR': str(origin / (phase + '-teardown/owned-audio')),
|
||||
'THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF': str(origin / (phase + '-teardown/control.json'))}
|
||||
if ordinary else {'THE_TREE_AUDIO_TEARDOWN_PROOF_DIR': str(origin / (phase + '-teardown'))}
|
||||
if bounded else {})},
|
||||
'exact controlled Guard/proof environment')
|
||||
else:
|
||||
require(row['controlled_environment'] == {}, 'fresh import environment')
|
||||
if phase == 'parse':
|
||||
require(len(re.findall(r'(?m)^FULL_DEPENDENCY_PARSE\b', raw)) == 1
|
||||
and re.findall(r'(?m)^FULL_DEPENDENCY_PARSE checks=([0-9]+) failures=([0-9]+)\n', raw)
|
||||
== [(str(len(parse_paths)), '0')] and row['actual_parse_checks'] == len(parse_paths), 'unique all-script parse marker')
|
||||
if test:
|
||||
parsed = markers.parse_raw(test, raw, contracts, source[test]['sha256'])
|
||||
require(parsed['passed'] and row['completion'] == parsed
|
||||
and row['reported_checks'] == parsed['checks']
|
||||
and row['reported_failures'] == parsed['failures']
|
||||
and row['completion_marker'] == parsed['marker'] and row['collector_errors'] == [],
|
||||
'source-pinned raw completion marker')
|
||||
require(row['source_sha256'] == row['source_sha256_end'] == source[test]['sha256'],
|
||||
'exact test source before/after')
|
||||
require(row['policy'] == p, 'current exact suite policy')
|
||||
if ordinary:
|
||||
proof = owners.ordinary_gate(confined(out, phase + '-teardown'), pid, out,
|
||||
p['ordinary_control_labels'])
|
||||
require(proof == relocated(row['ordinary_owner_pairs']), 'full ordinary/audio ownership proof')
|
||||
if bounded:
|
||||
proof = audio.teardown_gate(confined(out, phase + '-teardown'), pid, out)
|
||||
require(proof == relocated(row['old_bounded_proof']), 'full bounded audio teardown proof')
|
||||
require(('ordinary_owner_pairs' in row) == ordinary and ('old_bounded_proof' in row) == bounded
|
||||
and any(name.startswith(phase + '-teardown/') for name in files) == (ordinary or bounded),
|
||||
'teardown proofs exactly for the classified suites')
|
||||
if test == RESTART[0]:
|
||||
require(row['generated_reports'] == {HANDOFF: dict(handoff, actual_engine_pid=pid,
|
||||
producer_test=test, producer_source_sha256=source[test]['sha256'], payload_modified=False)},
|
||||
'the writer generates exactly the retained restart handoff')
|
||||
require(re.findall(WRITER_LINE, raw) == [(str(pid), handoff['sha256'])]
|
||||
and read_json(confined(out, 'stage/' + HANDOFF))['writer_pid'] == pid,
|
||||
'the writer engine printed and recorded its own handoff')
|
||||
writer_pid = pid
|
||||
else:
|
||||
require(row['generated_reports'] == {}, 'only the restart writer generates a report')
|
||||
if test == RESTART[1]:
|
||||
require(re.findall(READER_LINE, raw) == [(str(pid), str(writer_pid), handoff['sha256'])]
|
||||
and pid != writer_pid, 'a separate reader engine read the exact writer handoff')
|
||||
require(end['actual_captured_PIDs'] == pids, 'all captured engines')
|
||||
outer_cleanup(end['final_owned_cleanup'], pids)
|
||||
require(all(end[k] == v for k, v in shared.totals(tests, SUITES).items()), 'recomputed exact totals')
|
||||
require(shared.inventory(confined(out, 'stage'), stage=True) == dict(source, **{HANDOFF: handoff}),
|
||||
'entire retained source/settings and handoff exact')
|
||||
for name, pin in shared.read(shared.SUPPORT / 'bootstrap-scripts.json').items():
|
||||
require(text('bootstrap/' + name).encode() == pin['text'].encode()
|
||||
and files['bootstrap/' + name]['sha256'] == pin['sha256'], 'unchanged bootstrap/Guard pins')
|
||||
require(inventory(out) == dict(files, **{'receipt.json': receipt_pin}),
|
||||
'whole raw evidence unchanged during verification')
|
||||
require(content_map(shared.source_refs(shared.HERE.parent)) == source,
|
||||
'whole current source unchanged during verification')
|
||||
print(json.dumps({'verified': True, 'actual_suites': len(SUITES), 'parse_checks': len(parse_paths),
|
||||
'actual_checks': end['actual_all_roster_checks'], 'engines_started': 0,
|
||||
'formal_acceptance': False, 'original_unit_complete': False}))
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
argv = list(sys.argv[1:] if argv is None else argv)
|
||||
# No argparse abbreviation, repeated flags, = aliases or positional tokens.
|
||||
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
|
||||
parser.add_argument('--output', required=True)
|
||||
modes = parser.add_mutually_exclusive_group()
|
||||
modes.add_argument('--collect', action='store_true', help='Root only; fresh native collection')
|
||||
modes.add_argument('--prepare-only', action='store_true')
|
||||
seen = set()
|
||||
i = 0
|
||||
while i < len(argv):
|
||||
flag = argv[i]
|
||||
require(flag in ('--output', '--collect', '--prepare-only', '--help')
|
||||
and flag not in seen, 'unknown, duplicate or malformed flag')
|
||||
seen.add(flag)
|
||||
if flag == '--output':
|
||||
i += 1
|
||||
require(i < len(argv) and argv[i] and not argv[i].startswith('--'), 'output value required')
|
||||
i += 1
|
||||
args = parser.parse_args(argv)
|
||||
args.timeout = 1200
|
||||
root = shared.HERE.parent
|
||||
output = args.output
|
||||
# The registered validation passes the literal '{build_dir}/p07-weather-evidence'. The current Runner
|
||||
# replaces only {python} in validation argv and gives the task's build directory as BUILD_OUTPUT_DIR.
|
||||
if output.startswith('{build_dir}/'):
|
||||
build = os.environ.get('BUILD_OUTPUT_DIR', '')
|
||||
require(build and Path(build).is_absolute() and Path(build).is_dir() and not Path(build).is_symlink(),
|
||||
'registered build directory from BUILD_OUTPUT_DIR')
|
||||
output = build + output[len('{build_dir}'):]
|
||||
out = plain_path(output)
|
||||
refs = shared.source_refs(root)
|
||||
require(set(SUITES).issubset(shared.roster(root)), 'normal discovery includes narrow roster and restart tests')
|
||||
discovered = shared.roster(root)
|
||||
require(discovered.index(RESTART[0]) < discovered.index(RESTART[1]), 'the full roster also runs the writer first')
|
||||
markers = shared.module('p07_exact_markers', shared.HERE / 'strict_test_markers.py')
|
||||
contract_path = root / 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
|
||||
contract_sha = shared.sha(contract_path)
|
||||
contracts = markers.load_contracts(contract_path)
|
||||
require(all(name in contracts for name in SUITES), 'complete marker roster')
|
||||
policy = shared.read(shared.SUPPORT / 'suite-policy.json')
|
||||
shared.policy_coverage(SUITES, refs, policy)
|
||||
for test in SUITES:
|
||||
shared.phase_policy(test, Path(refs[test]['path']), policy['suite_policies'])
|
||||
require(test not in policy['outside_routes'], 'no outside report routes in the narrow run')
|
||||
require(policy['fixed_generated_report_routes'].get(test, []) == ([HANDOFF] if test == RESTART[0] else []),
|
||||
'only the restart writer declares one generated report')
|
||||
for test in ROSTER[:1] + RESTART:
|
||||
require(contracts[test]['source_sha256_at_schema_audit'] == refs[test]['sha256'],
|
||||
'new strict source-hashed marker')
|
||||
parse_paths = ['res://' + name for name in sorted(refs) if name.endswith('.gd')]
|
||||
if args.prepare_only:
|
||||
ancestor = out.parent
|
||||
while not ancestor.exists():
|
||||
ancestor = ancestor.parent
|
||||
free = shutil.disk_usage(ancestor).free
|
||||
size = sum(p['bytes'] for p in refs.values())
|
||||
print(json.dumps({'roster': ROSTER, 'source_files': len(refs), 'source_bytes': size,
|
||||
'free_bytes': free, 'conservative_required_bytes': size + shared.MIN_SPACE,
|
||||
'capacity_available': free >= size + shared.MIN_SPACE, 'parse_paths': len(parse_paths),
|
||||
'restart_phases': RESTART, 'future_native_phases': len(SUITES) + 2, 'actual_checks': None, 'engines': 0, 'stages': 0,
|
||||
'formal_acceptance': False}))
|
||||
return 0
|
||||
if args.collect:
|
||||
require(not out.exists() and out.parent.is_dir(), 'fresh output with existing parent required')
|
||||
require(not out.is_relative_to(root), 'private collection outside source tree')
|
||||
return collect(args, root, out, refs, markers, contracts, policy,
|
||||
contract_path, contract_sha, parse_paths)
|
||||
return verify(out, refs, markers, contracts, policy, contract_sha, parse_paths)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (AssertionError, OSError, ValueError, KeyError, TypeError) as error:
|
||||
print(type(error).__name__ + ': ' + str(error), file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
Reference in New Issue
Block a user