P06-01: Fix review r01 findings for A06 reserve
Settle Mana raised above capacity between boundaries as recorded overflow instead of rejecting saves or latching a fault, exclude destroyed facilities from upkeep demand, and keep the depletion drill's outage sustained. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
65534cfd15
commit
f247d19325
@@ -99,6 +99,7 @@ test-s6:
|
||||
|
||||
test-mvp:
|
||||
$(GODOT) --headless --path . --script res://tests/mvp/test_premium_room_guest_flow.gd
|
||||
$(GODOT) --headless --path . --script res://tests/mvp/test_runtime_mana_depletion.gd
|
||||
$(GODOT) --headless --path . --script res://tests/mvp/test_premium_room.gd
|
||||
$(GODOT) --headless --path . --script res://tests/mvp/test_original_room_zones.gd
|
||||
$(GODOT) --headless --path . --script res://tests/mvp/test_upkeep_loss_streak.gd
|
||||
|
||||
@@ -866,18 +866,18 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
# (0 stays stopped) only within capacity; nothing is silently truncated.
|
||||
var restored_mana_reserve := ManaReserveCycle.new()
|
||||
var saved_game_resources: Variant = snapshot[&"game_state"].get(&"resources", null)
|
||||
if not saved_game_resources is Dictionary \
|
||||
or not ManaReserveCycle.amount_is_valid(saved_game_resources.get(&"mana", saved_game_resources.get("mana", null))):
|
||||
if not saved_game_resources is Dictionary:
|
||||
return false
|
||||
var saved_mana: Variant = saved_game_resources.get(&"mana", saved_game_resources.get("mana", null))
|
||||
if runtime_data.has(&"mana_reserve"):
|
||||
if not restored_mana_reserve.restore_snapshot(runtime_data[&"mana_reserve"]) \
|
||||
# Between boundaries Mana may exceed capacity; the next boundary records
|
||||
# and discards the excess, so only a valid integer amount is required.
|
||||
if not ManaReserveCycle.stored_amount_is_valid(saved_mana) \
|
||||
or not restored_mana_reserve.restore_snapshot(runtime_data[&"mana_reserve"]) \
|
||||
or restored_mana_reserve.get_minute() > restored_minute:
|
||||
return false
|
||||
var last_reserve_flow := restored_mana_reserve.get_last_flow()
|
||||
if not last_reserve_flow.is_empty() \
|
||||
and int(saved_game_resources.get(&"mana", saved_game_resources.get("mana", 0))) > int(last_reserve_flow[&"after"]):
|
||||
return false
|
||||
elif not restored_mana_reserve.start_at(restored_minute):
|
||||
elif not ManaReserveCycle.amount_is_valid(saved_mana) \
|
||||
or not restored_mana_reserve.start_at(restored_minute):
|
||||
return false
|
||||
var restored_economy := EconomyService.new()
|
||||
if not restored_economy.restore_snapshot(runtime_data[&"economy"]):
|
||||
@@ -10741,6 +10741,16 @@ func _reservation_references_are_valid(
|
||||
return false
|
||||
return true
|
||||
|
||||
## A06 upkeep demand: every owned consumer, open or not, except destroyed
|
||||
## facilities. Removed facilities are already unregistered from the network.
|
||||
func _mana_reserve_demand() -> int:
|
||||
var destroyed: Array[StringName] = []
|
||||
for facility: FacilityInstance in facility_service.get_instances():
|
||||
if facility.suspension_reason == &"DESTROYED":
|
||||
destroyed.append(facility.instance_id)
|
||||
return mana_network_service.get_total_consumer_demand(destroyed)
|
||||
|
||||
|
||||
func _advance_mana(absolute_minute: int) -> void:
|
||||
if _operations_tick_blocked:
|
||||
return
|
||||
@@ -10760,7 +10770,7 @@ func _advance_mana(absolute_minute: int) -> void:
|
||||
# A06: generation and upkeep demand settle before local distribution and the
|
||||
# whole-tower gate, so the network and projection see the committed reserve.
|
||||
var reserve_flow := mana_reserve.advance_to(
|
||||
absolute_minute, int(_state.get_resource(&"mana")), mana_network_service.get_total_consumer_demand()
|
||||
absolute_minute, int(_state.get_resource(&"mana")), _mana_reserve_demand()
|
||||
)
|
||||
if not bool(reserve_flow.get(&"ok", false)):
|
||||
_latch_infrastructure_failure(
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
{
|
||||
"id": "A06",
|
||||
"title": "マナ有限残量契約(MP-01-r02推薦案)",
|
||||
"decision": "初期100・容量300・世界樹生成20/ゲーム内10分。10分境界ごとにB+G+F−Dを計算し、需要を先に支払い、超過は明示破棄、0で全館停止。需要は登録済み全consumerのmaintenance_mana合計(閉店・停止中も計上)。炉の投入は自然生成と需要支払後のbase_after=min(C,max(0,B+G−D))で空き100以上のとき受理。現Runtimeのphase順序を保持しMANA内で処理。旧セーブの残量は0〜300のみ保持し、超過・不正値は拒否。炉の人物適格性・収率の実接続は別承認。",
|
||||
"decision": "初期100・容量300・世界樹生成20/ゲーム内10分。10分境界ごとにB+G+F−Dを計算し、需要を先に支払い、超過は明示破棄、0で全館停止。需要は保有施設のmaintenance_mana合計(閉店・停止中も計上、撤去・破壊済みは除外)。境界間に返却等で容量を超えた分は次の境界でoverflowとして記録・破棄。炉の投入は自然生成と需要支払後のbase_after=min(C,max(0,B+G−D))で空き100以上のとき受理。現Runtimeのphase順序を保持しMANA内で処理。受入契約なしの旧セーブの残量は0〜300のみ保持し、超過・不正値は拒否。炉の人物適格性・収率の実接続は別承認。",
|
||||
"origin": "user_reply_in_conversation",
|
||||
"approved_at_jst": "2026-10-19",
|
||||
"reference": "execution/mana-proposal/PROPOSAL.md"
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
|
||||
## 製品への反映
|
||||
|
||||
- `scripts/mana/mana_reserve_cycle.gd`:承認値と境界処理です。10分境界ごとに `available=B+G+F`、`consumed=min(available,D)`、`after=min(C,available−consumed)` を計算します。容量を超えた分は overflow として記録し、破棄します。同じ境界は二度処理しません。境界を飛ばした場合は、各境界を順番に処理します。保存データは検証が通ってから反映し、不正なら何も変えません。炉の受理判定は `furnace_offer_accepted`(base_after 後の空きが100以上)です。
|
||||
- `scripts/mana/mana_network_service.gd`:`get_total_consumer_demand()` を追加しました。登録済みの全 consumer の需要を合計します。無効化・停止中の consumer も含みます。
|
||||
- `scripts/mana/mana_reserve_cycle.gd`:承認値と境界処理です。10分境界ごとに `available=B+G+F`、`consumed=min(available,D)`、`after=min(C,available−consumed)` を計算します。容量を超えた分は overflow として記録し、破棄します。イベント費用の返却などで境界の間に容量を超えた場合も、次の境界で同じく処理します。同じ境界は二度処理しません。境界を飛ばした場合は、各境界を順番に処理します。保存データは検証が通ってから反映し、不正なら何も変えません。炉の受理判定は `furnace_offer_accepted`(base_after 後の空きが100以上)です。
|
||||
- `scripts/mana/mana_network_service.gd`:`get_total_consumer_demand(excluded_ids)` を追加しました。登録済みの consumer の需要を合計します。無効化・停止中の consumer も含みます。Runtime の `_mana_reserve_demand()` が、破壊済み(DESTROYED)施設を除外して呼び出します。撤去済みの施設はネットワークから登録解除されます。
|
||||
- `autoload/simulation_runtime.gd`:MANA phase の最初で残量を確定してから、局所配電と全館ゲートを評価します。phase の順序は変えていません。新しいセーブは `mana_reserve` を持ちます。Operations の巻き戻し対象にも含めました。
|
||||
- 現行セーブは整数の残量を受け付けます。境界の間に容量を超えた値も読み込めます。
|
||||
- 旧セーブの移行:`mana_reserve` が無いセーブは、残量が0〜300の整数であれば、その値を保持して開始します。明示的な0は停止のまま保持します。300超・負値・非整数は拒否し、現在の状態を保持します。
|
||||
|
||||
## 試験
|
||||
@@ -18,3 +19,16 @@
|
||||
|
||||
- 炉の人物適格性・収率の実接続、未定義13施設の需要、D05 の詳細は別承認です。
|
||||
- 全館の実プレイ、性能、Windows での受入は未実施です。原88の正式完了数は変わりません。
|
||||
|
||||
## 独立レビュー r01 の指摘と修正
|
||||
|
||||
候補 65534cf への判定は FAIL でした。修正した内容は次のとおりです。
|
||||
|
||||
1. 境界の間にマナが増えた場合の不整合を直しました。以前は、読込を拒否するか、毎分 INVALID_FLOW を起こして全館が停止していました。現在は、次の境界で overflow として記録・破棄します。
|
||||
2. `test_runtime_mana_depletion` は停電が続く前提でした。`ManaOutage` に置き換え、旧セーブも `mana_reserve` を外して作るようにしました。また、Makefile の test-mvp に追加しました。
|
||||
3. 需要に破壊済みの施設が含まれていました。提案書の「撤去・破壊済みは除外」に合わせて除外し、A06 の記録文も同じ内容に訂正しました。
|
||||
|
||||
## 未検証として残す点
|
||||
|
||||
- 世界樹の生成を0にする経路が無いため、提案書の比較表にある「生成0→0→通常」は適用外です。
|
||||
- 炉の receipt と配電投影の保存は、炉の実接続が別承認なので対象外です。
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
"build_atomic_requirements.py": "6d6ea9e4216372dbd0f9fed3f885a4481b21a3f539a26f64cf98a6830af984c5",
|
||||
"build_facility_value_contracts.py": "35027b23ce75ce9e4e0cdfc5132fd2eb771ba9f598d97750d7ecf9ecb257f769",
|
||||
"current_evidence_links.json": "db1837bcc8497201134297e312f4b266a297102285307ec3075d4e23632e26c2",
|
||||
"decisions.json": "927065d9f840e68667d10998d26eb5d04f5591d9f24082106d082e51d5af5c96",
|
||||
"decisions.json": "7771ef179b73f061363685a218f7b0d300847fc5881ce0c8b108ca62b2076dfe",
|
||||
"facility_value_contracts.json": "447388ba03a29bfac25f2bcd068665aedbb07ba7641ce86f2db93d44f4c9f326",
|
||||
"facility_value_contracts_validation.json": "bf490a9ca585c48fc318e76c4a069ef244827d828d46e6415399f7710590dd16",
|
||||
"performance_audit.json": "0b630b15a392c3e0366f1362bfe4ddacb968f915f54549f82e4d6fa315384762",
|
||||
|
||||
@@ -231,11 +231,12 @@ func get_storage_current(node_id: StringName) -> int:
|
||||
return int(_nodes[node_id][&"current"])
|
||||
|
||||
|
||||
## Upkeep demand of every registered consumer, including disabled ones.
|
||||
func get_total_consumer_demand() -> int:
|
||||
## Upkeep demand of every registered consumer, including disabled ones,
|
||||
## except the given consumer ids (for example destroyed facilities).
|
||||
func get_total_consumer_demand(excluded_ids: Array = []) -> int:
|
||||
var total := 0
|
||||
for node_id: StringName in _nodes:
|
||||
if int(_nodes[node_id][&"kind"]) == NodeKind.CONSUMER:
|
||||
if int(_nodes[node_id][&"kind"]) == NodeKind.CONSUMER and not node_id in excluded_ids:
|
||||
total += int(_nodes[node_id][&"demand"])
|
||||
return total
|
||||
|
||||
|
||||
@@ -34,10 +34,12 @@ 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:
|
||||
if _minute < 0:
|
||||
return _failure(&"NOT_STARTED")
|
||||
if typeof(minute) != TYPE_INT or not _amount(before) or before > CAPACITY \
|
||||
if typeof(minute) != TYPE_INT or not _amount(before) \
|
||||
or typeof(demand) != TYPE_INT or demand < 0 or demand > LIMIT or minute < _minute:
|
||||
return _failure(&"INVALID_FLOW")
|
||||
var boundary: int = minute - minute % INTERVAL_MINUTES
|
||||
@@ -69,9 +71,14 @@ static func plan(before: int, demand: int, furnace: int, start: int, end: int) -
|
||||
&"consumed": consumed, &"shortage": demand - consumed,
|
||||
&"overflow": maxi(remaining - CAPACITY, 0), &"after": mini(remaining, CAPACITY)}
|
||||
|
||||
## Legacy saves without a reserve receipt must already fit the approved capacity.
|
||||
static func amount_is_valid(value: Variant) -> bool:
|
||||
return _amount(value) and value <= CAPACITY
|
||||
|
||||
## A current save may hold more than capacity until the next boundary settles it.
|
||||
static func stored_amount_is_valid(value: Variant) -> bool:
|
||||
return _amount(value)
|
||||
|
||||
func create_snapshot() -> Dictionary:
|
||||
return {&"version": VERSION, &"policy_id": POLICY_ID, &"capacity": CAPACITY,
|
||||
&"generation": WORLD_TREE_GENERATION, &"interval_minutes": INTERVAL_MINUTES,
|
||||
@@ -97,7 +104,7 @@ func restore_snapshot(source: Variant) -> bool:
|
||||
for key: Variant in flow:
|
||||
if not _amount(flow[key]):
|
||||
return false
|
||||
if flow["to_minute"] != source["minute"] or flow["before"] > CAPACITY \
|
||||
if flow["to_minute"] != source["minute"] \
|
||||
or flow["generation"] != WORLD_TREE_GENERATION \
|
||||
or flow["from_minute"] + INTERVAL_MINUTES != flow["to_minute"]:
|
||||
return false
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const ManaOutage = preload("res://tests/helpers/mana_outage.gd")
|
||||
var checks := 0
|
||||
var failures := 0
|
||||
func _initialize() -> void:
|
||||
@@ -31,11 +32,12 @@ func _run() -> void:
|
||||
runtime.free(); clock.free(); state.free(); quit(failures); return
|
||||
# Only reserve depletion is a diagnostic input; no numeric generation policy
|
||||
# is asserted by this test. Everything else advances through Runtime ticks.
|
||||
state.set_resource(&"mana", 0)
|
||||
ManaOutage.hold(runtime, state)
|
||||
# Pre-fix saves legitimately contain zero reserve with old powered flags.
|
||||
var legacy: Dictionary = runtime.create_snapshot()
|
||||
var legacy_data: Dictionary = legacy[&"systems"][&"runtime"]
|
||||
legacy_data[&"runtime_schema_version"] = 9
|
||||
legacy_data.erase(&"mana_reserve")
|
||||
preload("res://tests/fixtures/pre_upkeep_runtime.gd").strip_empty_upkeep(legacy_data)
|
||||
var source_copy := legacy.duplicate(true)
|
||||
var old_reception_id := reception.instance_id
|
||||
@@ -65,7 +67,7 @@ func _run() -> void:
|
||||
var room_id := room.instance_id
|
||||
var reception_id := reception.instance_id
|
||||
runtime.facility_service.set_operational_status(room_id, {&"suspension_reason": &"DESTROYED"}, clock.absolute_minute)
|
||||
state.set_resource(&"mana", 1)
|
||||
ManaOutage.release(runtime, state, 1)
|
||||
clock.advance_minutes(1)
|
||||
room = runtime.facility_service.get_instance(room_id)
|
||||
reception = runtime.facility_service.get_instance(reception_id)
|
||||
|
||||
@@ -34,7 +34,13 @@ func _initialize() -> void:
|
||||
var saved: Dictionary = cycle.create_snapshot()
|
||||
expect(not cycle.advance_to(370, 57, 63)[&"processed"] and cycle.create_snapshot() == saved, "same boundary never consumed twice")
|
||||
expect(not cycle.advance_to(360, 57, 63)[&"ok"], "time reversal rejected")
|
||||
expect(not cycle.advance_to(380, 301, 0)[&"ok"] and cycle.create_snapshot() == saved, "over-capacity reserve rejected without change")
|
||||
expect(not cycle.advance_to(380, -1, 0)[&"ok"] and cycle.create_snapshot() == saved, "negative reserve rejected without change")
|
||||
var high = Cycle.new()
|
||||
high.start_at(360)
|
||||
flow = high.advance_to(370, 525, 16)[&"flow"]
|
||||
expect(flow[&"after"] == 300 and flow[&"overflow"] == 229, "amount raised above capacity between boundaries settles with recorded overflow")
|
||||
var high_restored = Cycle.new()
|
||||
expect(high_restored.restore_snapshot(high.create_snapshot()), "over-capacity before survives snapshot validation")
|
||||
var skipped: Dictionary = cycle.advance_to(400, 57, 63)
|
||||
expect(skipped[&"boundaries"] == 3 and skipped[&"after"] == 0 and skipped[&"flow"][&"shortage"] == 43, "skipped boundaries settle in order")
|
||||
var restored = Cycle.new()
|
||||
|
||||
@@ -18,6 +18,8 @@ func _run() -> void:
|
||||
_test_generation_and_demand()
|
||||
_test_save_round_trip_and_migration()
|
||||
_test_depletion_stops_tower()
|
||||
_test_between_boundary_increase()
|
||||
_test_destroyed_facility_demand()
|
||||
_cleanup()
|
||||
if _failures == 0:
|
||||
print("PASS: %d checks" % _checks)
|
||||
@@ -100,6 +102,46 @@ func _test_depletion_stops_tower() -> void:
|
||||
_expect(_state.get_resource(&"mana") == maxi(0, 20 - demand), "world tree recovers the reserve at the next boundary")
|
||||
|
||||
|
||||
func _test_between_boundary_increase() -> void:
|
||||
_clock.advance_minutes(10 - (_clock.absolute_minute % 10) + 3)
|
||||
_state.add_resource(&"mana", 400)
|
||||
var high: int = _state.get_resource(&"mana")
|
||||
_expect(high > Cycle.CAPACITY, "mana can rise above capacity between boundaries (event refund / command)")
|
||||
var saved: Dictionary = _runtime.create_snapshot()
|
||||
_expect(_runtime.restore_snapshot(saved.duplicate(true)) and _state.get_resource(&"mana") == high, "over-capacity mid-interval save restores")
|
||||
_clock.advance_minutes(1)
|
||||
_expect(not _runtime.get_operations_report()[&"runtime_failure_latched"], "over-capacity amount does not latch a runtime fault")
|
||||
_clock.advance_minutes(10 - (_clock.absolute_minute % 10))
|
||||
var flow: Dictionary = _runtime.mana_reserve.get_last_flow()
|
||||
_expect(_state.get_resource(&"mana") == Cycle.CAPACITY and flow[&"before"] == high, "next boundary settles to capacity")
|
||||
_expect(flow[&"overflow"] == high + 20 - int(flow[&"consumed"]) - Cycle.CAPACITY, "excess is recorded as overflow")
|
||||
|
||||
|
||||
func _test_destroyed_facility_demand() -> void:
|
||||
var build = _runtime.submit_command(
|
||||
&"build_facility",
|
||||
{&"definition_id": &"cafeteria", &"origin": Vector3i(-5, 0, WorldService.LAYER_FRONT), &"rotation": 0},
|
||||
&"player", &"", 50, -1, &"a06.build.cafeteria"
|
||||
)
|
||||
_expect(build.is_success(), "cafeteria construction starts")
|
||||
_clock.advance_minutes(180)
|
||||
var cafeteria: FacilityInstance = null
|
||||
for facility: FacilityInstance in _runtime.facility_service.get_instances():
|
||||
if facility.definition_id == &"cafeteria":
|
||||
cafeteria = facility
|
||||
_expect(cafeteria != null, "cafeteria completes")
|
||||
if cafeteria == null:
|
||||
return
|
||||
var with_cafeteria: int = _runtime.call("_mana_reserve_demand")
|
||||
_expect(with_cafeteria == _runtime.mana_network_service.get_total_consumer_demand(), "owned facility demand counts")
|
||||
_runtime.facility_service.set_operational_status(cafeteria.instance_id, {&"business_open": false}, _clock.absolute_minute)
|
||||
_expect(_runtime.call("_mana_reserve_demand") == with_cafeteria, "closed facility still pays upkeep demand")
|
||||
_runtime.facility_service.set_operational_status(cafeteria.instance_id, {&"suspension_reason": &"DESTROYED"}, _clock.absolute_minute)
|
||||
_expect(_runtime.call("_mana_reserve_demand") == with_cafeteria - 2, "destroyed facility demand is excluded")
|
||||
_clock.advance_minutes(10 - (_clock.absolute_minute % 10))
|
||||
_expect(int(_runtime.mana_reserve.get_last_flow()[&"demand"]) == with_cafeteria - 2, "boundary uses demand without destroyed facility")
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
|
||||
Reference in New Issue
Block a user