Compare commits

...
13 Commits
Author SHA1 Message Date
Claude Opus 5.5 (Desktop) fee479af83 docs: freeze the P24-02-EVLOG event log readability specification
SPEC r01 and its provenance for the auxiliary P24-02-EVLOG (original
credit 0): the event log names every fact kind the product publishes
(WEATHER_FORECAST_DRAWN and WARP_TRAVELED had none), shows the weather
and times of the daily forecast and the warp riders and fare, and
resolves path actor, warp connector and elevator shaft targets. It is
presentation only; the Runtime, the event stream and saves do not
change. Eight acceptance conditions, the registered validation
p24-evlog, one final S full roster and a fresh separate review.

No new human decision is needed. Only the range whose upstream is
accepted (event stream, weather, warp, elevators) is registered; the
whole original P24-02, the P24 package, the earlier
the-tree-p24-02-operations task, the important-warning cut-ins and
the Windows, IME and OS scaling conditions stay open. No product
change, registration or commander run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:22:54 +09:00
Claude Opus 5.5 (Desktop) 50207e9e6e Correct the retention claim in the P16-01-DISMISS formal proof
The formal proof said all_prior_failures_retained was true without
qualification. The first a073ccb derive run (its two outputs, stdout
and first script bytes) and the two stage-only UI diagnostic copies are
not kept as original bytes, so the field is now false. New fields state
what is kept (every formal test raw output, the earlier reviews and
deliveries, the failed run logs and receipts) and what is missing, with
a correction note; record_gaps_kept is unchanged. The ledger references
to the proof take its new hash and the static plan check is rewritten.
The review result, delivery, raw outputs and history are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:58:32 +09:00
Claude Opus 5.5 (Desktop) 34b16de279 Align the P16-01-DISMISS acceptance record with its kept record gaps
The acceptance record said every failure's raw output was kept, which
contradicts the record gaps listed right below it (the first a073ccb
derive run and the stage-only diagnostic copies). It now says the
formal test raw output and the earlier review results are kept, no
earlier S evidence is reused for the new S, and the missing original
bytes are stated in the next section. The two ledger references to the
record take its new hash; the static plan check is rewritten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:55:36 +09:00
Claude Opus 5.5 (Desktop) 8a2ea74f44 Record the P16-01-DISMISS auxiliary acceptance and keep original P16-01 open
P16-01-DISMISS (dismissal of a general employee by the ordinary
command and the Main staff panel, with the A14 severance, held unpaid
wages, refusals R1-R9 and the cleanup at the command boundary) passed
all four normal gates: validation, review, integration and final. A
fresh separate Opus review passed F1-F10 with no high or medium
finding and five lows. S f5fe219 -> Q 4ca3bbe with the same tree; the
measurements are on S.

Original credit is 0, so the count stays 24/88 and 4/30. Original
P16-01 moves from planned to in_progress; recruitment, market and debt
acquisition and the cost per employment category remain. P16-03 stays
planned with a note. The P05 residual on the dismissal severance is
restated (A14 answered, connected, not counted as P05 completion).
The five review lows, the R9 boundary, the kept record gaps and the
Windows device acceptance are disclosed. Earlier failures and
changes_requested reviews are kept. IMPLEMENTATION_PLAN gains the
current 24-unit, feature-first section above the kept history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:52:22 +09:00
Agent Control Runner 4ca3bbef55 Merge commit 'f5fe2196740466a673879cb2950d96446fd99d6e' into HEAD 2026-10-25 18:38:28 +09:00
Agent Control Runner f5fe219674 P16-01-DISMISS: 一般従業員の通常操作での解雇(退職金を含む。P16-01 の補助の作業。原単位への加算 0) 2026-10-25 17:01:25 +09:00
Agent Control Runner a073ccb59b P16-01-DISMISS: 一般従業員の通常操作での解雇(退職金を含む。P16-01 の補助の作業。原単位への加算 0) 2026-10-25 14:45:45 +09:00
Agent Control Runner b06fec17c7 P16-01-DISMISS: 一般従業員の通常操作での解雇(退職金を含む。P16-01 の補助の作業。原単位への加算 0) 2026-10-24 12:49:31 +09:00
Claude Opus 5.5 (Desktop) 0b59d7c23c Record the P07-01-WX auxiliary acceptance and keep original P07-01 open
P07-01-WX (natural weather, rain arrivals and stay, storm, world tree
and adult fog effects, HUD forecast, save and migration) passed all
four normal gates: validation, review, integration and final. A fresh
separate Opus review passed WX1-WX8 with no high or medium finding and
two lows. S db87270 -> Q 9b5b09b with the same tree; the measurements
are on S.

Original credit is 0, so the count stays 24/88 and 4/30. Original
P07-01 moves from planned to in_progress. The furnace yield 120 at fog
boundaries and AR.weather.fog_magic remain, waiting for the P13 furnace
facility, D04 and the A06 separate approval. The event-log name of the
weather event and the two review lows are disclosed. Earlier failures
and changes_requested reviews are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:45:24 +09:00
Agent Control Runner 9b5b09b35e Merge commit 'db87270972c9b243e3a1fccdf2c1fa96975dffc1' into HEAD 2026-10-24 10:25:30 +09:00
Agent Control Runner db87270972 P07-01-WX: 仕上がった天候の機能の受入:自然天候・雨・嵐・世界樹と成人客のマナ霧・上部の表示と予報・保存と移行(マナ炉の産出 120 を除く。P07-01 の補助の作業。原単位への加算 0) 2026-10-24 08:59:36 +09:00
Agent Control Runner f99c881bca P07-01-WX: 仕上がった天候の機能の受入:自然天候・雨・嵐・世界樹と成人客のマナ霧・上部の表示と予報・保存と移行(マナ炉の産出 120 を除く。P07-01 の補助の作業。原単位への加算 0) 2026-10-24 07:08:23 +09:00
Agent Control Runner c50136017b P07-01-WX: 仕上がった天候の機能の受入:自然天候・雨・嵐・世界樹と成人客のマナ霧・上部の表示と予報・保存と移行(マナ炉の産出 120 を除く。P07-01 の補助の作業。原単位への加算 0) 2026-10-24 05:04:48 +09:00
52 changed files with 8117 additions and 85 deletions
+388 -7
View File
@@ -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",
@@ -334,6 +336,7 @@ func initialize(clock: SimulationKernel, state: Variant, seed := DEFAULT_WORLD_S
register_command_executor(&"decide_guest", Callable(self, "_execute_decide_guest"))
register_command_executor(&"register_staff", Callable(self, "_execute_register_staff"))
register_command_executor(&"hire_staff_candidate", Callable(self, "_execute_hire_staff_candidate"))
register_command_executor(&"dismiss_staff", Callable(self, "_execute_dismiss_staff"))
register_command_executor(&"create_staff_shift", Callable(self, "_execute_create_staff_shift"))
register_command_executor(&"set_staff_schedule_override", Callable(self, "_execute_set_staff_schedule_override"))
register_command_executor(&"set_staff_work_assignment", Callable(self, "_execute_set_staff_work_assignment"))
@@ -463,6 +466,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 +1296,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 +1340,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):
@@ -2708,6 +2731,7 @@ func _build_operations_daily_financials(
&"AUTHORITY_FINE": &"fines",
&"OPERATING_EXPENSE": &"cost_of_goods",
&"STAFF_SIGNING_COST": &"cost_of_goods",
FinancePolicy.SEVERANCE_CATEGORY: &"cost_of_goods",
&"EVENT_ACTION": &"cost_of_goods",
&"EVENT_COST_COMMITTED": &"cost_of_goods",
&"DEFENSE_SKILL_COST": &"cost_of_goods",
@@ -2951,6 +2975,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 {
@@ -3386,11 +3412,15 @@ func _parse_operations_runtime_snapshot(
var terminal_shape := deferred_runtime_minute == -1 \
and not failure_latched and restored_coordinator.is_terminal() \
and coordinator_current_matches and bool(restored_clock_paused)
# The closed arrears are counted as the live check counts them
# (_operations_closed_arrears_total): wages, upkeep and the unpaid rank tax and interest.
var last_close := restored_coordinator.get_daily_close_minute(restored_coordinator.get_open_financial_day() - 1)
if not bool(restored_coordinator.validate_invariants().get(&"ok", false)) \
or restored_coordinator.get_origin_minute() != OPERATIONS_COORDINATOR_ORIGIN_MINUTE \
or not (normal_shape or failure_shape or pause_shape or terminal_shape) \
or restored_coordinator.get_population_count() != restored_guests.guest_count() \
or restored_coordinator.get_arrears_total() != (_staff_payroll_arrears_total(restored_staff) + restored_upkeep_settlement.get_arrears_through(restored_coordinator.get_daily_close_minute(restored_coordinator.get_open_financial_day() - 1))) \
or restored_coordinator.get_arrears_total() != (_staff_payroll_arrears_total(restored_staff) + restored_upkeep_settlement.get_arrears_through(last_close)
+ _finance_arrears_through(restored_economy, last_close)) \
or not _operations_payroll_crosslinks_are_valid(
restored_coordinator, restored_staff, restored_economy, restored_upkeep_settlement
) \
@@ -3930,6 +3960,7 @@ func _operations_expected_category_totals(
&"CONSTRUCTION_REFUND": &"construction_cash_flow",
&"AUTHORITY_FINE": &"fines", &"OPERATING_EXPENSE": &"cost_of_goods",
&"STAFF_SIGNING_COST": &"cost_of_goods", &"EVENT_ACTION": &"cost_of_goods",
FinancePolicy.SEVERANCE_CATEGORY: &"cost_of_goods",
&"EVENT_COST_COMMITTED": &"cost_of_goods",
&"DEFENSE_SKILL_COST": &"cost_of_goods",
&"MAINTENANCE_ACCRUAL": &"maintenance",
@@ -6901,6 +6932,285 @@ func _rollback_staff_candidate_hire(snapshot: Dictionary) -> bool:
_runtime_event_minute()
)
## P16-01-DISMISS: whether a hired employee can be dismissed now, with the severance
## (A14), the Task the removal reopens and the rescue Tasks the command interrupts.
## Nothing is changed.
func get_staff_dismissal_preview(staff_id: StringName) -> Dictionary:
var staff: StaffRuntime = staff_service.get_staff(staff_id) if staff_service != null else null
var preview := {
&"staff_id": staff_id, &"can_dismiss": false, &"reason": &"NOT_FOUND",
&"severance": 0, &"unpaid_wages": 0, &"reopened_task_ids": [], &"reopened_task_types": [],
&"interrupted_rescue_task_ids": [], &"interrupted_rescue_task_types": [], &"interrupted_rescue_roles": [],
&"available_funds": FinancePolicy.available_funds(economy_service) if economy_service != null else 0,
&"front_cash": economy_service.get_balance(PLAYER_CASH_ACCOUNT) if economy_service != null else 0,
}
if staff == null:
return preview
var severance := FinancePolicy.severance(staff.get_effective_daily_wage())
preview[&"severance"] = severance
preview[&"unpaid_wages"] = staff.unpaid_wages
var work := _staff_dismissal_work(staff)
for kind: StringName in [&"reopened", &"interrupted_rescue"]:
for task: StaffTask in work[kind]:
(preview[StringName(String(kind) + "_task_ids")] as Array).append(task.task_id)
(preview[StringName(String(kind) + "_task_types")] as Array).append(task.task_type)
for task: StaffTask in work[&"interrupted_rescue"]:
(preview[&"interrupted_rescue_roles"] as Array).append(&"carrier" if task.task_id == staff.current_task_id else &"patient")
# R5: after the game ends the command is refused before acceptance (submit_command).
var ended := _operations_enabled and operations_coordinator != null and operations_coordinator.is_terminal()
var reason: StringName = &"BANKRUPT" if ended else _staff_dismissal_block_reason(staff, severance)
preview[&"reason"] = reason
preview[&"can_dismiss"] = reason.is_empty()
return preview
## The work a dismissal changes: the held Task the removal contract reopens, and the
## rescue Tasks the command interrupts (the held transport and every rescue of the
## employee as the patient; SPEC 6.2.1).
func _staff_dismissal_work(staff: StaffRuntime) -> Dictionary:
var reopened: Array[StaffTask] = []
var interrupted: Array[StaffTask] = []
var held := staff_service.get_task(staff.current_task_id)
if held != null and not held.is_terminal():
if held.task_type == &"rescue_transport":
interrupted.append(held)
else:
reopened.append(held)
for task: StaffTask in staff_service.get_tasks():
if task.task_type in [&"rescue_pickup", &"rescue_transport"] and not task.is_terminal() \
and task.target_entity_id == staff.staff_id and not interrupted.has(task):
interrupted.append(task)
return {&"reopened": reopened, &"interrupted_rescue": interrupted}
## Every refusal of SPEC 6.3 the executor decides (R1, R2, R3, R7, R8, R9).
func _staff_dismissal_block_reason(staff: StaffRuntime, severance: int) -> StringName:
if staff.employment_kind == StaffRuntime.EmploymentKind.UNIQUE:
return &"UNIQUE"
if staff.employment_kind != StaffRuntime.EmploymentKind.FREE_HIRE:
return &"NOT_GENERAL_EMPLOYEE"
if staff_service.is_externally_driven_task(staff.current_task_id):
return &"EXTERNAL_TASK"
if not _staff_task_reservations_releasable(staff):
return &"RESERVATION_RELEASE_FAILED"
# Root's temporary boundary R9: an unfinished raid's defender stays until the raid ends.
if not _defense_lock_owner(staff.staff_id, &"DEFENDER").is_empty():
return &"DEFENSE_LOCKED"
if severance > FinancePolicy.available_funds(economy_service):
return &"INSUFFICIENT_FUNDS"
return &""
## R3 decided before any change: the removal contract releases every Reservation of the
## held Task, and InventoryService.release_reservation fails for a missing or a
## consumed Reservation (a released or expired one is a replayed success).
func _staff_task_reservations_releasable(staff: StaffRuntime) -> bool:
var task := staff_service.get_task(staff.current_task_id)
if task == null:
return true
for reservation_id: StringName in task.reservation_ids:
var reservation := inventory_service.get_reservation(reservation_id)
if reservation == null or reservation.state == InventoryReservation.State.CONSUMED:
return false
return true
## Dismissal of a general employee (FREE_HIRE). All refusals are decided before any
## change (SPEC 4.1 c). The employee's seat in a boarded rescue pair, the existing
## removal contract, the severance paid from the front fund with the hidden fund
## covering the rest (one posting, A14) and the cleanup of SPEC 6.2 then commit as one
## operation and are rolled back together on a failure.
func _execute_dismiss_staff(command: SimulationCommand) -> CommandResult:
var raw_staff_id: Variant = command.payload.get(&"staff_id", command.payload.get("staff_id", null))
if typeof(raw_staff_id) not in [TYPE_STRING, TYPE_STRING_NAME] or String(raw_staff_id).is_empty():
return CommandResult.failed(
CommandResult.Code.INVALID_PAYLOAD, command.command_id, "A Staff id is required."
)
var staff_id := StringName(raw_staff_id)
var staff := staff_service.get_staff(staff_id)
if staff == null:
return CommandResult.failed(
CommandResult.Code.NOT_FOUND, command.command_id,
"Staff was not found: %s" % staff_id, {&"reason": &"NOT_FOUND", &"staff_id": staff_id}
)
var severance := FinancePolicy.severance(staff.get_effective_daily_wage())
var reason := _staff_dismissal_block_reason(staff, severance)
if not reason.is_empty():
# R1, R2 and R3 keep the existing removal contract's code and text.
var refusals := {
&"UNIQUE": [CommandResult.Code.TRANSACTION_APPLY_FAILED, "Unique Staff cannot be dismissed, sold or permanently removed."],
&"EXTERNAL_TASK": [CommandResult.Code.TRANSACTION_APPLY_FAILED, "Finish the employee external operation before removal."],
&"RESERVATION_RELEASE_FAILED": [CommandResult.Code.TRANSACTION_APPLY_FAILED, "Staff removal could not release Task Reservations."],
&"NOT_GENERAL_EMPLOYEE": [CommandResult.Code.VALIDATION_FAILED, "Only a general employee can be dismissed."],
&"DEFENSE_LOCKED": [CommandResult.Code.VALIDATION_FAILED, "Staff is defending an unfinished raid."],
&"INSUFFICIENT_FUNDS": [CommandResult.Code.VALIDATION_FAILED, "Front and hidden funds cannot pay the severance."],
}
return CommandResult.failed(refusals[reason][0], command.command_id, refusals[reason][1], {
&"reason": reason, &"staff_id": staff_id, &"severance": severance,
&"available_funds": FinancePolicy.available_funds(economy_service),
})
var minute := _runtime_event_minute()
var before := {
&"economy": economy_service.create_snapshot(),
&"game_state": _state.create_snapshot(),
&"staff": staff_service.create_snapshot(),
&"inventory": inventory_service.create_snapshot(),
&"path": path_service.create_snapshot(),
&"elevators": elevators.create_snapshot(),
&"movement_credits": _movement_credits.duplicate(true),
&"domain_events": domain_events.create_snapshot(),
&"id_generator": id_generator.create_snapshot(),
}
var held_task_id := staff.current_task_id
var reopened: Array = _staff_dismissal_work(staff)[&"reopened"]
if not _release_dismissed_rescue_seat(staff_id):
_rollback_staff_dismissal(before)
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id,
"Staff dismissal could not release the employee's seat in a boarded rescue pair.",
{&"reason": &"RESCUE_SEAT_RELEASE_FAILED", &"staff_id": staff_id, &"error": elevators.last_error}
)
# The existing contract still refuses an unreleasable Task Reservation (R3, decided
# above) as the last guard; nothing it would change survives the rollback.
var removal := staff_service.remove_staff(staff_id, &"DISMISSED", true, minute)
if not removal.is_success():
_rollback_staff_dismissal(before)
removal.command_id = command.command_id
removal.details[&"reason"] = &"RESERVATION_RELEASE_FAILED"
removal.details[&"staff_id"] = staff_id
return removal
var cash := economy_service.get_balance(PLAYER_CASH_ACCOUNT)
var top_up := maxi(severance - cash, 0)
var transaction_id: StringName = &""
if severance > 0:
var entries: Array = []
if top_up > 0:
entries.append(LedgerEntry.new().configure(&"", minute, PLAYER_CASH_ACCOUNT, FinancePolicy.BACK_CASH,
top_up, FinancePolicy.TOP_UP_CATEGORY, staff_id, &"", &"finance.back_fund.top_up"))
entries.append(LedgerEntry.new().configure(&"", minute, FinancePolicy.SEVERANCE_EXPENSE, PLAYER_CASH_ACCOUNT,
severance, FinancePolicy.SEVERANCE_CATEGORY, staff_id, &"", &"staff.severance"))
economy_service.begin_notification_scope()
var paid := economy_service.post(entries, StringName("staff.severance.%d" % command.command_id))
if paid.is_success():
_state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT))
transaction_id = StringName(paid.details.get(&"transaction_id", &""))
economy_service.end_notification_scope()
if not paid.is_success():
_rollback_staff_dismissal(before)
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id,
"Staff dismissal could not commit its severance with the removal.",
{&"reason": &"SEVERANCE_POST_FAILED", &"staff_id": staff_id, &"post": paid.to_dictionary()}
)
var interrupted: Variant = _interrupt_dismissed_staff_rescue(staff_id, held_task_id, minute)
if interrupted == null:
_rollback_staff_dismissal(before)
return CommandResult.failed(
CommandResult.Code.TRANSACTION_APPLY_FAILED, command.command_id,
"Staff dismissal could not interrupt the rescue it leaves.",
{&"reason": &"RESCUE_CLEANUP_FAILED", &"staff_id": staff_id}
)
# SPEC 6.2 / 6.2.1: the facility projection and the Events' missing actors are
# settled at the command boundary by the existing rules, not only at the next minute.
_sync_facility_staffing(minute)
event_service.audit_actor_availability(minute)
var reopened_ids: Array[StringName] = []
for task: StaffTask in reopened:
if not task.is_terminal():
reopened_ids.append(task.task_id)
return CommandResult.succeeded(command.command_id, {
&"staff_id": staff_id,
&"severance": severance,
&"severance_top_up": top_up,
&"severance_transaction_id": transaction_id,
&"unpaid_wages_preserved": int(removal.details.get(&"unpaid_wages_preserved", 0)),
&"removed_shift_ids": removal.details.get(&"removed_shift_ids", []),
&"removed_override_days": removal.details.get(&"removed_override_days", []),
&"reopened_task_ids": reopened_ids,
&"interrupted_rescue_task_ids": interrupted,
&"player_cash": economy_service.get_balance(PLAYER_CASH_ACCOUNT),
&"back_cash": economy_service.get_balance(FinancePolicy.BACK_CASH),
})
## SPEC 6.2.1 and 10: a dismissed member of a rescue pair aboard an elevator leaves the
## cabin at the command, as the non-depicted removal; the other member stays aboard, as
## the existing rule keeps a live passenger to a real landing. These are the steps of
## ElevatorService's own cancellation of a single boarded rider (_cancel_single),
## through the public Path and elevator snapshot interfaces: the Path request ends, and
## the employee's trip, cabin seat and pairing are removed from the snapshot that
## ElevatorService.restore_snapshot then checks against the Path state.
func _release_dismissed_rescue_seat(staff_id: StringName) -> bool:
var actor_id := _staff_path_actor_id(staff_id)
var trip := elevators.get_actor_trip(actor_id)
var partner := StringName(trip.get(&"companion_id", &""))
if trip.get(&"state", &"") != PathService.ELEVATOR_REQUEST_BOARDED or partner.is_empty():
return true
if not path_service.complete_elevator_request(StringName(trip[&"connector_id"]), StringName(trip[&"request_id"])):
return false
var snapshot := elevators.create_snapshot()
var trips: Array = []
for value: Dictionary in snapshot[&"trips"]:
var row := value.duplicate(true)
if StringName(row[&"actor_id"]) == actor_id:
continue
if StringName(row[&"actor_id"]) == partner:
row.erase(&"companion_id")
trips.append(row)
snapshot[&"trips"] = trips
for car: Dictionary in snapshot[&"shafts"]:
(car[&"boarded"] as Array).erase(actor_id)
return elevators.restore_snapshot(snapshot)
func _staff_aboard(staff_id: StringName) -> bool:
return elevators.get_actor_trip(_staff_path_actor_id(staff_id)).get(&"state", &"") == PathService.ELEVATOR_REQUEST_BOARDED
## SPEC 6.2.1: no live rescue Task keeps the dismissed employee as its patient or
## carrier. The next minute's rule (_advance_staff_rescue) is applied to those Tasks
## now: each is interrupted and its elevator requests are cancelled, except that a live
## passenger already aboard rides on to a real landing. A pickup whose carrier was
## dismissed was reopened by the removal and stays open for another medic, as that
## rule keeps it. Returns null if a Task could not be interrupted.
func _interrupt_dismissed_staff_rescue(staff_id: StringName, held_task_id: StringName, minute: int) -> Variant:
var interrupted: Array[StringName] = []
for task: StaffTask in staff_service.get_tasks():
if task.task_type not in [&"rescue_pickup", &"rescue_transport"] or task.is_terminal():
continue
var carried := task.task_id == held_task_id and task.task_type == &"rescue_transport"
if task.target_entity_id != staff_id and not carried:
continue
var patient := staff_service.get_staff(task.target_entity_id)
var carrier := staff_service.get_staff(task.assigned_staff_id)
if not staff_service.cancel_task(task.task_id, &"RESCUE_INTERRUPTED", minute).is_success():
return null
interrupted.append(task.task_id)
for member: StaffRuntime in [patient, carrier]:
if member != null and not _staff_aboard(member.staff_id):
elevators.cancel_actor(_staff_path_actor_id(member.staff_id))
return interrupted
func _rollback_staff_dismissal(snapshot: Dictionary) -> bool:
var restored := economy_service.restore_snapshot(snapshot[&"economy"])
restored = _state.restore_snapshot(snapshot[&"game_state"]) and restored
restored = staff_service.restore_snapshot(snapshot[&"staff"]) and restored
restored = inventory_service.restore_snapshot(snapshot[&"inventory"]) and restored
restored = path_service.restore_snapshot(snapshot[&"path"]) and restored
restored = elevators.restore_snapshot(snapshot[&"elevators"]) and restored
_movement_credits = (snapshot[&"movement_credits"] as Dictionary).duplicate(true)
restored = domain_events.restore_snapshot(snapshot[&"domain_events"]) and restored
restored = id_generator.restore_snapshot(snapshot[&"id_generator"]) and restored
return restored and _validate_runtime_path_crosslinks(
path_service,
guest_service,
staff_service,
_movement_credits,
_runtime_event_minute()
)
func _staff_role_matches_facility(role_id: StringName, definition: FacilityDefinition) -> bool:
if definition.required_roles.is_empty() or role_id in definition.required_roles:
return true
@@ -8784,6 +9094,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 +9346,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))
@@ -9506,6 +9817,14 @@ func _reserve_staff_task_items(task: StaffTask, absolute_minute: int) -> Diction
var before := inventory_service.create_snapshot()
var reservation_ids: Array[StringName] = []
for item_id: StringName in _sorted_name_keys(task.required_items):
# One operation per reservation of the Task: InventoryService never removes a
# Reservation, so the count of the Task's earlier ones for the item makes the next
# reservation after a release (an interruption, a removal or a dismissal) a new
# operation, also within the same minute.
var earlier := 0
for reservation: InventoryReservation in inventory_service.get_reservations():
if reservation.requester_id == task.task_id and reservation.item_id == item_id:
earlier += 1
var result := inventory_service.reserve_item(
inventory_id,
item_id,
@@ -9514,7 +9833,7 @@ func _reserve_staff_task_items(task: StaffTask, absolute_minute: int) -> Diction
&"staff_task",
absolute_minute,
absolute_minute + 480,
StringName("staff.task.reserve.%s.%s" % [task.task_id, item_id])
StringName("staff.task.reserve.%s.%s.%d" % [task.task_id, item_id, earlier])
)
if not bool(result.get(&"ok", false)):
inventory_service.restore_snapshot(before)
@@ -10993,13 +11312,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 +11352,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(
@@ -11288,6 +11610,7 @@ func get_staff_management_report() -> Dictionary:
&"rest_status": get_staff_rest_report(staff.staff_id).get(&"label", ""),
&"can_edit": block_reason.is_empty(),
&"assignment_block_reason": block_reason,
&"dismissal": get_staff_dismissal_preview(staff.staff_id),
&"unique_contract": _staff_unique_contract_report(staff),
&"original_relationship": relationship_service.get_original_relationship(staff.staff_id),
})
@@ -12392,6 +12715,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 +13897,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.
@@ -1,3 +1,28 @@
## 現在の対象(2026-10-25・正式24成果/機能実装を優先)
元30パッケージ・88成果単位で、部分点はありません。正式完了は24成果、完了パッケージは4(P00、P04、P06、P14)です。
**2026-10-20 の後に正式完了した成果**
- P28-01「現HEADの性能基準」(`413c8e3`)。性能の不足は保持しています。
- P05-02「全費目と資金区分」(`48d3b00`)。P05-03 と P05 の残件は保持しています。
**原本への加算 0 の補助の作業として正式完了したもの**(原単位は閉じません)
- P28-02-DIAG(実測上位の特定)と P28-02-EQ(同等性の先行固定)。
- P07-01-WX(天候、Q `9b5b09b`)。原 P07-01 は、霧の境界のマナ炉の産出 120 と `AR.weather.fog_magic` が残り、in_progress です。
- P16-01-DISMISS(一般従業員の通常操作での解雇と退職金、Q `4ca3bbe`)。原 P16-01 は、求人募集・市場と借金での獲得・雇用区分ごとの費用が残り、in_progress です。受入記録は `execution/p16-01-dismiss/DISMISS_ACCEPTANCE_4CA3BBE.md` です。
**優先の順**(ユーザーの指示、2026-10-23)
- まずゲームとしての機能実装の完了を優先します。統合試験の動作速度の調整(P28)と見た目の調整は後に回します。
- 承認済みの決定: A13(天候の初期仕様)、A14(退職金の初期仕様)。再質問しません。
- 回答待ち: P05-03 の3件(推定しません)。
- P24 は上流の条件が揃うまで保留です。P28-02-OPT の記録は保持し、後段にします。
**次の対象**: 機能の残件の次の候補を、同じ実 Opus が、原条文・上流の条件・既存の証拠・通常の登録案とともに Root へ渡し、Root が通常の登録をします。新しい実装は登録の後です。
**解消扱いしない未達**: 同じ Windows の実機・IME・OS の倍率・同じ候補での実機の受入、長い通常のプレイ、全ゲームの最終の受入。
以下の「現在」は各記録時点の履歴です。上記と現在の台帳を優先してください。
## 現在の対象(2026-10-20・正式22成果/P14-03正式完了、P14パッケージ完了)
元30パッケージ・88成果単位で、部分点はありません。正式完了は22成果、完了パッケージは4(P00、P04、P06、P14)です。
@@ -3958,6 +3958,72 @@
"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"
}
},
{
"test": "tests/staff/test_general_dismissal.gd",
"template": "PASS: <checks> checks",
"required": true,
"fixed_check_count_used": false,
"source_sha256_at_schema_audit": "f1f77bfd6320a3c8e0851915b150e95fc40f4a352876358393572a6d3f7c79ce",
"authority": {
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/SPEC.md",
"scope": "new P16-01-DISMISS suite marker; native execution pending"
}
},
{
"test": "tests/staff/test_general_dismissal_restart_1_write.gd",
"template": "PASS: <checks> checks",
"required": true,
"fixed_check_count_used": false,
"source_sha256_at_schema_audit": "b38745eccb4e51ddf4790051795e2f64faf10f875edc7b754ac2d04db2917a5b",
"authority": {
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/SPEC.md",
"scope": "new P16-01-DISMISS F8 restart writer marker; native execution pending"
}
},
{
"test": "tests/staff/test_general_dismissal_restart_2_read.gd",
"template": "PASS: <checks> checks",
"required": true,
"fixed_check_count_used": false,
"source_sha256_at_schema_audit": "4e13de2594096fab648fc8c418a1a4aa764ac72fc13fbd68cba1c6f05767fc83",
"authority": {
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/SPEC.md",
"scope": "new P16-01-DISMISS F8 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。
@@ -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)"
}
}
@@ -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`)。
@@ -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 の実機の受入、長い通常のプレイ、全ゲームの最終の受入は残っている。
@@ -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
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,120 @@
# P16-01-DISMISS 正式受入記録(補助の作業。原本への加算 0)
- P16-01-DISMISS は、原 P16-01 の補助の作業である。対象は、一般従業員(ギルドで雇った `FREE_HIRE`)の通常の操作での解雇で、退職金を含む。
- 次の3つをそろえて、正式に完了した。
- 受入 10 件(F1〜F10)についての、別の fresh な Desktop Opus 5.5 の独立レビューの PASS(高・中 0、low 5)
- 通常の validation・review・integration・final の全 PASS
- 実 DB と traceability の結合
- S `f5fe219` → Q `4ca3bbe`。
- 原本への加算は 0 で、正式の数は 24/88・4/30 のまま。部分点なし。
- 原 P16-01・P16-03 は未達のまま。`AR.staff.dismissal_severance` の package P05 の原 work・原 AC も、この作業では閉じない。
| 名前 | commit | 備考 |
|---|---|---|
| 提出 S | `f5fe2196740466a673879cb2950d96446fd99d6e` | 修正 2 |
| 統合・最終検証 Q | `4ca3bbef553259e111b35856785ae83d34380287` | Q の tree は S の tree と同じ(`2cebcc3e…`) |
| 基準 M | `0b59d7c23c301cf014343e217827cb2d837cc11c` | |
- 正式証拠: `DISMISS_FORMAL_COMPLETION_4CA3BBE.json`
- 独立レビューの結果(実結果の bytes の写し): `DISMISS_INDEPENDENT_REVIEW_F5FE219.json`(SHA256 `1f530e6c4e6158a469271d287763fc8a573295f44f04dd26bf9b5e6a93f8b737`)
## 位置づけと原本の条件
- 仕様は同じフォルダの `SPEC.md`(登録版 r04、ファイルの SHA256 `b4cc31c0…5f77`)。
- 人の決定 A14(退職金の初期仕様、答え「この初期仕様で進める」)は、`PROVENANCE.json` のとおり。
- 退職金は、その職員の日給の3日分。
- 表金から払い、不足分を裏金で補う。合計でも足りなければ解雇しない。
- 未払いの給与は退職金と分けて保持し、次の 00:00 に精算する。
- 退職金は帳簿で区別し、日次決算では運営費に入れる。
- R9(進行中の襲撃の防衛者は解雇しない、`DEFENSE_LOCKED`)は、Root が既存の範囲の権限で採った一時の境界で、人の承認ではない。
- 原単位の文:
- P16-01 の acceptance: 求人から配置・精算・退職まで通常操作でき、ユニーク禁止操作を維持する。
- P16-03 の work: 勤務、休息、給与、気絶、救護、退職時参照除去を横断検証する。
- 原子要件: `AR.staff.general_dismissal`(P16)、`AR.staff.dismissal_severance`(P05、D04)、`AR.unique.no_dismissal`(P17)。
- 原の横断条件(P16、6 項目):
- 原本条件とUI操作が一致
- Runtimeの実利用・会計・行動へ接続
- 途中保存・再起動で再現
- 異常/境界/中断を検証
- 関連回帰PASS
- 要件別ログとソースハッシュを保存
- 受入 10 件(F1〜F10)は `SPEC.md` §7 と、登録の `acceptance_baseline.json` の本文のとおり。
## 4つの通常のゲート(Runner の the-tree-p16-01-dismiss、task P16-01-DISMISS、completed、修正 2)
| ゲート | commit | Runner の証跡 | 結果 |
|---|---|---|---|
| validation | S | evidence 6 | `p16-dismiss` exit 0、verified true、12 本(14 段)・1,888 checks・parse 354、engines_started 0 |
| review | S | evidence 7 | 実結果 `1f530e6c…`、pass、F1〜F10、low 5 |
| integration | Q | evidence 8 | `p16-dismiss` exit 0、verified true、engines_started 0 |
| final | Q | evidence 9 | `32d1d379…-final.json` SHA `91b6d220…`、verified true、12 本・1,888 checks・parse 354、engines_started 0 |
- owner の承認:
- Root が、正確な Q `4ca3bbe` の資格を確かめた(`ROOT_P16_Q4CA3BBE_QUALIFICATION_20261025_094225_R01.json`)。
- main の統合と push についての既存の人の許可を、正確な Q と main_base `0b59d7c` に当てはめた。新しい人の発言は作っていない。
- 記録は、Runner の approvals の行と `ROOT_OWNER_APPROVAL_4CA3BBE_R01.json`、統合の開始と終わりの記録、`ROOT_P16_DISMISS_COMPLETED_DB_JOIN_4CA3BBE_R01.json`。
- 測定は全て S で行った。
- native: 12 本・1,888 checks(新しい suite 721、再開の書き手 120・読み手 273 を含む)
- S の全 roster: 201 本・24,073 checks・失敗 0。既存の代表負荷の性能の suite 10 本を含む。
- Q での integration と final について:
- S で測った証拠を、中身が同じ Q の新しい checkout で、中身ベースの verifier が照合したものである。
- Q で測ったとは書かない。
## 何ができるようになったか
- 通常の命令 `dismiss_staff` と、Main の職員の管理の画面の「解雇する…」「解雇を確定」で、一般従業員を解雇できる。
- 押す前に、退職金・裏金からの補填・未払いの給与・開き直す作業・命令の中で取り消す救護を出す。結果も出す。
- 拒否の行(ユニーク・成人契約・物流の途中・予約を解けない・運営の終了・資金不足・襲撃の防衛中)では、ボタンが無効で理由が出る。
- 解雇の命令の境目で、勤務・配置・経路・昇降機・救護・出来事・施設の人員の参照を片づける。乗ったままの救護の組でも解雇でき、残る人は本物の階まで乗る。
- 開き直した予約つきの作業を、別の職員が取り直して終えられる(在庫の予約の操作 ID を直した)。
- 締めの後に税・利息の未払いが残った保存が読めなかった P05-02 からの不具合を直した。
## 独立レビュー(S f5fe219)
- レビュー担当: 別の fresh な Claude Desktop Opus 5.5 の session(`local_4e746265`)。実装担当とも、前の S の2人のレビュー担当とも別である。
- 凍結の入力の SHA を確かめ、読むだけで判定した。凍結の入力は packet・schema・材料 7,292 ファイル。
- 判定の範囲:
- 受入 F1〜F10 の全部
- SPEC §4・§5・§6 の全ての行
- 原の横断条件と、U1(未達の開示)・U2(証拠の適用)
- 依頼文の項目 1〜11。F9 の通常の入力(viewport の入力の経路と、時計が動いている時の押し方を分けて判定)、§4.1 (d) の4つの巻き戻しの枝、画面の試験の実際の範囲、来歴の欠落を含む。
- 配送の前に、会話の記録の全体を確かめた。
- tool の呼び出しは Bash 74・Read 5・Write 1・Edit 1 など。
- 書き込みは、その session の scratchpad と TMPDIR の補助と、結果のファイル(作成1回)だけ。
- engine・試験・Runner のコマンド・git の書き換えは無い。
- low 5 件。原文は独立レビューの結果のファイルにある。どれも受入を止めないと判定された。
1. 命令の中で取り消す救護の、患者の側の結果の文を、画面で確かめていない。
2. 画面の中の守り(確認を開いた後に拒否に変わった時・古い確認の時に送らない)の分岐を、試験で通していない。
3. 出来事の欠けた actor の方針のうち REPLACE_WITH_ROLE と BRANCH_FAILURE を、解雇の境目で試験していない。
4. `_rollback_staff_dismissal` の戻り値を使っていない(既存の雇用の巻き戻しと同じ形)。
5. 来歴の欠落(下の「保持した記録の欠落」)は受入の判断に影響しないが、証拠の保持の欠落としては残る。
## 修正の経過と保持した失敗
1. S `b06fec1`
- native・validation・全 roster(201 本・23,659 checks)は合格した。
- 1回目のレビューは pass。同じレビュー担当の補足(改訂)の判定は changes_requested(high 1・medium 6・low 4)。
- 乗ったままの救護の組の解雇を、実装の判断で拒んでいた(SPEC §6.2 の「直し、拒否に変えない」に合わない)。開き直した予約つきの作業を別の職員が取れなかった。ほかに、説明の文と確かめ方の代用。
- 修正 1 で、組を解く後始末、予約の操作 ID、説明の分け方、条文どおりの試験を入れた。
2. S `a073ccb`
- native・validation・全 roster(201 本・24,017 checks)は合格した。
- レビューは changes_requested(medium 1・low 3)。画面の試験がボタンの信号を直接出していて、F9 の通常の入力を確かめていなかった。
- 修正 2 で、画面の試験を viewport の入力で操作する形にし、巻き戻しの枝と画面の文の試験を足した(製品のファイルは変えていない)。
- 正式の試験の生の出力と、前のレビューの結果は保持した。新しい S の受入に、前の S の証拠を流用していない。
- 後続のスクリプトを作る1回目の実行と、診断用の写しなどの元の bytes の欠落は、次の節に明示する。
## 保持した記録の欠落(承認でも免除でもない)
- S `a073ccb` の後続のスクリプトを作る1回目の実行の失敗。1回目の2つの出力、stdout、1回目の script の bytes は残っていない。失敗の全文は会話から書き写した(`DERIVE_A073CCB_SCRIPTS_FIRST_RUN_FAILURE.md`)。
- 修正 2 の画面の場面の診断用の写し2つ(stage の上だけ)は、ファイルとして残っていない。log・生成の命令の写し・元にした source の hash は残っている(`P16_01_DISMISS_REPAIR2_DIAG_COPIES_PROVENANCE_R01.json`)。
- Root はこれらを証拠の保持の欠落として保持している。新しい S の正式の試験の生の出力が失われたという意味ではない。
## 未達として保つもの(主張しない)
- 原 P16-01 は未達のまま。求人募集の通常の操作(`AR.staff.recruitment`)、奴隷市場・破産客からの獲得(D04)、雇用の区分ごとの費用の整理が残る。
- 原 P16-03 は未達のまま。勤務・休息・気絶・救護の横断(D04)と、解雇の経路の外の退職時の参照の除去が残る。
- 人身売買(`AR.staff.sale_*`、D04)は扱っていない。
- `AR.staff.dismissal_severance` は A14 の決定とこの受入で接続したが、package P05 の原単位の完了には数えない。
- 進行中の襲撃の防衛者の解雇(R9)は、原本の要件として未達のまま。退却や戦闘の規則は作っていない。
- 試験の入力は viewport の入力で、OS の入力ではない。同じ Windows の実機・IME・OS の倍率・同じ候補での実機の受入(SPEC §10)は残る。UI は今の部品のままで、完成の見た目ではない。
- 長い通常のプレイと、全ゲームの最終の受入は残っている。
@@ -0,0 +1,875 @@
{
"at_jst": "2026-10-25T18:49:38.598450+09:00",
"auxiliary_unit": "P16-01-DISMISS",
"original_units": [
"P16-01",
"P16-03"
],
"atomic_requirements": [
"AR.staff.general_dismissal (P16)",
"AR.staff.dismissal_severance (P05, D04; severance decided by A14)",
"AR.unique.no_dismissal (P17)"
],
"project": "the-tree-p16-01-dismiss",
"task": "P16-01-DISMISS",
"state": "completed",
"S": "f5fe2196740466a673879cb2950d96446fd99d6e",
"Q": "4ca3bbef553259e111b35856785ae83d34380287",
"M": "0b59d7c23c301cf014343e217827cb2d837cc11c",
"repairs": 2,
"Q_tree_equals_S_tree": "2cebcc3e851387c26bb7066d37a60d7ae6e51703",
"all4_normal_gates_passed": true,
"scope": "Auxiliary unit of the original P16-01: dismissal of a general employee (FREE_HIRE) by the ordinary dismiss_staff command and the Main staff panel, with the A14 severance (three days of the effective daily wage, the front fund first and the hidden fund covering the rest), the held unpaid wages, the refusals R1-R9 (R9 DEFENSE_LOCKED is a Root temporary boundary, not a human approval) and the cleanup at the command boundary. Not a new human gameplay approval and not a deletion of the original P16-01 or P16-03 acceptance.",
"original_work": {
"P16-01": "求人、市場、原本の雇用区分ごとの費用と獲得・退職を非描写状態として整理する。",
"P16-03": "勤務、休息、給与、気絶、救護、退職時参照除去を横断検証する。"
},
"original_acceptance": {
"P16-01": "求人から配置・精算・退職まで通常操作でき、ユニーク禁止操作を維持する。",
"P16-03": "人員上限、未払い、交代中の予約・移動が整合し、消失や二重精算がない。"
},
"original_package_work": "雇用区分別費用、全職種業務、生成時特性、獲得/退職/処分の非描写状態処理を整理。乱数特性は生成時に固定しセーブへ保存。",
"original_package_acceptance": "求人から配置・業務・休息・精算・退職まで通常UIで通る。費用/忠誠/特性効果の変化が可視化され、ロードで特性が再抽選されない。",
"original_cross_cutting_acceptance": [
"原本条件とUI操作が一致",
"Runtimeの実利用・会計・行動へ接続",
"途中保存・再起動で再現",
"異常/境界/中断を検証",
"関連回帰PASS",
"要件別ログとソースハッシュを保存"
],
"acceptance_ids": [
{
"id": "P16-01-DISMISS-F1",
"text": "通常の命令: §6.1 の許可の行の `FREE_HIRE` の職員を §4.1 の command で解雇でき、名簿から消え、`STAFF_REMOVED`(reason `DISMISSED`)が1つ記録される。"
},
{
"id": "P16-01-DISMISS-F2",
"text": "区分の拒否: `UNIQUE` と `ADULT_CONTRACT` は命令でも画面でも解雇できず、§5 の比べ方で業務の状態が変わらない。"
},
{
"id": "P16-01-DISMISS-F3",
"text": "退職金: §3.5 で決めた額と払う元で、解雇ごとに1回だけ払われ、日次決算と帳簿の照合が一致する。§3.5 で決めた支払ができない時は解雇されず、§5.c の比べ方で業務の状態が変わらない。"
},
{
"id": "P16-01-DISMISS-F4",
"text": "未払いの給与: 解雇の前の未払いが退職金とは別に保持され、次の 00:00 の締めで1回だけ払われ(A09 の補填を含む)、解雇の後に日給が生じない。"
},
{
"id": "P16-01-DISMISS-F5",
"text": "再送: 同じ key の再送が `DUPLICATE_IDEMPOTENCY_KEY` で、details に最初の結果がつき、§5.b の比べ方で snapshot の全体が同じ。退職金を二度払わない。"
},
{
"id": "P16-01-DISMISS-F6",
"text": "拒否の境界: §6.3 の拒否の全ての行(R1〜R9)で拒み、§5 の該当の比べ方で業務の状態が変わらない。"
},
{
"id": "P16-01-DISMISS-F7",
"text": "許可の境界と後始末: §6.1 の許可の全ての行(A1〜A10)で解雇でき、§6.2 の後始末が全て成り立つ。開き直した作業、サービスの待ちと失敗、救護のやり直し、出来事の欠けた職員の扱いは、それぞれ既存の規則どおりになる。A6・A7・A8 は §6.2.1 のとおり、命令の直後に保存して同じ process と別の process で読み込んでも参照が残らず、保存の照合が通る。"
},
{
"id": "P16-01-DISMISS-F8",
"text": "保存と再開: 解雇の前後(命令の直後、次の進行の前を含む)・締めの前後の保存を同じ process と別の process で読み、同じ状態で、退職金と未払いの給与の支払が二重にならない。"
},
{
"id": "P16-01-DISMISS-F9",
"text": "通常の画面: ゲームの本物の Runtime と Main の職員の管理の画面で、通常の入力で許可の行の一般従業員を解雇でき(退職金の額と開き直る作業を押す前に見られる)、拒否の行では操作が出ないか無効で理由が出る。fixture の直接の呼び出し(`staff_service.remove_staff`)だけで通常の入口の完了を主張しない。"
},
{
"id": "P16-01-DISMISS-F10",
"text": "回帰: 関連の回帰(狭い roster と再開の2段)と最終の S の全 roster が PASS。既存の試験の期待値の変更は `EXPECTATION_CHANGES.json` に変更前後と根拠を一覧にする。"
}
],
"acceptance_baseline": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/acceptance_baseline.json",
"bytes": 3915,
"sha256": "2c40932e96f6ca20e46aac8bd3b65cac51ae466fef6b0b62d7bb170e3a924258"
},
"spec": {
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/SPEC.md",
"bytes": 24414,
"sha256": "b4cc31c0b1cbfb99f0adcbb356cd919bf45c147a150bc7efa81bd982cab05f77",
"contract_spec_sha256": "14efffd90d7fc0ae466375030d0f1680a1319e1bbe1ec0f373c5a5d97ea59e6b"
},
"provenance_A14": {
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/PROVENANCE.json",
"bytes": 3061,
"sha256": "c023142d76241d3c5b78b6d53c98b0fe2e1c8c72ee03170c7406c98a144f6223"
},
"unit_disclosures": [
{
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/IMPLEMENTATION_NOTES.md",
"bytes": 25864,
"sha256": "4b7971deb708a424eb4ba8c70af6a479c66c6b7674e9c8d45b7882271fa7b7f4"
},
{
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/EXPECTATION_CHANGES.json",
"bytes": 2062,
"sha256": "99a3a39eff6307e0edda9a12411af6e3ad116b4b26a61bcfcd25428af687d9fc"
}
],
"runner_task": {
"id": "P16-01-DISMISS",
"state": "completed",
"submitted": "f5fe2196740466a673879cb2950d96446fd99d6e",
"candidate": "4ca3bbef553259e111b35856785ae83d34380287",
"main_base": "0b59d7c23c301cf014343e217827cb2d837cc11c",
"reason": null,
"repairs": 2
},
"runner_events": [
{
"id": 1,
"event": "registered",
"created": 1792807652.738952
},
{
"id": 2,
"event": "owner_delegated_implementer",
"created": 1792807668.3897338
},
{
"id": 3,
"event": "commander_proposal",
"created": 1792807823.042252
},
{
"id": 4,
"event": "submitted",
"created": 1792813771.980511
},
{
"id": 5,
"event": "validating",
"created": 1792814391.051862
},
{
"id": 6,
"event": "review_pending",
"created": 1792814399.380697
},
{
"id": 7,
"event": "owner_prepared_actual_desktop_independent_review_input",
"created": 1792818189.395961
},
{
"id": 8,
"event": "review_pending",
"created": 1792902931.012789
},
{
"id": 9,
"event": "owner_actual_independent_Desktop_review_output",
"created": 1792902941.714538
},
{
"id": 10,
"event": "reviewed",
"created": 1792902941.715863
},
{
"id": 11,
"event": "review_pending",
"created": 1792904036.379626
},
{
"id": 12,
"event": "owner_actual_independent_Desktop_review_supplement_output",
"created": 1792904045.946167
},
{
"id": 13,
"event": "repair_pending",
"created": 1792904045.9476
},
{
"id": 14,
"event": "commander_proposal",
"created": 1792904253.122462
},
{
"id": 15,
"event": "repair_ready",
"created": 1792904253.123292
},
{
"id": 16,
"event": "submitted",
"created": 1792907145.647569
},
{
"id": 17,
"event": "validating",
"created": 1792907933.879277
},
{
"id": 18,
"event": "review_pending",
"created": 1792907940.6854641
},
{
"id": 19,
"event": "owner_prepared_actual_desktop_independent_review_input",
"created": 1792911399.9793851
},
{
"id": 20,
"event": "review_pending",
"created": 1792913267.315629
},
{
"id": 21,
"event": "owner_actual_independent_Desktop_review_output",
"created": 1792913276.881197
},
{
"id": 22,
"event": "repair_pending",
"created": 1792913276.8830101
},
{
"id": 23,
"event": "commander_proposal",
"created": 1792913430.5026371
},
{
"id": 24,
"event": "repair_ready",
"created": 1792913430.504103
},
{
"id": 25,
"event": "submitted",
"created": 1792915285.8875031
},
{
"id": 26,
"event": "validating",
"created": 1792916082.429237
},
{
"id": 27,
"event": "review_pending",
"created": 1792916089.916318
},
{
"id": 28,
"event": "owner_prepared_actual_desktop_independent_review_input",
"created": 1792919753.202212
},
{
"id": 29,
"event": "review_pending",
"created": 1792921050.491998
},
{
"id": 30,
"event": "owner_actual_independent_Desktop_review_output",
"created": 1792921061.577804
},
{
"id": 31,
"event": "reviewed",
"created": 1792921061.580501
},
{
"id": 32,
"event": "approval_pending",
"created": 1792921121.711384
},
{
"id": 33,
"event": "approved",
"created": 1792921381.492307
},
{
"id": 34,
"event": "completed",
"created": 1792921438.661454
}
],
"normal_evidence_join": [
{
"id": 6,
"task_id": "P16-01-DISMISS",
"kind": "validation",
"commit_sha": "f5fe2196740466a673879cb2950d96446fd99d6e",
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/acb324fc062c44c9a075b19991fb8e9e-validation.json",
"passed": 1,
"artifact_sha256": "f29b4b738fc2092430898a8deadcef86acb1ab3ac114482849fc30935df6e2cc",
"checks": [
{
"id": "p16-dismiss",
"exit_code": 0,
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/91313315f7fb46528b55818e300e26f7",
"cwd": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p16-01-dismiss/P16-01-DISMISS-validation-af80c5bc",
"stdout": {
"verified": true,
"actual_suites": 12,
"parse_checks": 354,
"actual_checks": 1888,
"engines_started": 0,
"formal_acceptance": false,
"original_unit_complete": false
},
"raw": {
"command.json": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/91313315f7fb46528b55818e300e26f7/command.json",
"bytes": 739,
"sha256": "3f81eca34d40772edaf0bc39112a01f42343e5d30ea253e74183445e144249d1"
},
"stdout.log": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/91313315f7fb46528b55818e300e26f7/stdout.log",
"bytes": 167,
"sha256": "0c677da2b25fa2e6601b2e6ac6fcfe2854c6790d84354f053ef332ff7ef16b2e"
},
"stderr.log": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/91313315f7fb46528b55818e300e26f7/stderr.log",
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"sandbox.sb": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/91313315f7fb46528b55818e300e26f7/sandbox.sb",
"bytes": 1984,
"sha256": "063e31b34a4160f36a2b17bbf0a003a6b92df70e8251071715511f02bcff2048"
}
}
}
]
},
{
"id": 7,
"task_id": "P16-01-DISMISS",
"kind": "review",
"commit_sha": "f5fe2196740466a673879cb2950d96446fd99d6e",
"artifact": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-f5fe219-r01/actual-independent-desktop-output/result.json",
"passed": 1,
"artifact_sha256": "1f530e6c4e6158a469271d287763fc8a573295f44f04dd26bf9b5e6a93f8b737",
"checks": []
},
{
"id": 8,
"task_id": "P16-01-DISMISS",
"kind": "integration",
"commit_sha": "4ca3bbef553259e111b35856785ae83d34380287",
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/15ec10bebdd243c1a4e021de0304c7e5-integration.json",
"passed": 1,
"artifact_sha256": "d9f6fe28a1b040a39da6e3c12d0109a1924155e2aece46037c31aeb3a69a185a",
"checks": [
{
"id": "p16-dismiss",
"exit_code": 0,
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/307bc8e26b0245aea3bc5b3a78826986",
"cwd": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p16-01-dismiss/P16-01-DISMISS-integration-18921040",
"stdout": {
"verified": true,
"actual_suites": 12,
"parse_checks": 354,
"actual_checks": 1888,
"engines_started": 0,
"formal_acceptance": false,
"original_unit_complete": false
},
"raw": {
"command.json": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/307bc8e26b0245aea3bc5b3a78826986/command.json",
"bytes": 740,
"sha256": "2e73d96350daeabd34569cb9f6952d02fffeadd042523f0cc6b667e3a836fbcb"
},
"stdout.log": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/307bc8e26b0245aea3bc5b3a78826986/stdout.log",
"bytes": 167,
"sha256": "0c677da2b25fa2e6601b2e6ac6fcfe2854c6790d84354f053ef332ff7ef16b2e"
},
"stderr.log": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/307bc8e26b0245aea3bc5b3a78826986/stderr.log",
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"sandbox.sb": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/307bc8e26b0245aea3bc5b3a78826986/sandbox.sb",
"bytes": 1985,
"sha256": "56cc0a16a025f5bf9be5c69d93de17994015146be92ebc07e83f03f242f33f68"
}
}
}
]
},
{
"id": 9,
"task_id": "P16-01-DISMISS",
"kind": "final",
"commit_sha": "4ca3bbef553259e111b35856785ae83d34380287",
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/32d1d3797e35400aa84ce8d55df03d22-final.json",
"passed": 1,
"artifact_sha256": "91b6d2202dd62fec3bb11683d344c653450447746f686e903e99bd583d587561",
"checks": [
{
"id": "p16-dismiss",
"exit_code": 0,
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/92845d661406435eb46a31979a409095",
"cwd": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p16-01-dismiss/P16-01-DISMISS-final-d27fcc32",
"stdout": {
"verified": true,
"actual_suites": 12,
"parse_checks": 354,
"actual_checks": 1888,
"engines_started": 0,
"formal_acceptance": false,
"original_unit_complete": false
},
"raw": {
"command.json": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/92845d661406435eb46a31979a409095/command.json",
"bytes": 772,
"sha256": "adc2dc85d22491c5714a23dcab56acc5eec8546c16ced5957d154310e8d2d5e5"
},
"stdout.log": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/92845d661406435eb46a31979a409095/stdout.log",
"bytes": 167,
"sha256": "0c677da2b25fa2e6601b2e6ac6fcfe2854c6790d84354f053ef332ff7ef16b2e"
},
"stderr.log": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/92845d661406435eb46a31979a409095/stderr.log",
"bytes": 0,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"sandbox.sb": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/92845d661406435eb46a31979a409095/sandbox.sb",
"bytes": 1979,
"sha256": "dc417131d1b72066af7113145ae3aefa69951546a82d9614dab35fd8e77ee618"
}
}
}
]
}
],
"earlier_evidence_retained": [
{
"id": 1,
"kind": "validation",
"commit_sha": "b06fec17c7c1e2e4cc22116f209c51dec751215c",
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/adab90a677f04ee0b0430517aa1bdb09-validation.json",
"passed": 1
},
{
"id": 2,
"kind": "review",
"commit_sha": "b06fec17c7c1e2e4cc22116f209c51dec751215c",
"artifact": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-b06fec1-r01/actual-independent-desktop-output/result.json",
"passed": 1
},
{
"id": 3,
"kind": "review",
"commit_sha": "b06fec17c7c1e2e4cc22116f209c51dec751215c",
"artifact": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-b06fec1-r01/actual-independent-desktop-output-r02/result.json",
"passed": 0
},
{
"id": 4,
"kind": "validation",
"commit_sha": "a073ccb59b9e20e91875008b5d34431ed5a1174f",
"artifact": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/runs/4fb740b267d84783bf2d035b389103cc-validation.json",
"passed": 1
},
{
"id": 5,
"kind": "review",
"commit_sha": "a073ccb59b9e20e91875008b5d34431ed5a1174f",
"artifact": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-a073ccb-r01/actual-independent-desktop-output/result.json",
"passed": 0
}
],
"earlier_evidence_note": "evidence 2 is the first pass on S b06fec1 by local_e0348c69, superseded by the same reviewer's revised changes_requested (evidence 3); evidence 5 is the changes_requested on S a073ccb by local_cad69b68",
"approval": {
"task_id": "P16-01-DISMISS",
"candidate": "4ca3bbef553259e111b35856785ae83d34380287",
"main_base": "0b59d7c23c301cf014343e217827cb2d837cc11c",
"owner": "Root/Codex applying existing human authorization for exact-candidate main integration and push; no new human utterance",
"created": 1792921381.4913468
},
"owner_records": [
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/ROOT_P16_Q4CA3BBE_QUALIFICATION_20261025_094225_R01.json",
"bytes": 22154,
"sha256": "c6c5af0c64725279bfbba3959d0ee8e737eee923c6b7c2f936a41881d663370d"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/ROOT_OWNER_APPROVAL_4CA3BBE_R01.json",
"bytes": 3748,
"sha256": "ecd7303a9dc523d5ac04046ef78fe0f543c0030c641db9c3bc5333bf871e4f06"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/ROOT_INTEGRATE_4CA3BBE_R01.start.json",
"bytes": 830,
"sha256": "35aff6f3c5deaa85c265995d4d6963d336ddbc33c07c24993e350a76922e5120"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/ROOT_INTEGRATE_4CA3BBE_R01.ended.json",
"bytes": 424,
"sha256": "dab0fa02d8426c7c081209405153c160b5d28a5d6a8c64f40db06fb076bffbad"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/ROOT_P16_DISMISS_COMPLETED_DB_JOIN_4CA3BBE_R01.json",
"bytes": 18156,
"sha256": "c1eaa159e1c3dd7037044f6b9979a1daa6857a095ad3fcdf090a21894b194438"
}
],
"traceability": {
"path": "/Users/korizatou114/Library/Application Support/agent-control/the-tree-p16-01-dismiss/traceability/P16-01-DISMISS.json",
"bytes": 5201,
"sha256": "836f2bb099ab0cea0ae0b75d4f0cee981988395c58a541b56e430ea84254206c"
},
"independent_reviewer": {
"task_id": "P16-01-DISMISS",
"commit": "f5fe2196740466a673879cb2950d96446fd99d6e",
"session_id": "local_4e746265-c2a6-4576-a894-84cbb0b7fc46",
"model": "claude-opus-5-5",
"context": "fresh",
"read_only": true,
"read_only_meaning": "The governor read every tool call of the actual reviewer transcript (762 rows, 149 assistant messages, all model claude-opus-5-5, 2026-10-25T09:18:31Z to 09:35:20Z): tools used were Bash 74, Read 5, Write 1, Edit 1, mcp__ccd_turn__update_plan 2 and mcp__ccd_turn__wrap_up 1; no Agent or Artifact call. Read: the frozen checkout's tests/helpers/p16_dismissal_fixture.gd and tests/staff/test_general_dismissal.gd only. Write and Edit: only the reviewer's own scratchpad helper write_result.py (/private/tmp/claude-501/-Users-korizatou114-Projects-------------/2edbe800-dc0f-4fbc-8e2f-54790a2406a5/scratchpad); the Edit changed one line reference in it. Every Bash command was read in full. Bash writes: only the reviewer's scratchpad (packet_data.json, spec_packet.md, diff.patch, gitdiff.patch) and its own TMPDIR (lsfiles.txt), and DESKTOP_RESULT.json created once by write_result.py (the listing just before shows no result file); rows without a timestamp are api-request-blob copies of the same tool calls, not further runs. git use: diff 12, log 1, status 1, rev-parse 1, ls-files 1 (no commit, checkout, reset, merge or worktree). No test, Godot, agent-control, tools/run_*.py or run_all_tests run. Two tool results were exit 1 of read-only cat/head commands. After the review the governor checked: frozen checkout HEAD f5fe219 with only ?? .review-material/, all 7,292 material files equal to the manifest SHA-256, packet/normal packet/schema/manifest equal to READY, no active run, task state review_pending. The empty folder .claude/.cc-writes in the review folder appeared at 18:18:44 JST, right after the reviewer's first cd into that folder (09:18:43Z); it is the harness's working-directory folder, not a file written by a tool. Schema checked by the reviewer by hand (transcript 2edbe800-dc0f-4fbc-8e2f-54790a2406a5.jsonl; tool calls {\"mcp__ccd_turn__update_plan\": 2, \"Bash\": 74, \"Read\": 5, \"Write\": 1, \"Edit\": 1, \"mcp__ccd_turn__wrap_up\": 1}; edit tools [\"Edit\", \"Write\"]; pattern matches read by the governor: 2)",
"tests_or_engines_started": false,
"packet_sha256": "1bd8457027d81ba2f856f9320e60b9f46f979ccf11f2edfd786d1204896614f8",
"normal_packet_sha256": "720a675afb1238eff11ef1fb51b60e00b69740568e22681275a138115ad780f5",
"schema_sha256": "49ca72d63dfe23d6a6ef50f4f261c7bf706b2fcb34cb4375ce733254693ebc39",
"material_manifest_sha256": "3a149af9a30f01ea4b041b02fb7179a6564204d1c9825a8c85910f5cb8e73de2",
"result_file": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-f5fe219-r01/DESKTOP_RESULT.json",
"result_sha256": "1f530e6c4e6158a469271d287763fc8a573295f44f04dd26bf9b5e6a93f8b737",
"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 P16-01-DISMISS; did not review or edit the result)",
"session_started_by": "Root started the proposal card task_3d0cb301 once in the Claude Desktop UI (Root record ROOT_P16_F5FE219_FULL_ROSTER_REVIEW_READY_20261025_091748_R01.json, SHA-256 6f42a5fbadcb73140e6eda4fb4f7774dceb9bab5c171ae6cc2f762c58af66adf); the governor proposed the card and did not start it; a new fresh session, not local_a7f85421 (implementer), local_e0348c69 (the S b06fec1 reviewer) or local_cad69b68 (the S a073ccb reviewer); session metadata: model claude-opus-5-5, effort xhigh, permission mode auto, parent local_a7f85421",
"cli_authentication_recovered": false,
"cli_review_succeeded": false,
"recorded_at": 1792921039.984701
},
"independent_result": {
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/DISMISS_INDEPENDENT_REVIEW_F5FE219.json",
"bytes": 19385,
"sha256": "1f530e6c4e6158a469271d287763fc8a573295f44f04dd26bf9b5e6a93f8b737",
"copy_of": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-f5fe219-r01/DESKTOP_RESULT.json"
},
"review_findings": [
{
"severity": "low",
"message": "項目 6・§4.3【受入を止めない。画面の試験の範囲】患者の側の結果の文は、画面で確かめていない。tests/staff/test_general_dismissal.gd 997〜1013 行では、患者は押す前の文(「この従業員への救護は取り消し、救護係を外します。」)だけを確かめて「やめる」で閉じる。運搬者を解雇した後に患者も解雇するが、その時はもう救護の作業が無く、確かめるのは名簿から消えることだけ。§4.3 が明示する結果は「成功、拒否の理由、払った退職金、保持した未払い」で、これらは画面で全て確かめている。結果の「関わっていた救護の作業は取り消しました。」は scripts/ui/staff_management_panel.gd 391〜408 行の同じ分岐で、運搬者の画面で確かめている。患者を解雇した時に interrupted_rescue_task_ids が空でないことは、source(simulation_runtime.gd 7177 行〜。患者を目標にした作業を取り消して並べる)と、命令の試験(_test_rescue_matrix の患者の場面で、作業の取り消しと救護係の解放)で読める。そのため、説明と実際の動作は合うと判断する。直すなら、運搬の途中で患者を先に解雇し、結果の文を画面で確かめる場面を足すとよい。"
},
{
"severity": "low",
"message": "§5.a・U1【受入を止めない。画面の中の守りの試験が無い】画面の中の判定で送らない守りの分岐は、この S の試験で一度も通らない。対象は、staff_management_panel.gd 329 行〜の request_dismissal の拒否の分岐と、349 行〜の confirm_dismissal の分岐(確認を開いた後に拒否に変わった時と、古い確認の時に送らない)。修正 2 で Button.pressed.emit をやめた結果、無効のボタンへの viewport の押しは Godot が受け付けず、これらの関数に届かない。5.a のうち、無効の行で押しても送らないこと、「解雇する…」と「やめる」で送らないことは、snapshot 全体の bytes で確かめている。§4.3・F2・F9 の明示の条件は満たすので、受入は止めない。ただし、IMPLEMENTATION_NOTES §4 の「確定の時に名簿を読み直す。読み込みや変化の後の古い確認では送らない。」は、source だけの主張になっている。直すなら、確認を開いた後に資金を減らして「解雇を確定」を viewport で押し、送らないことと理由の表示を bytes で確かめる場面を足すとよい。"
},
{
"severity": "low",
"message": "A8・F7【受入を止めない。出来事の方針の一部の試験が無い】命令の中の event_service.audit_actor_availability(simulation_runtime.gd 7117 行)は、既存の _handle_missing_actors(scripts/events/event_service.gd 1056 行〜)の4つの方針の全てを通す。一方、試験(_test_event_actors 654 行〜、再開の2段の A8・A8-continue-without)は CANCEL と CONTINUE_WITHOUT だけで、REPLACE_WITH_ROLE と BRANCH_FAILURE は解雇の命令の境目で試験していない。命令は既存の公開の関数を次の時刻より早く呼ぶだけで、どの分岐も、欠けた actor と拘束を外すか instance を取り消すので、参照を残さないと source から読める。そのため受入は止めない。"
},
{
"severity": "low",
"message": "§4.1 (d)【受入を止めない。巻き戻しの失敗を検出しない】_rollback_staff_dismissal(simulation_runtime.gd 7196 行〜)の戻り値は、呼ぶ側(7068・7078・7100・7108 行の4つの失敗の分岐)で使われていない。戻り値には、各 restore_snapshot と最後の _validate_runtime_path_crosslinks の結果が入る。そのため、巻き戻し自体が失敗しても latch せず、TRANSACTION_APPLY_FAILED を返す。この形は既存の雇用の巻き戻し(_rollback_staff_candidate_hire)と同じで、IMPLEMENTATION_NOTES §1 が開示している。二重の失敗はこの S の受入の条件の外で、4つの枝の巻き戻しの結果は試験で確かめているので、受入は止めない。"
},
{
"severity": "low",
"message": "来歴【受入の判断に影響しない。証拠の保持の欠落としては残る】(1) DERIVE_A073CCB_SCRIPTS_FIRST_RUN_FAILURE.md(ccae9e5e…)の欠落(1回目の2つの出力と stdout の元の bytes)は、前の S の後続のスクリプトの話。この S の分の derive_p16_f5fe219_scripts.py(ef47e675…)は1回目で exit 0(stdout 923a4d53…)。また、この S の native と全 roster の出力は、それを作ったスクリプトに依らず、凍結 checkout の bytes と直接照合して確かめた。(2) 局所の診断の記録 P16_01_DISMISS_REPAIR2_LOCAL_CHECKS_R01.json(4b5f2326…)は diagnostic_only・formal_acceptance false で、受入の根拠に使っていない。(3) 画面の場面の診断の写し2つ(stage の上の tests/staff/diag_r2_ui_flow.gd)は元の bytes が残っておらず、log と control の4つと、生成の命令の写し(05cc8b07…)だけがある。写しは IMPLEMENTATION_NOTES §4 の、修正 1 の診断の原因の説明(倍率 0.889、行の作り直し、締めの後の panel の切り替え)の根拠で、その説明は正式の証拠でないと明記されている。この S の F9 の判定は、正式の validation と全 roster が走らせた suite(f1f77bfd…。凍結 checkout と bytes で同じ)の結果と source だけに基づき、写しの bytes に依らない。(4) 全 roster の途中の R36 の clone の共有(A073CCB_CLOSED_ALIGN_R36.json 3fed617b…、script 1db5b7a5…)は、a073ccb の閉じた2つの checkout だけを対象にし、全 roster の stage には触れていない。全 roster の source-start の 1,438 ファイルが凍結 checkout と bytes で一致し、frozen_before と frozen_after が f5fe219・清潔なので、入力は変わっていない。driver の space の欄が R34 だけを挙げるのは記録の不足だが、別の記録で補われている。以上から、どの過程も入力の来歴と受入の判断を変えないと判断する。ただし、(1) と (3) の元の bytes は失われ、同じだったと誰も確かめられないので、Root が保持するとおり、証拠の保持の欠落として残る(承認でも免除でもない。受入を止める理由にはしない)。"
}
],
"review_high_medium": 0,
"review_low": 5,
"review_provenance_records": [
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-f5fe219-r01/READY_FOR_DESKTOP_REVIEW.json",
"bytes": 2934,
"sha256": "aa8362e46aa06494aa49750679c965f81f5a35550c7c87b7e56a96338a5a4218"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-f5fe219-r01/REVIEW_BRIEF.md",
"bytes": 14868,
"sha256": "00d7e397eabbabb0024db6b7a4af8b6523f8fd8fcaac359af091c0dd23e8f612"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-f5fe219-r01/DESKTOP_DELIVERY.json",
"bytes": 3982,
"sha256": "0328910727b91155d315f111d0c56b95e5b3f71822fcb50189dc9eb76422be0b"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-f5fe219-r01/INTAKE_STARTED.json",
"bytes": 217,
"sha256": "cd5e3fbee5037d5320e249fa872f3b3dbd19b6fef5212ba8c011b2b844719ada"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-f5fe219-r01/INTAKE_FINISHED.json",
"bytes": 287,
"sha256": "f8ef1ebd47decfb90dfa48fab25f3073addfd004f8b79f0658137e9151ea841f"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/DESKTOP_DELIVERY_INSPECT_F5FE219.stdout",
"bytes": 32452,
"sha256": "03740c5fbe8b608c39121290faab2a16654a88aeb94b90b97fcedc31a6fb910f"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/DESKTOP_DELIVERY_WRITE_F5FE219.stdout",
"bytes": 192,
"sha256": "87e2e43eb8e2d9f9324b26194e4ecdbfcfc7e9ef1aae6e72a04438481e0795bd"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/RECEIVE_REVIEW_F5FE219.stdout",
"bytes": 179,
"sha256": "fe6019f16786d2944f34d5bac265c4c70253bc55a680a33a77af2106a1444ebe"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/PREPARE_INTEGRATION_F5FE219_SUMMARY.json",
"bytes": 3213,
"sha256": "ed07d7c6f62282b7584adf713a9e8e4d5a53b619e0035b49324137d3c12302ad"
}
],
"earlier_reviews_retained": [
{
"S": "b06fec17c7c1e2e4cc22116f209c51dec751215c",
"reviewer": "local_e0348c69",
"first": {
"status": "pass",
"result": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-b06fec1-r01/DESKTOP_RESULT.json",
"bytes": 12105,
"sha256": "12434d831139d3b5a0a149858cdd11e33dd71c035909c33ea36f5cf2677f241b"
}
},
"revised": {
"status": "changes_requested",
"high": 1,
"medium": 6,
"low": 4,
"result": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-b06fec1-r01/DESKTOP_RESULT_SUPPLEMENT_R02.json",
"bytes": 19724,
"sha256": "8048931a47f5cc231fe043fd9febe678f5784da2f5cecdc8a66fdb9f9889cdbb"
}
},
"repair": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/COMMANDER_REPAIR_PLAN_P16_01_DISMISS_R01.md",
"bytes": 5878,
"sha256": "6dbb5ce11e7c8f392ba71f7dd16ea34bab5146c312f7a33718205466c581d2b2"
}
},
{
"S": "a073ccb59b9e20e91875008b5d34431ed5a1174f",
"reviewer": "local_cad69b68",
"status": "changes_requested",
"medium": 1,
"low": 3,
"result": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-a073ccb-r01/DESKTOP_RESULT.json",
"bytes": 17281,
"sha256": "dacafb2225938df225cf30ef5412ab8ebded48b2307a230a76d7ad963ee87a22"
},
"brief": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/p16-01-dismiss-independent-review-a073ccb-r01/REVIEW_BRIEF.md",
"bytes": 11497,
"sha256": "98e1a1e1df97d0177752cff562520298ca287ac5b3934568d90499a1a7b0eb94"
},
"repair": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/COMMANDER_REPAIR_PLAN_P16_01_DISMISS_R02.md",
"bytes": 5953,
"sha256": "fec141abf13d4a4e3e9947d1c2b4888a04376317ce25f12be7f1f74d57dcaafd"
}
}
],
"measurement": {
"measured_on": "f5fe2196740466a673879cb2950d96446fd99d6e",
"native": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/NATIVE_F5FE219_START.json",
"bytes": 872,
"sha256": "984524b3ac62eded620d01e024b29ef5aa0462140eab3c45cf6f9c3a98a24bd5"
},
"native_bundle_transfer": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/NATIVE_F5FE219_BUNDLE_TRANSFER.json",
"bytes": 1349,
"sha256": "fad160fd2ac3af620d922852b2b85735df7208cbc3a165b642b5bdd6693bf0d7"
},
"formal_validation_summary": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/FORMAL_VALIDATE_F5FE219_SUMMARY.json",
"bytes": 2819,
"sha256": "d89ef221efec094612d719fe0b27f5839f02e970994389ca1af9d6130c75c0ce"
},
"full_roster_summary": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/FULL_ROSTER_F5FE219_R01_SUMMARY.json",
"bytes": 3457,
"sha256": "53d615aecb12a9fb37b6d1a6f21ac7fc23f2c073d47687c62d1ec9f65f262cd6"
},
"full_roster_placement": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/FULL_ROSTER_F5FE219_PLACEMENT.json",
"bytes": 981,
"sha256": "23d07fb54ba648680c43c05430324d7d265029244efc1624684a6c046ec74048"
},
"native_phases": 14,
"native_suites": 12,
"native_checks": 1888,
"full_roster_suites": 201,
"full_roster_checks": 24073,
"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/p16-01-dismiss-control-r01/WX_CLOSED_ALIGN_R29.json",
"bytes": 5996450,
"sha256": "727ae9cbfd288064bd6899e12ba33c1f2a99d118b0a0ad7cc02fa91f9e8ed28a"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/B06FEC1_CLOSED_ALIGN_R30.json",
"bytes": 1544347,
"sha256": "25392b6da3c18f2c8adcb04fa8a6c1cd44e9a0bbdd0368ec8a69c6d636f840c2"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/P16_01_DISMISS_FULL_ROSTER_RAW_COMPRESSION_B06FEC1_R31.json",
"bytes": 3151119,
"sha256": "63611a45ba37a42b044ba22306643f41dbbba4accc87dc31c425a8e1f2dc951f"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/B06FEC1_BUILD_ITEMS_MOVE_R32.json",
"bytes": 1330,
"sha256": "e057a341803d95c4328f7758e69da74de1ccb2c2a237e5b725391bb1dac35fbc"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/P16_01_DISMISS_FULL_ROSTER_RAW_COMPRESSION_A073CCB_R33.json",
"bytes": 3187050,
"sha256": "d9f19a2ca8b2d51b11eed91b81f1d9d0c0a4aaa55332b7a136e552c0d0789256"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/A073CCB_BUILD_ITEMS_MOVE_R34.json",
"bytes": 1410,
"sha256": "6fbd0681edde6d80be21960c64ce8131c003be4eb8f8969a3d20080cc4ba681b"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/P16_01_DISMISS_FULL_ROSTER_RAW_COMPRESSION_F5FE219_R35.json",
"bytes": 3223348,
"sha256": "eaacdea029142ee164d6c3169f8c65a0977db87f1ba45b4329af0006ee5af66f"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/A073CCB_CLOSED_ALIGN_R36.json",
"bytes": 3115499,
"sha256": "3fed617b19d54eb6e91f55f493cb1c52a2149716194ded325b4006cc85dd5d3c"
}
],
"capacity_note": "R36 ran during the S f5fe219 full roster (free about 0.52 GB with 23 suites left); it aligned only the closed S a073ccb checkouts and touched no roster input",
"historical_evidence_retained": [
{
"S": "b06fec17c7c1e2e4cc22116f209c51dec751215c",
"records": [
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/FORMAL_VALIDATE_B06FEC1_SUMMARY.json",
"bytes": 2819,
"sha256": "b11f4f8416ae9c93e7886dcc05cb3efe5ccbfd695b784efda49538bab6c2b9a6"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/FULL_ROSTER_B06FEC1_R01_SUMMARY.json",
"bytes": 3227,
"sha256": "636a4d69b0e3afb697266d750148398a3145750c812e8cbbb9ba5ab16c40860e"
}
],
"meaning": "S before repair 1: native, validation and full roster passed; the review was revised to changes_requested"
},
{
"S": "a073ccb59b9e20e91875008b5d34431ed5a1174f",
"records": [
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/FORMAL_VALIDATE_A073CCB_SUMMARY.json",
"bytes": 2820,
"sha256": "c4541e73bfa024db00280203c0eb4aab4134c3c7318023bb95e50c15f575d64b"
},
{
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/FULL_ROSTER_A073CCB_R01_SUMMARY.json",
"bytes": 3331,
"sha256": "c72f2fdfe9f916520e32952ba12ef2f408cb3d79fa581d39b053ca474b799489"
}
],
"meaning": "S after repair 1: native, validation and full roster passed; the review was changes_requested (F9 normal input)"
},
{
"record": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/P16_01_DISMISS_LOCAL_CHECKS_R01.json",
"bytes": 13545,
"sha256": "e26f9a6abec88809edeb1668dfa651c3b229db394a03a0f69ec330a04df4ea16"
},
"meaning": "first local checks with their failed runs (diagnostic)"
},
{
"record": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/P16_01_DISMISS_REPAIR1_LOCAL_CHECKS_R01.json",
"bytes": 14381,
"sha256": "d3b8be1314ec522e55e4a835218c50df9eab7a6b08879cbcc572e77bc49bc91b"
},
"meaning": "repair 1 local checks (diagnostic)"
},
{
"record": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/P16_01_DISMISS_REPAIR1_LOCAL_CHECKS_R01_CORRECTION_01.json",
"bytes": 6197,
"sha256": "391592eedbba654cdb0079521f6ba9f1480cbb03a0b60836e0ab7c24d7daa583"
},
"meaning": "correction of the repair-1 intermediate failure text (run-r1-1 7/640, run-r1-2 7/641, run-r1-3..5 2/663); the receipt kept unchanged"
},
{
"record": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/P16_01_DISMISS_REPAIR2_LOCAL_CHECKS_R01.json",
"bytes": 10924,
"sha256": "4b5f2326d820b21fa524e159fc3aaca059cb3a1749a9623ca3450fe2681e6714"
},
"meaning": "repair 2 local checks with the failed first UI run (diagnostic)"
}
],
"record_gaps_kept": [
{
"record": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/DERIVE_A073CCB_SCRIPTS_FIRST_RUN_FAILURE.md",
"bytes": 1508,
"sha256": "ccae9e5e9c0922843f06e6240e92a1d35a2c7abf8766cfa8ece10f662e5a17ff"
},
"meaning": "the first run of the a073ccb follow-up script derivation failed; its two outputs, its stdout and the first script bytes are not kept; the failure text was transcribed from the conversation"
},
{
"record": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/P16_01_DISMISS_REPAIR2_DIAG_COPIES_PROVENANCE_R01.json",
"bytes": 4072,
"sha256": "110bda16b0751e3876d266ba9ac3a25b5eeb1c2c82971e8fb93c3eda77dafc77"
},
"extracts": {
"path": "artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/DIAG_R2_UI_FLOW_COPIES_TRANSCRIPT_EXTRACTS_R01.json",
"bytes": 21117,
"sha256": "05cc8b07b4ecdd745cd779a0735215768c188461afbbc908e5042b3fd2cc0efa"
},
"meaning": "the two stage-only UI diagnostic copies are not kept as files; their logs, generating commands (transcribed) and derived-from sources are kept"
},
{
"note": "Root keeps these as evidence-retention gaps; neither approved nor waived. The independent review judged they do not change the input provenance or the acceptance. They are not a loss of the new S formal test raw."
}
],
"remaining_not_claimed": [
"Original P16-01 stays unmet: the ordinary recruitment operation (AR.staff.recruitment), market and debt acquisition (AR.staff.market_acquisition, debt_acquisition, D04) and the cost arrangement per employment category (SPEC section 8)",
"Original P16-03 stays unmet: the cross-check of shift, rest, collapse and rescue (D04) and the reference removal on departures outside the dismissal path (SPEC section 8)",
"Human trafficking (AR.staff.sale_*, D04) is not addressed",
"AR.staff.dismissal_severance is connected by the A14 decision and this acceptance but is not counted as the completion of the original P05 package work/AC",
"Dismissing the defender of an unfinished raid (R9 DEFENSE_LOCKED, Root temporary boundary) stays unmet as an original requirement; no retreat or combat rule was made",
"Review low: the patient-side result text of an interrupted rescue is not checked on screen",
"Review low: the panel guards that refuse a stale or changed confirmation are not exercised by a test",
"Review low: the Event missing-actor policies REPLACE_WITH_ROLE and BRANCH_FAILURE are not tested at the dismissal boundary",
"Review low: the return value of _rollback_staff_dismissal is not used (the existing hire rollback form)",
"The test input route is viewport input (root.push_input), not OS input; the same Windows device, IME, OS scaling and same-candidate device acceptance (SPEC section 10) remain; the UI uses the current parts and is not the finished look",
"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. Implementation, repairs 1 and 2, natives and full rosters by the registered Claude Opus 5.5 Desktop implementer and commander (local_a7f85421); commits made by Runner submit; independent reviews by separate fresh Opus 5.5 Desktop sessions (local_e0348c69, local_cad69b68, local_4e746265)",
"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": false,
"formal_test_raw_and_earlier_reviews_retained": true,
"prior_failure_retention_scope": "Kept: the raw output of every formal test (natives, registered validations, full rosters of S b06fec1, a073ccb and f5fe219), the earlier review results and deliveries, the failed local and diagnostic run logs and the receipts. Not kept as original bytes: the first run of the a073ccb follow-up script derivation (its two outputs, its stdout and the first script bytes; the failure text was transcribed from the conversation) and the two stage-only UI diagnostic copies of repair 2 (their logs, transcribed generating commands and derived-from source hashes are kept); see record_gaps_kept. No earlier S evidence is reused for the new S.",
"correction_note": "2026-10-25: all_prior_failures_retained was written true in the first version of this record; it is false because of the record gaps above (Root observation). The earlier version stays in the Git history (commit 8a2ea74).",
"CLI_auth_recovered": false,
"no_new_human_utterance": true
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
{
"task": "P16-01-DISMISS",
"spec": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/SPEC.md",
"rule": "SPEC F10: every changed expectation of an existing suite is listed with the value before and after and the reason.",
"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), P05-02 A12 (the five empty fund accounts) and P07-01 A13 (natural weather) into the old checkpoint before the whole typed comparison",
"after": "the economy mapping also adds the empty STAFF_SEVERANCE_EXPENSE account (allow_negative false) when the old save lacks it and the loaded save has it; the fund accounts and the severance account are added in one step and the whole economy must then be byte-identical, so nothing else may differ",
"reason": "A14 / SPEC section 3.6: a save from before the severance gains its empty expense account on load (FinancePolicy._prepare_severance_account, the A12 form). The genuine legacy14 fixture is such a save, so its loaded economy differs from the old checkpoint exactly in this account."
}
],
"not_expectation_changes": [
"tests/economy/test_p05_funds_runtime.gd _test_old_save_migration keeps its expectation: its old save is copied from a current save, so it already holds the severance account and only the five fund accounts are removed.",
"No existing suite expected a save with unpaid rank tax or interest after a close to be refused; the restore fix of IMPLEMENTATION_NOTES.md section 6 changes no listed expectation.",
"Repair 1: no existing suite reads the operation id of a staff Task's inventory reservation (staff.task.reserve.*); the reservation-count key of IMPLEMENTATION_NOTES.md section 5 changes no listed expectation."
]
}
@@ -0,0 +1,177 @@
# P16-01-DISMISS 実装の記録(実 Opus の実装担当、2026-10-24、修正 1 と修正 2 は 2026-10-25)
- 仕様: 同じフォルダの `SPEC.md`(登録版 r04。main `0b59d7c` に固定。ファイルの SHA-256 は `b4cc31c0…5f77`、Runner の `spec_sha256` は `14efffd9…e6b`)。出典は `PROVENANCE.json`。
- 原本への加算は 0。原単位 P16-01・P16-03、`AR.staff.dismissal_severance` の package P05 の原 work・原 AC は閉じない(SPEC §8)。
- 管理の記録は `artifacts/original-completion-2026-10-02/p16-01-dismiss-control-r01/` にある(git の外)。
- 変えた製品のファイル: `autoload/simulation_runtime.gd`・`scripts/economy/finance_policy.gd`・`scripts/ui/staff_management_panel.gd`。`scripts/staff/staff_service.gd` と `scenes/main/main.gd` は変えていない(§1・§4)。
## 0. 修正 1 の経過(S `b06fec1` → 修正の後の S)
- S `b06fec1` は、同じ実 reviewer の補足(改訂)の判定で changes_requested になった(high 1・medium 6・low 4。`DESKTOP_RESULT_SUPPLEMENT_R02.json`)。1回目の pass と、その後の補足の判定は、どちらも書き換えずに残している。
- 主な所見と直し:
- high: S `b06fec1` は、乗ったままの救護の組の解雇を、実装の判断で `RESCUE_PAIR_ABOARD` として拒んでいた。これは SPEC §6.2 の最後の文「直し、拒否に変えない」、F1・F7(許可の全ての行)に合わない。拒否を消し、組を解く後始末を作った(§3)。
- medium: 開き直した予約つきの作業を別の職員が取れなかった(`IDEMPOTENCY_CONFLICT` で BLOCKED)。予約の操作 ID を直した(§5)。
- medium: preview と結果が、命令の中で取り消す救護の搬送を「開き直す」と書いていた。分けて実際どおりにした(§4)。
- medium: preview が R5(運営の終了)と R3(予約を解けない)を判定していなかった。判定を足した(§1)。
- medium と low: 条文が明示する確かめ方を代用していた所がある(同じ process の読み込み、通勤の昇降機、拒否の行の画面、bytes の比べ、別の process の境目など)。条文どおりの試験を足した(§7)。
- 新しい拒否や、R9 に似た境界は足していない。受入の条件も削っていない。
### 修正 2 の経過(S `a073ccb` → 修正の後の S)
- S `a073ccb` は、新しい fresh な別の実 reviewer の判定で changes_requested になった(medium 1・low 3。`p16-01-dismiss-independent-review-a073ccb-r01/DESKTOP_RESULT.json`)。F1〜F8 と F10 は満たし、F9 を満たさないという判定。結果・配送・transcript は書き換えずに残している。
- 所見と直し(製品のファイルは変えていない。試験と記録だけ):
- medium(F9・§4.3 の通常の入力): S `a073ccb` の画面の試験は、Main のキーの処理を直接呼び、ボタンの `pressed` の信号を直接出していた。viewport の入力の経路で、解雇と確定のボタンに届くことを確かめていなかった。画面の試験を viewport の入力に置き換えた(§4・§7)。
- low(§4.1 (d)): 試験で失敗を起こしていなかった2つの巻き戻しの枝を、開示した fixture で起こした。記録の「起こせない」という理由は、`remove_staff` の枝について正しくなかったので直した(§1)。
- low(§4.3): 裏金からの補填の文と、命令の中で取り消す救護の文を、画面で確かめた(§7)。
- low(後続のスクリプトの1回目の元の bytes の欠落): 管理の記録の欠落として残す。この W の中で直すものは無い。
## 1. 命令と判定の順(SPEC §4.1)
- `SimulationRuntime` に通常の command `dismiss_staff`(payload `staff_id`)を足した(`_execute_dismiss_staff`)。
- (a) 運営の終了(`BANKRUPT`)と (b) 同じ key の再送(`DUPLICATE_IDEMPOTENCY_KEY`)は、今の `submit_command` と `CommandQueue.enqueue` のまま受付の前・受付で止まる(R5・R6、5.b)。
- (c) executor は、状態を変える前に次の順で確かめる。どれかに当たれば、その理由で失敗の結果を返す(5.c)。
1. payload に `staff_id` が無い → `INVALID_PAYLOAD`
2. 職員が居ない → `NOT_FOUND`(R4)
3. `UNIQUE` → `UNIQUE`(R1。今の `remove_staff` と同じ code と文)
4. `FREE_HIRE` でない(`ADULT_CONTRACT`)→ `NOT_GENERAL_EMPLOYEE`(R7)
5. 外部が持つ作業(仕入れの物流の受け取り・配達・返送)→ `EXTERNAL_TASK`(R2。今の `remove_staff` と同じ code と文)
6. 持っている作業の予約を解けない → `RESERVATION_RELEASE_FAILED`(R3。今の `remove_staff` と同じ code と文)
- 修正 1 で、変える前の判定にした(`_staff_task_reservations_releasable`)。在庫の解放は、予約が無い時と消費済みの時に失敗する(`InventoryService.release_reservation`)。それを、読むだけの `get_reservation` で前もって見る。
- `remove_staff` の同じ拒否は、最後の守りとして残す。そこで止まった時も、全体を巻き戻す。
7. 終わっていない襲撃の防衛者 → `DEFENSE_LOCKED`(R9。Root の一時の境界)
8. 表金と裏金の合計が退職金に足りない → `INSUFFICIENT_FUNDS`(R8)
- (d) 全て通れば、次を1つの操作として行う。
1. 乗ったままの救護の組なら、解雇する人の座席を解く(§3)。
2. 既存の削除の契約 `StaffService.remove_staff(staff_id, &"DISMISSED", true, 分)`。
3. 退職金の記帳(§2)。
4. 救護の後始末(§3)、施設の人員の投影(`_sync_facility_staffing`)、出来事の欠けた職員の扱い(`event_service.audit_actor_availability`)。
- (d) の途中で失敗すれば、命令の前の写し(staff・inventory・path・elevators・移動の残り・出来事・ID・帳簿・Gold の写し)に全て戻して、`TRANSACTION_APPLY_FAILED` を返す。理由は次のとおり。
- 座席を解けない時 `RESCUE_SEAT_RELEASE_FAILED`
- `remove_staff` が予約を解けない時 `RESERVATION_RELEASE_FAILED`(R3 の最後の守り)
- 退職金を記帳できない時 `SEVERANCE_POST_FAILED`
- 救護の作業を取り消せない時 `RESCUE_CLEANUP_FAILED`
- 4つとも、試験で失敗を起こし、5.c の比べ方で何も変わらないことを確かめる(修正 2 で座席と `remove_staff` の2つを足した。§7)。
- 退職金: 記帳の key を先に使う(開示した fixture)。乗ったままの組の段でも起こし、座席を解いた後の path と昇降機の巻き戻しを確かめる。
- 救護の後始末: 迎えの作業に無い予約の ID を持たせる(開示した fixture)。
- `remove_staff`: 在庫の解放の操作 ID(`staff.task.release.<作業>.<予約>`)を、先に別の予約の操作で束縛する(開示した fixture)。前もった判定(予約が無い・消費済み)は通るが、在庫が `IDEMPOTENCY_CONFLICT` で解放を拒む。修正 1 の記録の「試験の組み立てからは起こせない」は、この枝について誤りだった。
- 座席: 運搬者の Path の乗車の要求を、公開の `PathService.complete_elevator_request` で先に終える(開示した fixture)。この状態は保存の照合が拒む形で、通常のゲームの状態からは起きない。
- 巻き戻しの関数 `_rollback_staff_dismissal` の戻り値は使っていない。既存の雇用の巻き戻し(`_rollback_staff_candidate_hire`)と同じ形のまま。
- 画面と試験のための読むだけの関数 `get_staff_dismissal_preview(staff_id)`。返すのは、解雇できるか・理由・退職金・未払い・開き直す作業・命令の中で取り消す救護の作業と役(運搬者か患者か)・表金・資金の合計。
- 運営の終了の後は、理由を `BANKRUPT` にする(R5。命令は受付の前に拒まれる)。
- `get_staff_management_report` の各行に `dismissal` として載る。
## 2. 退職金(SPEC §3.5・§3.6、A14)
- `FinancePolicy` に、`SEVERANCE_EXPENSE`(口座 `STAFF_SEVERANCE_EXPENSE`)・`SEVERANCE_CATEGORY`(区分 `STAFF_SEVERANCE`)・`SEVERANCE_DAYS`(3)・`severance(日給)` を足した。額は `get_effective_daily_wage() × 3`。
- 記帳は命令の時刻に、1つの取引(idempotency key `staff.severance.<command の ID>`)で行う。
- 表金で足りない分: 裏金 → 表金の補填(区分 `BACK_FUND_TOP_UP`。P05-02 の返済と同じ形)。
- 退職金: 表金 → `STAFF_SEVERANCE_EXPENSE`(区分 `STAFF_SEVERANCE`、source は職員の ID)。
- 日次決算の照合では、`cost_of_goods`(画面「運営費(仕入れ・契約金等)」)に入れる。
- 写した表は `_build_operations_daily_financials` と `_operations_expected_category_totals` の2つ。施設ごとの表は、施設でない source を読まないので変えていない。
- 補填は、fund report v2 の `back_top_up`(その日の全ての補填)に入る。
- 旧セーブ: `FinancePolicy.prepare_accounts` の最後で、退職金の口座が無ければ空の口座として足す。A12 の5つの口座とは別に判定するので、P05-02 の後・P16 の前のセーブにも足す。口座があって負を許す設定なら拒む。
- 解雇の前に貯まった未払いの給与は、今の契約のまま `_departed_payroll` に保持し、次の 00:00 の締めで払う(A09 の補填を含む)。解雇の日の日給は、今の給与の契約のまま(締めの時に雇用中の職員だけ)。
## 3. 後始末(SPEC §6.2・§6.2.1)
- 勤務・予定・配置は `remove_staff` が行う。経路の actor・昇降機の予約・移動の残り・`STAFF_REMOVED`(reason `DISMISSED`)は、今の `_on_staff_removed` が行う。
- 施設の人員の投影は、命令の中で `_sync_facility_staffing` をすぐ行う。サービスの途中の職員が抜けて人数が `staff_min` を欠けると、次の施設の更新から今の規則で職員を待ち(5 分)、代わりが来なければ失敗する(A4 の試験)。
- **乗ったままの救護の組(修正 1)**: S `b06fec1` の `RESCUE_PAIR_ABOARD` の拒否を消した。組の段階でも解雇できる。
- 解雇する人の座席だけを、命令の時に組から解く(`_release_dismissed_rescue_seat`)。SPEC §10 の「非描写の状態として命令の時点で取り除く」に当たる。
- 手順は、`ElevatorService` 自身の乗っている1人の取り消し(`_cancel_single`)と同じ。公開の `path_service.complete_elevator_request` で乗車の要求を終え、昇降機の公開の snapshot から、その人の trip・座席・相方の `companion_id` を外して `restore_snapshot` に渡す。
- `ElevatorService` は変えていない。`restore_snapshot` の照合(Path の要求と trip の対応、座席、相方の対応)を通る形なので、命令の直後の保存も照合を通る。
- 残る人(患者または運搬者)は、今の規則(`_advance_staff_rescue` の、乗っている間は組を保つ分岐)どおり、1人で乗ったまま本物の階に着く。
- 運搬者を解雇した時: 患者は着いた階で倒れたままになり、次の時刻の救護の確認が、迎えを今の規則で作り直す。
- 患者を解雇した時: 運搬者は着いた階で手が空く。
- 試験は組の段階で運搬者と患者のそれぞれを解雇し、次を確かめる(§7)。
- 参照が残らない。
- 残る人が本物の階(昇降機の止まる階)で降りる。
- 救護が今の規則でやり直される。
- 命令の直後の保存を、同じ process(写しの復元・ディスク)と別の process(再開の2段)で読む。
- 救護(`_interrupt_dismissed_staff_rescue`): 次の時刻の救護の確認(`_advance_staff_rescue` の最初の段)と同じ規則を、命令の中で、解雇した職員に関わる作業にだけ当てる。
- 患者を解雇した時: その患者を目標にした、終わっていない救護の作業を `RESCUE_INTERRUPTED` で取り消し、運搬者を作業から外す。
- 運搬者を解雇した時: `remove_staff` が開き直した搬送(`rescue_transport`)は、運搬者が居ないので取り消す(今の規則と同じ)。迎え(`rescue_pickup`)は開き直ったまま、別の医療職を待つ(今の規則でも有効な作業)。
- 昇降機の予約の取り消しは、乗っていない生きた人だけに行う。乗っている人は、着くまで乗せたままにする(今の規則)。
- 出来事: 命令の中で、公開の `event_service.audit_actor_availability` を呼ぶ。進行中・選択待ち・中断の出来事の欠けた職員を、今の `missing_actor_policy` で扱う(CANCEL と CONTINUE_WITHOUT を試験)。予約した費用は、今の規則で1回だけ戻る。この audit は他の欠けた actor も今の規則で扱う(次の時刻と同じ処理が早く行われるだけ)。
- 命令の直後に保存し、同じ process と別の process で読み込んでも、欠けた職員への生きた参照が残らず、保存の照合が通る。生きた参照とは、終わっていない作業の患者と運搬者、昇降機の予約と相方、経路の actor、施設の人員、出来事の拘束と actor のこと。
- 履歴として残るもの: 終わった作業の記録の `target_entity_id`、サービスの `staff_ids`(始めた時の記録)、給与の締めの行、雇った候補の記録(同じ候補は二度雇えない今の規則のまま)。
## 4. 画面(SPEC §4.3)
- `StaffManagementPanel` に、「解雇する…」と確定の欄(「解雇を確定」「やめる」)を足した。判定は Runtime の preview に従う。
- 押す前に、退職金の額(日給の3日分)、裏金からの補填、未払いの給与、開き直す作業を出す。
- 修正 1 で、命令の中で取り消す救護を分けて出す(`interrupted_rescue_roles`)。運搬者なら「運んでいる救護の搬送は取り消し、患者の救護をやり直します。」、患者なら「この従業員への救護は取り消し、救護係を外します。」。
- 拒否の行(R1・R2・R3・R5・R7・R8・R9)ではボタンを無効にし、理由を出す。`UNIQUE` は今の「ユニーク:雇用・維持費無料 / 解雇・売却不可」、それ以外は「解雇不可:…」。無効のボタンを無理に押しても、命令を送らない(5.a)。
- 確定の時に名簿を読み直す。読み込みや変化の後の古い確認では送らない。
- 時間が止まっている時はすぐに、進んでいる時は次の時刻に、命令が処理される。
- 結果として、払った退職金・補填・保持した未払い・開き直した作業・取り消した救護、または拒否の理由(押した後に資金が減って拒まれた場合を含む)を出し、名簿と選択を更新する。
- 解雇の受理を待つ間は、その職員の配属の変更とドラッグを止める。
- `Main` の職員の管理の画面は、今の部品と送り手(`_audio_feedback.submit_command`)のままで使えるので、`scenes/main/main.gd` は変えていない。UI は今の部品のままで、完成の見た目ではない。
- 入力の経路(F9、修正 2): 試験は本物の Main と Runtime を使い、viewport の入力で操作する。
- キー(雇用の候補を開く H)は `root.push_input` の `InputEventKey`。
- ボタン(候補の雇用・「雇用済み」・名簿の行・「解雇する…」・「やめる」・「解雇を確定」・締めの後に戻る「従業員」)は、全 roster で PASS している `tests/ui/test_business_management.gd` の `_click_principal_button` と同じ形で押す。ScrollContainer で見える位置へ送り、マウスの移動、押しと離しを viewport の local の座標で送り、各フレームを待つ。
- 拒否の行では、無効のボタンを viewport の入力で押しても命令を送らず、snapshot の全体の bytes が同じことを確かめる。
- 時計が動いている時の押し方: 離す瞬間だけ時計を動かし、すぐ止める。命令は次の分の処理になり、フレームが分を進めない(1分は実時間 1/6 秒なので、フレームを待つ間に時計を動かしたままにすると、確かめる前に分が進む)。
- 修正 1 の headless の診断でボタンが反応しなかった原因(修正 2 の局所の診断で確かめた。正式の証拠ではない):
- Main は基準の大きさ 1440×810 から縮めて表示される(窓 1280×720 で倍率 0.889)。修正 1 の診断は `push_input` に local の座標を指定せず(もう1つの形の `Input.parse_input_event` も窓の座標)、押した位置が約 1.125 倍ずれてボタンから外れていた。
- 名簿の行は `refresh` で作り直される。そのフレームに足した部品では `ScrollContainer.ensure_control_visible` がスクロールしないので、1フレーム待ってから見える位置へ送る。
- 0 時の締めの後、Main は側面の panel を帳簿に切り替え、職員の画面を含む部分を隠す(今の `_on_snapshot_published` の動き)。試験は、プレイヤーと同じく「従業員」のボタンで戻る。
- どれも試験の押し方の問題で、画面の部品や配置は変えていない。
- これは viewport の入力で、OS の入力ではない。Windows の実機・IME・OS の倍率・同じ候補での実機の受入は、SPEC §10「Windows 実機は後」の後続の条件として、実施しておらず未達のまま残す。
## 5. 予約の操作 ID(修正 1、A5・F7)
- 原因: 在庫の予約の操作 ID が、作業と品目だけで決まっていた(`staff.task.reserve.<作業>.<品目>`)。在庫の照合の fingerprint には予約した分が入るので、開き直した作業を別の分に別の職員が取ると、`IDEMPOTENCY_CONFLICT` になって作業が BLOCKED になる(戻す経路は無い)。同じ分なら、閉じた旧い予約の結果が再生される。
- 直し(`_reserve_staff_task_items`): 操作 ID に、その作業のその品目のこれまでの予約の数を足す(`staff.task.reserve.<作業>.<品目>.<回数>`)。在庫は予約も照合の記録も消さないので、回数は増える一方で、予約ごとに別の操作になる。分ではなく回数にしたので、同じ分に解雇して取り直しても、旧い予約に当たらない。
- 試験(§7):
- 元の予約・解雇・別の職員の再予約を同じ分で起こし、別の職員が終えるまで。同じ分の派遣は、StaffService の公開の `dispatch_now` で起こす。
- 列で処理される解雇と同じ tick での再予約から、完了まで。
- 清掃(`facility_clean`)の作業の開き直しと、別の清掃係による完了。
- 既存の試験は、この操作 ID の文字列を見ていない(期待の変更なし)。
## 6. 見つけた既存の不具合と直し(S `b06fec1` で直したもの、変更なし)
- 締めの後に税・利息の未払いが残ったゲームの保存が読めなかった(P05-02 からの不具合)。
- 保存を読む時の Operations の照合(`_parse_operations_runtime_snapshot`)は、締めた未払いを「給与 + 維持費」だけで数えていた。実行中の照合(`_operations_closed_arrears_total`)が足している税・利息の未払い(`_finance_arrears_through`)を、数えていなかった。
- 読む時の照合を、実行中と同じ式にそろえた。照合を緩めたのではない。
- 試験: 帳簿の日の流れが、税の未払い 30 を確かめてから締めの後の保存を復元する。同じ process(修正 1 で全ての保存点)と別の process でも読む。
## 7. 試験
- 新しい suite `tests/staff/test_general_dismissal.gd`(F1〜F9。§6 の全ての行。共通の部品は `tests/helpers/p16_dismissal_fixture.gd`)。最後の場面がゲームの Main を使うので、suite-policy で ordinary の音の後片付けの証明(`single`)を持つ。修正 1 で足した場面:
- 乗ったままの組を含む、救護の全ての段階での運搬者と患者の解雇。preview と結果で、開き直すと取り消すを分けて確かめる。
- 通勤の職員が上の階の職場へ向かう時に、昇降機を待つ段と乗っている段で解雇する(A2)。
- 予約つきの作業の同じ分の再予約と完了、同じ tick の再予約と完了。清掃の作業の開き直しと完了(A5・F7)。
- 帳簿の日の流れの全ての保存点を、同じ process でディスクから読み、最後まで続ける(F8)。
- 物流の返送の段の拒否(R2)。
- 救護の後始末の失敗の巻き戻し(5.c)。
- 列で待つ命令の処理を 5.c の増分で比べる(A9)。
- その命令は、Runtime 自身の命令の段(`_process_commands`)で、tick の他の処理なしに処理する。
- Main の画面で、拒否の行の全て(R1・R2・R3・R5・R7・R8・R9)を確かめる。ボタンが無効で、理由が出て、無理に押しても snapshot の全体の bytes が同じ。
- 保持した未払い(本物の 0 時の締めで資金が足りず、日給が未払いになる)も、押す前と結果で確かめる。
- 押した後の拒否の理由も確かめる。
- 画面の比べは全て `var_to_bytes`。
- 修正 2 で変えた・足した場面:
- Main の画面の場面を、viewport の入力で操作する形に置き換えた(§4)。`Button.pressed.emit()` と `_unhandled_key_input` の直接の呼び出しは、この suite から無くした。
- Main の画面で、裏金からの補填(表金 G100・退職金 G300・補填 G200)の押す前と結果の文を確かめる。資金の fixture は、解雇の後に元の残高に戻る形にした。
- Main の画面で、救護室へ歩いて運ぶ搬送の段に、患者と運搬者の押す前の文と、運搬者の結果の文(取り消した救護)を確かめる。
- 巻き戻しの枝(`_test_rollback_branches`): `remove_staff` の予約の解放の失敗、乗ったままの組の座席の解放の失敗、乗ったままの組で座席を解いた後の退職金の記帳の失敗。どれも 5.c と 5.d の比べ方で、組が乗ったままであることも確かめる。
- 再開の2段 `tests/staff/test_general_dismissal_restart_1_write.gd`(唯一の生成ファイルは `artifacts/p16-01-dismiss/restart-handoff.json`)と `..._2_read.gd`。
- 帳簿の6つの保存点を読む。
- 次の命令の境目の保存を読む。修正 1 で、A6 の迎え・A6 と A7 の乗ったままの組・A8 の CONTINUE_WITHOUT を足した。
- A6: 搬送を徒歩で運ぶ段・迎えの段・組で乗っている段
- A7: 運搬者が1人で乗っている段・組で乗っている段
- A8: CANCEL・CONTINUE_WITHOUT
- 局所の確かめで分かった、既存の規則による制約(製品の不具合ではない。試験の組み立てを直した):
- 保持した未払いの給与は未払いの費用として数えられ、建設と雇用を止める(`UNPAID_EXPENSES`)。
- 締めを通らない未払いを持たせた職員の登録は、運営の照合(`OPERATIONS_CROSSLINK_MISMATCH`)を止める。そのため画面の試験は、本物の締めで未払いを作る。
- validation `p16-dismiss`: `tools/run_p16_dismiss_tests.py`。`tools/run_p07_weather_tests.py` から、名前・名簿・再開の段・handoff・protocol 名だけを明示の置き換えで変えた(段の手順・厳密な照合・ordinary と old_bounded の分類は同じ)。実 12 段(新しい suite 1 + 既存 9 + 再開 2)と import・parse。
- suite-policy と marker contract に新しい3本の行を足し、今の source の SHA-256 で固定した。
- 既存の試験の期待値の変更は `EXPECTATION_CHANGES.json`(kitchen の旧セーブの写しに退職金の口座を足す1件)。
## 8. 範囲の外として残すもの(主張しない)
- まだ始まっていない(待ち行列の)出来事の actor の ID は、拘束を持たない。始まる時に、今の規則(`start_next_event` の欠けた actor の扱い)で扱われる。命令の中では変えない。
- 進行中の襲撃の防衛者の解雇(R9)は、原本の要件として未達のまま(SPEC §6.3・§8)。
- 解雇した職員が歩いて出る描写は作らない(SPEC §10)。UI は今の部品のまま。Windows の実機・IME・OS の倍率・同じ候補の実機の受入は後。
- 原単位 P16-01・P16-03 の残りは未達のまま(SPEC §8): 求人募集、奴隷市場・破産客からの獲得、人身売買、勤務・休息・気絶・救護の横断、解雇の経路の外の退職時の参照の除去。
@@ -0,0 +1,177 @@
{
"by": "Claude Opus 5.5 Desktop (local_a7f85421-3eed-4482-8030-c0dca669573a), the commander that wrote SPEC.md r01; before registration; read only",
"unit": "P24-02-EVLOG",
"auxiliary_of": "P24-02",
"original_credit": 0,
"original_completed": 24,
"original_total": 88,
"packages_completed": 4,
"packages_total": 30,
"spec": {
"path": "docs/plans/original-completion-2026-10-02/execution/p24-02-evlog/SPEC.md",
"sha256": "a08bf2455cbfe01433655544edc578de7eb67bc17eaa313d3dac891b6b5ef206",
"bytes": 10654
},
"main_read": "50207e9e6e41e361dab5397ecdff8417b98b5dfc",
"human_decision_required": false,
"human_decision_note": "No new game rule, value or human decision. The two Japanese titles are a presentation choice of the implementer (to be recorded at implementation); the weather words reuse the existing HUD words. Not a new human approval.",
"original_text": {
"path": "docs/traceability/original/BASELINE.txt",
"sha256": "75e4bdbdeb379c9269553f0c776bdb9cc96e01f2d6aa5ca650d132c63a8aeb1a",
"lines": {
"223": "## 11.4 建設・通知UX",
"224": " * **建設:** グリッドスナップ、接続ライン警告表示。",
"225": " * **通知:** ユニット頭上のフキダシ(Zzz、怒り、ハート)、イベントログ。",
"226": " * **カットイン:** 「NTR侵食開始」などの重要警告、決闘発生、防衛戦開始演出。"
},
"cited": {
"225": " * **通知:** ユニット頭上のフキダシ(Zzz、怒り、ハート)、イベントログ。"
},
"requirement_group": "B-11.4"
},
"original_package": {
"id": "P24",
"title": "インスペクター・通知・操作完成",
"requirement_ids": [
"B-11.1",
"B-11.2",
"B-11.3",
"B-11.4",
"B-13.4"
],
"work": "シフトのドラッグ配置、欲求/思考ログ、感情/盾、料金内訳、稼働阻害理由、接続線、フキダシ、カットインを接続。クリックによる代替操作も用意。",
"acceptance": "無効ドロップで状態を壊さず理由を表示。保存後も対象選択を回復。ログの説明と実際の選択が一致。モーダル/停止/再開の入力を検証。",
"depends_on": [
"P09",
"P15",
"P16",
"P18",
"P23",
"P19"
],
"decisions": [
"D09"
],
"state": "in_progress",
"cross_cutting_acceptance": [
"原本条件とUI操作が一致",
"Runtimeの実利用・会計・行動へ接続",
"途中保存・再起動で再現",
"異常/境界/中断を検証",
"関連回帰PASS",
"要件別ログとソースハッシュを保存",
"Windows部分実機確認。不可なら未検証を残す"
],
"cross_cutting_acceptance_scope": "Completion requirements only, not achieved results. Apply each to relevant slices; non-applicable views require a recorded reason.",
"package_dependencies_scope": "integration prerequisites; a vertical slice may start once its own required contracts are accepted"
},
"original_unit": {
"id": "P24-02",
"title": "操作と通知",
"work": "配置、無効ドロップ、モーダル、停止、再開、フキダシ、ログ、カットインを整える。",
"acceptance": "無効操作を説明し、クリック代替と日本語入力・表示倍率を確認する。",
"state": "in_progress",
"remaining_gap_event_log": "event_log: Current native event-log suite passed. Real production sequence, Japanese amounts/time/reasons and pause/load nonduplication remain; legacy evidence stays original-version unless applicability proven."
},
"known_residual_closed_by_this_unit": {
"from": "P07-01 residuals_outside_unit_acceptance",
"text": "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"
},
"cross_cutting_application": {
"原本条件とUI操作が一致": "F2-F4 and F7: the event log rows of the existing published facts read in Japanese, opened and selected by viewport input in the real Main",
"Runtimeの実利用・会計・行動へ接続": "F2-F4: rows of the real 06:00 weather draw, real warp travel (with the mana fare) and real elevator trips; presentation only, the Runtime is not changed (F5)",
"途中保存・再起動で再現": "F6: same-process and separate-process loads give the same rows once",
"異常/境界/中断を検証": "F1 unknown-kind fallback kept, F4 unresolvable-target fallback kept, F5 no state change",
"関連回帰PASS": "F8: narrow roster, restart pair and one final S full roster",
"要件別ログとソースハッシュを保存": "registered validation p24-evlog with source-hashed suites and markers (SPEC section 9)",
"Windows部分実機確認。不可なら未検証を残す": "not performed in this unit; the same Windows device, IME and OS scaling stay unverified and are disclosed (SPEC section 10)"
},
"U1": "Disclosure of what stays unmet: SPEC sections 8 and 10 (the rest of P24-02, the P24 package, Windows/IME/OS scaling)",
"U2": "Evidence applicability: all measurements on the submitted S; local diagnostics and earlier evidence are not formal evidence",
"accepted_upstream": [
{
"what": "domain event stream and the event log dialog",
"paths": [
"scripts/core/domain_event_stream.gd",
"scripts/ui/event_log_dialog.gd"
],
"existing_suite": "tests/ui/test_event_log_ui.gd (in the full roster)"
},
{
"what": "natural weather and the 06:00 forecast draw (WEATHER_FORECAST_DRAWN)",
"runner": {
"project": "the-tree-p07-01-wx",
"task": [
"P07-01-WX",
"completed",
"db87270972c9b243e3a1fccdf2c1fa96975dffc1",
"9b5b09b35e888e5ac1a49174b8a7f73e51c502d2",
2
]
},
"ledger": "P07-01-WX auxiliary formally completed, Q 9b5b09b"
},
{
"what": "warp travel and fare (WARP_TRAVELED) and elevator trips",
"ledger": {
"P14_package_state": "completed",
"units": {
"P14-01": "completed",
"P14-02": "completed",
"P14-03": "completed"
}
}
}
],
"upstream_not_needed": "The P24 package dependencies (P09, P15, P16, P18, P23, P19) are for the inspector, thought logs, bubbles and cut-ins; this unit only presents facts the product already publishes, so it registers only the range whose upstream is accepted (package_dependencies_scope).",
"held_not_in_scope": [
"The whole original P24-02 and the P24 package (placement and invalid drop, modal and pause/resume, bubbles, cut-ins, click alternatives, Japanese input and scale, the failure reasons in the event log)",
"Important-warning cut-ins of P18/P19/P21 wait for their natural occurrence",
"IME, OS scaling and the same Windows device conditions stay unmet",
{
"earlier_runner_project_not_resumed_or_merged": "the-tree-p24-02-operations",
"task": [
"P24-02",
"submitted",
"fe44f550750acbf5c510346e7cacbea2ccbdd184",
null,
6
]
},
"P28 speed and appearance work and the P16-01-DISMISS low 5 reinforcement are not in this unit"
],
"relation_to_P16_01_RECRUIT": "Recruitment waits for the human answer and is unregistered and unimplemented. One implementer, no concurrent implementation. The allowed paths can overlap in the future (scenes/main/main.gd, tests/**, suite-policy, marker contract), so whichever is registered later takes the latest main as its base.",
"source_pins_note": "SPEC.md says the read sources are in SOURCE_PINS.json in the same folder; that file is in the registration folder artifacts/original-completion-2026-10-02/p24-02-evlog-registration-r01/, and its content is copied here.",
"source_pins": {
"file_sha256": "6c85d349832402902aca0086a5b54941a15adcc26825ca1c56a3467fb2290522",
"main": "50207e9e6e41e361dab5397ecdff8417b98b5dfc",
"read_only": true,
"files": {
"docs/traceability/original/BASELINE.txt": "75e4bdbdeb379c9269553f0c776bdb9cc96e01f2d6aa5ca650d132c63a8aeb1a",
"docs/plans/original-completion-2026-10-02/work_packages.json": "dfd1bb4adf29178379bdd189bcf5cea9c7906bbd1aa59dd5e4136da7398ab38f",
"docs/plans/original-completion-2026-10-02/atomic_requirements.json": "44530bb929fcc8c68c06d0d94da66dd04b8db7680af731b5037cd44677502938",
"scripts/ui/event_log_dialog.gd": "62000f559c49c3a58b244b991c10c1738add10702bfa66cb7cb40798cb559290",
"scenes/main/main.gd": "0fded7badf0a125fb04c6b98488f8a590312765c199daf5920b84849aecda3ef",
"autoload/simulation_runtime.gd": "f1135eeea519a3f1146605fc2b471e79658043d90da5d6760fdee6d095d6e391",
"scripts/core/domain_event_stream.gd": "f9b57a4b4b53cf7d67132a159ea90378c633b4f19d8de28dbe7a33ebce8e3d79",
"scripts/core/domain_event.gd": "cc9a903c366d9f889134561694b0c7fd52da260e97236a7b60830d7a68947bd4",
"scripts/transport/warp_service.gd": "7602c6b8a9672a703bfa98a1424c2cf68cf50184f87f72349d564af6a5ca9d41",
"scripts/transport/elevator_service.gd": "6f2b9cac49970b80fbfb0f71da38dee3431d78df3d361c9233e763e1bf6a8cd4",
"scripts/environment/natural_weather_policy.gd": "7b5ff916554bc63497d55b2008d34a7a658db53b642fcd1645a4a9f353b7729e",
"tests/ui/test_event_log_ui.gd": "9b1d7cb112790bb58969338ab1dc0bbe2a1c26b4307c4bfcd37491cf950c557b",
"tests/transport/test_runtime_warp.gd": "cfbb69e63aca12d9d198393fd853c76be63df95e8d1e777e7a14194dbd181234",
"tests/environment/test_p07_weather_runtime.gd": "e34b52ba3511f7391de1435c2f907997dd10235742ebc029831808af969c68e0",
"tools/regression/suite-policy.json": "bc0539529f53fd6e5f9ad62ed83d7b75c537243a7d65781e37b9f2ac0b8125f0",
"tools/run_p16_dismiss_tests.py": "39ef80e42feabc9841b64e7e35b0855626e7a7e8b868466d0a14e56ba8ccf2a5"
},
"blob_equals_worktree": true
},
"registration_material_sha256": {
"acceptance_baseline.json": "6d02aea176e5c37c9abfec973dfc0043a954382091292df9c781860d9cd5ad38",
"validations.json": "e7fc20221c7727888ba9b7fb1eb1df4f571107ad849c037f8db591c7eee14a22",
"policy.json": "4f2536fa12ff29fc085d63645a4fe46e40bcce9b997fef708d1a688fe510d653",
"task_contract.template.json": "9959af23862cb341fa28ed6721046fa9794bfb1bcae3a1e513bf92dc3b82bb24",
"REGISTRATION_STEPS.md": "35d89c88b55819970841c745e059c57537edb9e7776eee7dbbbd8ebc42e37f08"
},
"not_done": "No product change, no registration, no commander RUN. Root does init/add/prepare and the delegation; the implementer records its RUN afterwards."
}
@@ -0,0 +1,85 @@
# P24-02-EVLOG 出来事の一覧で読めない記録をなくす 仕様(登録の前の版 r01。P24-02 の補助の作業)
- 状態: 登録の前の仕様(実 Opus の commander、2026-10-25)。人の新しい決定は要らない。Root が main に `docs/plans/original-completion-2026-10-02/execution/p24-02-evlog/SPEC.md` として固定し、その commit を `base_commit` に通常の登録をする。製品は登録まで変えない。読んだ source は同じフォルダの `SOURCE_PINS.json`(main `50207e9`)。
- 原本への加算: 0。原単位 P24-02 の原 work・原 AC は、この作業では閉じない(§8)。
- 表示だけの作業: Runtime の状態・記録の流れ・保存の形は変えない。変えるのは、出来事の一覧の画面の文の組み立てだけ。
## 1. 原本の条件(原文。変えない)
- `docs/traceability/original/BASELINE.txt` 223〜225 行(11.4 建設・通知UX): 225 行 `* **通知:** ユニット頭上のフキダシ(Zzz、怒り、ハート)、イベントログ。`
- package P24「インスペクター・通知・操作完成」の acceptance の一部: 「ログの説明と実際の選択が一致。」
- 原単位 P24-02「操作と通知」の work「配置、無効ドロップ、モーダル、停止、再開、フキダシ、ログ、カットインを整える。」と acceptance「無効操作を説明し、クリック代替と日本語入力・表示倍率を確認する。」の、「ログ」の部分。
- 台帳の P24-02 の残件 `event_log`: 「実際の記録の流れ、日本語の金額・時刻・理由、停止と読み込みで重複しないこと」が残る。
- 既知の残件: P07-01-WX の残件「出来事の一覧で毎日の `WEATHER_FORECAST_DRAWN` が『未識別の記録』と出る」(`work_packages.json` の P07-01 の `residuals_outside_unit_acceptance`)。
## 2. 今の製品の欠落(main `50207e9`、読むだけで確かめた)
- `scripts/ui/event_log_dialog.gd` の `EVENT_NAMES`(88 種類)に無い種類は、題が「未識別の記録」になる。製品の source が出す種類のうち、名前が無いのは次の2つ。
- `WEATHER_FORECAST_DRAWN`(毎朝 06:00 の天候の抽選。`autoload/simulation_runtime.gd` の天候の抽選の箇所。対象の ID は無い)
- `WARP_TRAVELED`(ワープの移動と料金。`_charge_warp_fare`。対象は connector の ID と乗った人の経路の actor の ID)
- 一覧の文は、記録の中身(payload)を出さない。そのため、予報の行は「未識別の記録 — 対象の指定なし」とだけ出て、天候と時刻が分からない。
- 対象の名前の解決(`_entity_name`)は、候補・施設・来客の ID だけを知る。次の ID の形は「名称を確認できない対象」になる。
- 経路の actor の ID(`staff:<職員の ID>`・`guest:<来客の ID>`): ワープと昇降機の記録の対象
- ワープの connector の ID(`warp.connector:<施設の ID>|<施設の ID>`)
- 昇降機の shaft の ID(`transport.shaft.<層>.<x>.<y>.<幅>`)
- 種類の全数の取り方(実装の時に試験で固める):
- `SimulationDomainEvent.new(` の2つ目の引数の文字の種類
- `_publish_actor_event(` の1つ目の引数の文字の種類
- `_complete_save_operation(` の2つ目の引数の文字の種類
- 変数で種類を渡す2か所(保存の終わり・検査の到着と遮断)の式の中の文字の種類
- main `50207e9` では、これで出す種類の全てが `EVENT_NAMES` の 88 種類と上の2つに入る。
## 3. 挙動(この作業で作るもの)
1. 名前: `WEATHER_FORECAST_DRAWN` と `WARP_TRAVELED` に、日本語の題を足す(例「その日の天候が決まった」「ワープで移動」。文言は実装で決めて記録する)。未知の種類の「未識別の記録」の守りは残す。
2. 予報の行: 天候の名前を、上部の表示と同じ語(晴れ・雨・嵐・マナ霧)で出す。雨などは始まりと終わりの時刻(`HH:MM〜HH:MM`)を、晴れは変化が無いことを出す。語は1か所にまとめ、上部の表示と一覧で同じものを使う。
3. ワープの行: 出発と到着の施設の名前、乗った人の名前と人数、払ったマナ(payload の `mana`)を出す。
4. 対象の名前: 経路の actor の ID は、職員の名前(候補の表示の名前)または「来客」に、ワープの connector は両端の施設の名前に、昇降機の shaft は、その位置の昇降機の施設の名前に解決する。名前を確認できない対象は、今の守り(「名称を確認できない対象(n件)」)のまま。
5. 一覧を開く・読む・選ぶ・文を組み立てることは、Runtime の状態を変えない(今と同じ)。
## 4. 変えないもの
- Runtime の記録の流れ(`DomainEventStream`)、記録の種類・中身・順序、保存の形と照合。
- 他の画面(上部の天候の表示の文は、語を共有するだけで変えない)。
- 未知の種類と未知の対象の守り。
## 5. 比べ方
- 画面の操作と文の組み立ての前後で、Runtime の snapshot の全体が bytes で同じ(今の `tests/ui/test_event_log_ui.gd` と同じ比べ方)。
## 6. 受入条件
- P24-02-EVLOG-F1 名前の全数: §2 の取り方で集めた、製品の source が出す全ての記録の種類に、日本語の題がある。新しい種類が題なしで足されると、試験が止まる。未知の種類は「未識別の記録」のまま。
- P24-02-EVLOG-F2 天候の予報: 本物の Runtime の毎朝 06:00 の抽選の行が、上部の表示と同じ語の天候の名前と、時刻(雨・嵐・マナ霧は始まり〜終わり、晴れは変化なし)を出す。4つの天候の全てで確かめる。
- P24-02-EVLOG-F3 ワープ: 本物のワープの移動の行が、出発と到着の施設の名前、乗った人の名前と人数、払ったマナを出す。客の移動と、職員の移動(救護の2人を含む)で確かめる。
- P24-02-EVLOG-F4 対象の名前: 経路の actor の ID(職員・来客)、ワープの connector、昇降機の shaft を、今の名前に解決する。昇降機の記録の行(依頼・乗車・到着・救護の依頼)でも確かめる。名前を確認できない対象は今の守りのまま。
- P24-02-EVLOG-F5 状態を変えない: 一覧を開く・読む・選ぶ・文を組み立てる前後で、Runtime の snapshot の全体が bytes で同じ。
- P24-02-EVLOG-F6 保存と読み込み: 予報とワープの行を含む保存を、同じ process と別の process で読むと、行は重複せず、同じ文で出る(読み込みの記録の1行が足されるだけ)。
- P24-02-EVLOG-F7 通常の画面: ゲームの本物の Runtime と Main で、viewport の入力(`root.push_input`。キーとボタンの押しと離しを local の座標で送る)で出来事の一覧を開き、予報とワープの行を選ぶと、その文が一覧と詳細に出る。OS の入力の受入は主張しない。
- P24-02-EVLOG-F8 回帰: 関連の回帰(狭い roster と再開の2段)と最終の S の全 roster が PASS。既存の試験の期待値の変更は `EXPECTATION_CHANGES.json` に変更前後と根拠を一覧にする。
## 7. 実装の範囲(allowed_paths)
- `scripts/ui/event_log_dialog.gd`(題・文・対象の名前の解決)
- `scenes/main/main.gd`(上部の天候の語を共有する場合だけ。上部の表示の文は変えない)
- `tests/**`、`tools/run_p24_evlog_tests.py`、`tools/regression/suite-policy.json`
- `docs/plans/original-completion-2026-10-02/execution/p24-02-evlog/**`、`docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json`
- `autoload/simulation_runtime.gd` と他の製品の file は変えない。新しい `.gd` の file を足すなら、import が作る `.uid` も同じ範囲(`tests/**`)の中に限る。製品の側に新しい file が要るなら、登録の前に Root と範囲を見直す。
## 8. 原単位との差(開示)
- P24-02 の残りは未達のまま: 配置・無効ドロップ・モーダル・停止と再開・フキダシ・カットイン・クリックの代替・日本語の入力と表示の倍率、`event_log` のうち「理由」(操作の失敗の理由を日本語で出す)と、長い通常の記録の流れ。
- P24 の package の上流(P09・P15・P16・P18・P19・P23)は、インスペクター・思考の記録・フキダシ・カットインなどに要る。この作業は、今の製品が出す記録の表示だけを扱うので、その上流を要らない。P24 の package の完了には数えない。
- P07-01-WX の残件「予報が未識別の記録と出る」は、この作業の受入で解消する。P07-01 の原単位は、この作業では変わらない。
## 9. 検証
- validation `p24-evlog`(`tools/run_p24_evlog_tests.py --output {build_dir}/p24-evlog-evidence`。`tools/run_p16_dismiss_tests.py` と同じ guarded の段の手順を、名前・名簿・再開の段・handoff・protocol 名の明示の置き換えで作る)。
- 狭い roster: 新しい suite `tests/ui/test_event_log_readable.gd`(F1〜F7)と、既存の `tests/ui/test_event_log_ui.gd`・`tests/transport/test_runtime_warp.gd`・`tests/environment/test_p07_weather_runtime.gd`・`tests/staff/test_rescue_elevator.gd`・`tests/s6/test_runtime_snapshot_serialization.gd`・`tests/s6/test_runtime_save_lifecycle.gd`。再開の2段 `tests/ui/test_event_log_readable_restart_1_write.gd`(予報とワープの行を含む保存。唯一の生成ファイルは handoff)と `..._2_read.gd`(別の process で読み、行と文が同じで重複しないことを確かめる)。
- その後: 正式の validation → 最終の S の全 roster を1回 → fresh な独立レビュー(この補助の作業の範囲。原単位の whole-unit の受入ではない)。速度の調整の作業と、微小な変更ごとの全回帰はしない。
## 10. 開示
- UI は今の部品のまま(完成の見た目ではない)。Windows 実機・IME・OS の倍率は後。
- 題の文言(§3 の 1)は実装の選択で、人の新しい承認ではない。天候の語は上部の表示の既存の語を使う。
- 試験の入力は viewport の入力で、OS の入力ではない。
@@ -222,7 +222,7 @@
"ASSETS_PLATFORM_AUDIT.md": "a3a4b8ca0fb39a9abf88692285d26aa879815e092352ba304ef2758e71da50bd",
"ATOMIC_REGISTER.md": "6070143b85b7c3b9b73d7c9768d88d2dae4d20407ad54a563cb6ffd74f2373ae",
"ATOMIC_SEMANTIC_REVIEW.md": "c156b1fb57c2b4a900f27a19049510f3e9616e723d5edb3417f50d733de5be48",
"IMPLEMENTATION_PLAN.md": "8f8c2666b4b0f0c0882355f5624cd5ffcdde13ebd582184158020d4ddf8852ee",
"IMPLEMENTATION_PLAN.md": "c458b0b40234940341455f2dc3fc3c1e628778895cfcb9043a1857f679816a86",
"IMPORT_UNICODE_AUDIT.md": "88b2382bfbb3411fece8be7e6004ba21d8bf6a47e9d1f3cf573ea6638f79c12f",
"P09_VALUE_CONTRACTS.md": "c5abdc4543e54033b1fcfd0943cba266f664d8c10968965fd36ade40da488a25",
"PERFORMANCE_AUDIT.md": "c75e806acf825fc4a61f773a2cce7e71be335a1e25dbb15d98091097e8dc47c1",
@@ -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": "9cc9c1502c04b13f4e0389ac2d8a8f263ede1d41f9c57beac2f889c15bbf7503",
"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": "dfd1bb4adf29178379bdd189bcf5cea9c7906bbd1aa59dd5e4136da7398ab38f"
}
}
@@ -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-25T18:51:22.217493+09:00",
"scope": "原30パッケージ/88成果、正式完了24成果・4パッケージ。部分点なし。P16-01-DISMISS補助(原加算0)を独立レビュー/通常final/実DB結合で正式完了し、原P16-01は求人募集・獲得・区分ごとの費用の残件でin_progress。ユーザー指示により機能実装の完了を優先し、P28-02の最適化と試験速度・見た目の調整は後段。全ゲーム/同一Windows最終受入は未完。",
"source_head": "4ca3bbef553259e111b35856785ae83d34380287",
"source_authority_note": "台帳更新直前main Q4ca3bbe(S f5fe219とtree同一)。P16-01-DISMISSはRunner completed 10/10・repairs2、validation/review/integration/final全PASS、final 32d1d379 SHA 91b6d220 verified true/engines_started0。別fresh Desktop Opus5.5がF1〜F10・§4〜§6・横断6・U1/U2 PASS(high/medium0・low5)。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": 19,
"planned": 45
},
"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": "P16-01-DISMISS補助(原加算0)正式完了。機能実装を優先し、次の機能残件の候補(原条文・上流条件・既存証拠・通常登録案)を同じ実OpusがRootへ渡す。新実装は登録後。P24は上流が揃うまで、P28の速度と見た目は後段で保留。",
"pending_decisions": "人への問いの回答待ち: P05-03の3件(推定しない)。天候(A13)と退職金(A14)は回答済みで再質問しない。",
"auxiliary_closed": "P16-01-DISMISS"
},
"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": "P16-01-DISMISS補助正式完了(原加算0、24/88)。次は機能残件の候補を同じ実OpusがRootへ渡し、Rootが通常登録。原P16-01は求人募集・獲得・費用区分の残件。P24は上流待ち、P28の速度と見た目は後段。",
"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-p16-01-dismiss",
"task": "P16-01-DISMISS",
"state": "completed",
"submission": "895491456477b42a74e59fa239bea86c4fb9aa33",
"candidate": "cb183fd12715bd0edc5f3b8a7fdff76bf1a8ec83",
"main_base": "613b9886dae31b2578f6aa13b81712b68ac694bf",
"submission": "f5fe2196740466a673879cb2950d96446fd99d6e",
"candidate": "4ca3bbef553259e111b35856785ae83d34380287",
"main_base": "0b59d7c23c301cf014343e217827cb2d837cc11c",
"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/p16-01-dismiss/DISMISS_FORMAL_COMPLETION_4CA3BBE.json",
"bytes": 49987,
"sha256": "d1799f166f340f9500f60333fbf7ba01681d926dd8c0fd4d655051c2966a4721"
},
"independent_reviewer_session": "local_cbdd640f-21ad-4f21-8d71-12b8debccc21",
"independent_result_sha256": "fb3e5be2a15214a449da41b8a496de1b2f18ecbb1e8482c68ecd45999b9ad9a5",
"independent_reviewer_session": "local_4e746265-c2a6-4576-a894-84cbb0b7fc46",
"independent_result_sha256": "1f530e6c4e6158a469271d287763fc8a573295f44f04dd26bf9b5e6a93f8b737",
"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": "the same actual Opus prepares the next feature-remaining candidate (original text, upstream conditions, existing evidence, normal registration draft) for Root; new implementation only after registration; P24 waits for its upstream; P28 speed/appearance deferred"
},
"historical_workflow_before_desktop_handoff_20261019": {
"source_thread": "01a14f6a-b6a1-7fd3-bd45-0f84bbef77be",
@@ -14311,5 +14312,166 @@
"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"
]
},
"historical_before_P16_01_DISMISS_formal_4ca3bbe": {
"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。",
"current_delivery_state_counts": {
"completed": 24,
"in_progress": 18,
"planned": 46
},
"current_closure_targets": {
"formally_closed": "P05-02",
"next_candidate": "P28-02",
"formal_count": 24,
"original_delivery_units": 88,
"partial_credit": false,
"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"
},
"current_workflow_handoff": {
"controller_local_session": "local_a7f85421-3eed-4482-8030-c0dca669573a",
"actual_model": "Opus5.5",
"project": "the-tree-p07-01-wx",
"task": "P07-01-WX",
"state": "completed",
"submission": "db87270972c9b243e3a1fccdf2c1fa96975dffc1",
"candidate": "9b5b09b35e888e5ac1a49174b8a7f73e51c502d2",
"main_base": "7ac2224e4705d06aab28803592a68b31d60841ed",
"evidence": {
"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_fc6760c6-9acd-4614-950e-632b91d53f54",
"independent_result_sha256": "1fd2c8bbe0f9d6b165d5f993e86002b94ead97674c27ed9270a223c929c1aaca",
"all4_normal_gates_passed": true,
"original_credit": 0,
"original_completed": 24,
"original_total": 88,
"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"
},
"next_closure_note": "P07-01-WX補助正式完了(原加算0、24/88)。次はP16-01-DISMISS r04をWX統合後のmainでRootが通常登録。原P07-01は炉の経路待ち。P28-02-OPTは後段(記録は保持)。"
},
"current_P16_01_DISMISS_auxiliary_completion": {
"state": "completed",
"S": "f5fe2196740466a673879cb2950d96446fd99d6e",
"Q": "4ca3bbef553259e111b35856785ae83d34380287",
"M": "0b59d7c23c301cf014343e217827cb2d837cc11c",
"all4_normal_gates_passed": true,
"proof": {
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/DISMISS_FORMAL_COMPLETION_4CA3BBE.json",
"bytes": 49987,
"sha256": "d1799f166f340f9500f60333fbf7ba01681d926dd8c0fd4d655051c2966a4721"
},
"independent_review": {
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/DISMISS_INDEPENDENT_REVIEW_F5FE219.json",
"bytes": 19385,
"sha256": "1f530e6c4e6158a469271d287763fc8a573295f44f04dd26bf9b5e6a93f8b737"
},
"acceptance_record": {
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/DISMISS_ACCEPTANCE_4CA3BBE.md",
"bytes": 10904,
"sha256": "a0298259c8e2adeaaca7ae4fe9bd61410199080e094e7fa83d230a2df5bd14c9"
},
"original_credit": 0,
"formal_original_completed": 24,
"original_total": 88,
"original_P16_01_state": "in_progress",
"original_P16_03_state": "planned",
"remaining_not_claimed": [
"Ordinary recruitment operation (AR.staff.recruitment), market and debt acquisition (AR.staff.market_acquisition, debt_acquisition, D04) and the cost arrangement per employment category",
"Original P16-01 whole-unit acceptance (recruitment to placement, settlement and departure by ordinary operation) after those paths exist",
"Dismissing the defender of an unfinished raid (R9 DEFENSE_LOCKED, a Root temporary boundary) stays unmet as an original requirement; no retreat or combat rule was made",
"Human trafficking (AR.staff.sale_*, D04) is not addressed",
"Review low: the patient-side result text of an interrupted rescue is not checked on screen",
"Review low: the panel guards against a stale or changed confirmation are not exercised by a test",
"Review low: the Event missing-actor policies REPLACE_WITH_ROLE and BRANCH_FAILURE are not tested at the dismissal boundary",
"Review low: the return value of _rollback_staff_dismissal is not used (the existing hire rollback form)",
"Record gaps kept (neither approved nor waived): the first a073ccb derive run bytes and the two stage-only UI diagnostic copies are not kept as files",
"The test input route is viewport input, not OS input; the same Windows device, IME, OS scaling and same-candidate device acceptance remain; the UI uses the current parts; long normal play and whole-game final acceptance remain"
]
}
}
@@ -5230,7 +5230,7 @@
},
"legacy_state": "in_progress",
"state_scope": "package-level; completed means whole package accepted; partial implementation is not completion",
"current_summary": "P05-01・P05-02は正式受入済み(P05-02はS8954914→Qcb183fd、独立Opus PASS、通常4ゲートPASS)。P05-03「金庫と銀行の通常営業」は未完(人への問い3件は回答待ち)で、package全体はin_progress。通常操作の裏金(back_gold)・退職金・UI low2・Windows/全ゲーム受入は未達を維持。",
"current_summary": "P05-01・P05-02は正式受入済み(P05-02はS8954914→Qcb183fd、独立Opus PASS、通常4ゲートPASS)。P05-03「金庫と銀行の通常営業」は未完(人への問い3件は回答待ち)で、package全体はin_progress。退職金はA14の回答とP16-01-DISMISS補助(Q 4ca3bbe、原加算0)で解雇の操作へ接続したが、P05の原work/ACの完了には数えない。通常操作の裏金(back_gold)・UI low2・Windows/全ゲーム受入は未達を維持。",
"remaining_deliverables": [
{
"id": "P05-01",
@@ -5667,7 +5667,7 @@
],
"progress_summary": "P05-02原work/acceptance/横断6を受入8件(F1〜F8)の別fresh Desktop Opus5.5独立レビューPASS(高/中0・low3)、通常validation/review/integration/final全PASSと実DB/traceability結合で正式完了。S8954914→Qcb183fd(tree同一)。測定はS(native10段867・全roster195/22539)、Qは中身同一の照合で測定と書かない。正式24/88・4/30、部分点なし。P05 package(P05-03)・通常操作の裏金(back_gold、P09/P11のremaining_paid_role)・UI low2・退職金・Windows/全ゲーム受入は未達を維持。",
"remaining_gaps": [],
"updated_at_jst": "2026-10-23T22:51:54.402572+09:00",
"updated_at_jst": "2026-10-25T18:51:22.217493+09:00",
"historical_progress_summary_before_P04_closure": "P05-02を現1299/source df8328eaで着手。原work/acceptanceと88成果単位を維持し、現全回帰の財務10試験655/0の本文・raw・実PID・marker・終了・private profileをRootと同1名が読取照合して再使用。現版は単一PLAYER_CASHで表金/裏金未接続。税/利息はaggregate0のみでproducer/category mapping未実装、D04本番条件と資金割当/旧Gold移行/支払規約の回答待ち。旧v5正式再開証拠は現版との差12refsにより自動転用しない。既存維持費/仕入れ/給与/元本契約は再使用可能だが全費目3営業日統合受入は未完。各費目を毎日非ゼロ/部分払/未払にする追加条件は設けない。既存確定費目の個別表示1ref候補を準備、本体未採用。製品変更0・重複全回帰0・正式完了追加0。Windows実機で本単位全体をブロックしない。 表示候補UI1refは本体未採用のまま、現1299に候補SHA19145a61を限定適用した隔離stageでparse・公開関連2suite142/0を確認。表示専用fixture31/0は異なる4費目、旧summary欠損key、日選択/refresh/最新への復帰を確認し4表示操作の全Runtime varbytes前後一致。値は本番税利息政策/実producer/3営業日証拠へ換算しない。通常OS Main PID24687で受付500G/資材8建設、通常completion、00:00維持費発生2G/支払2G/未払0/利益-2/手持4498を実観測。1280x720の借入なし1施設1決算で表/裏の4費目・施設別表示が可視。1461 paused front→back→frontは表示層active_layerの一byte0→1以外全literal一致、表への復帰全一致、通常close前後全一致。externalOStraceはRootの実CUA呼出忠実転記、独立hardwarecaptureではない。先行QA不備の負記録保持。同1名が有限証拠を独立受入、借入あり長表示/スクロールの有限確認は後段を参照、統合最終版全回帰は未実行。製品変更0/重複全回帰0/正式完了追加0、未確定資金規約・D04/全費目3日受入を保持。 同一表示候補SHA19145a61の隔離stageで、既存公開APIを用いた明示fixtureの借入元本1000G/返済400Gと有償倉庫2施設を準備し、実際の2締めを経て正式4member保存。native PID28624 14/0、通常OS Main PID29022でmanual_ui読込、日付選択、元本の折返し、4費目、施設別2行、実帳簿スクロール、返済100G未送信入力、上端復帰を確認。現金4692G/残元本600G、第1日維持費2G/利益-402G、第2日維持費6G/利益-6G/前日比396G。読込後7checkpointとpreclose/close全9varが600568bytesでliteral一致。実PNGとpure observer/strict native/正式保存handoff不変を同1名が独立レビュー。fixtureの元本額は本番融資条件ではなく、自然借入・税利息政策/producer・表裏資金・全費目3日受入へ換算しない。製品変更0/候補未採用/追加全回帰0/正式完了追加0。",
"historical_before_formal_cb183fd": {
"state": "in_progress",
@@ -5717,6 +5717,15 @@
"original_credit": 1
},
"residuals_outside_unit_acceptance": [
"P05 package stays in_progress: P05-03 planned; its three human questions are pending and no answer is inferred",
"Atomic back_gold through ordinary play not achieved: the F1 back-facility attendant is registered by register_staff because normal Main cannot hire service_attendant (P09/P11 remaining_paid_role, 2026-10-08)",
"Casino, extortion and unit-sale revenue sources absent (SPEC section 7 disclosure); fines and events pay from front funds only, disclosed without a dedicated test",
"Review low: supply panel mixes front-only on-hand with a front+back after-purchase figure (outside allowed_paths); deferred under the feature-first instruction",
"Review low: ledger panel labels front funds as on-hand; the ledger UI is the current mock to be replaced; deferred under the feature-first instruction",
"AR.staff.dismissal_severance (P05, D04): the human severance answer A14 was given and the dismissal severance is connected by the auxiliary P16-01-DISMISS (Q 4ca3bbe); it is not counted as the completion of the P05 package work/AC",
"Same Windows distribution device acceptance, long normal play and whole-game final acceptance remain"
],
"historical_residuals_before_P16_01_DISMISS_formal_4ca3bbe": [
"P05 package stays in_progress: P05-03 planned; its three human questions are pending and no answer is inferred",
"Atomic back_gold through ordinary play not achieved: the F1 back-facility attendant is registered by register_staff because normal Main cannot hire service_attendant (P09/P11 remaining_paid_role, 2026-10-08)",
"Casino, extortion and unit-sale revenue sources absent (SPEC section 7 disclosure); fines and events pay from front funds only, disclosed without a dedicated test",
@@ -5753,7 +5762,7 @@
],
"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",
"updated_at_jst": "2026-10-23T22:51:54.402572+09:00",
"updated_at_jst": "2026-10-25T18:51:22.217493+09:00",
"execution_progress": {
"current1285_candidate_checkpoint": {
"at_jst": "2026-10-07T23:54:31.996054+09:00",
@@ -6318,7 +6327,8 @@
},
"historical_current_summary_before_P05_01_reaudit_r01": "有料自動仕入れの既採用範囲を保持。読み取りコールバックが契約を破った場合の維持費2状態の復元漏れを補強し、同4case132/0と合成全165/13368で限定採用。全費目・税・借入・自然脅威・全P05は未完。 ミミ配置の新購入quoteと通常仕入れ入力の247機能+298parseを候補で限定照合。本体への今回反映、全費目/自然営業は未受入。 最新:検証済み18 source+outside1の計19 refsを本体source1285へ採用済み。全170/13961/0の実行ソースと全SHA一致。原本全受入は未完。 最新: 元本決算分類の現本体10/1失敗を再現。候補979/0・28恒久回帰を独立照合、本体採用0・全回帰/別PID保存未受入。全P05未完。 最新: 元本分類の再現1原因を修正。集中979/0、全172/14516/0を独立照合後4refs採用・28恒久回帰追加。この再現残件0、正式保存再開・本番融資操作は未受入、全P05未完。 最新: 元本4ケース・8正式保存を別PID再開し1825項目/0。全状態/通知/元本台帳・6再送noopを限定受入。本番銀行操作と全P05は未完。 最新: 既存負債の元本返済APIの欠落を修正。集中1064/0、全172/14540/0を独立照合後2refs採用・24恒久回帰追加。この再現残件0、新APIの正式保存再開・本番銀行操作は未受入、全P05未完。 最新: 元本返済API4ケース・8正式保存を別PID再開し1825項目/0。全状態/通知/元本台帳・6再送noopを限定受入。元本返済APIは限定受入。本番銀行操作と全P05は未完。 最新: 既存負債の返済を通常コマンドへ接続。集中1113/0・全172/14567/0を独立照合後2refs採用・27恒久回帰追加。この再現残件0、新コマンドの正式保存再開と本番銀行UIは未受入、全P05未完。 最新: 通常返済commandの5ケース・10正式保存を別PID再開2281/0で限定受入。原本3.4と9.2に対する元本返済経費の除外差異1を実Writer6日で確認し未修正。決算式・赤字判定と旧v4移行を通常UIより先に修正する。全P05未完。 最新: 実Runtime36項目/10失敗で元本返済経費の除外、3連続赤字の未発生、終了未移行、時間/操作の継続を再現。既知原因1未修正。原本決算と旧v4移行を通常UIより先に修正する。 最新: 原本の返済経費・3連続赤字・終了を集中1108/0、実旧v4の10正式保存移行と旧赤字回数の単体境界で検証。全172/14580/0後5refs採用・13恒久回帰追加。この既知原因残件0。Ops保存v5、新v5の別PID保存再開は未受入、全P05未完。 最新: 新v5の12正式保存・48memberを別PID再開2552/0で限定受入。実旧v4からの混在6日履歴、返済経費・3連続赤字・終了、10組の全状態不変を確認。全P05未完。 最新: 既存元本返済UIの集中535/0・全172/14626/0を照合し6参照を採用。融資条件・OS・Windows・全P05は未受入。 最新: 通常Mainの元本返済から原本3連続赤字終了まで進め、別PIDの2正式保存、15全状態、3終了後全状態不変を71/0で限定受入。本体変更0、全P05は未完。",
"historical_current_summary_before_P04_closure": "P05-01「自動仕入れ候補の受入」は原7条件と適用横断6条件を同1名が全受入。原work1394→1396は履歴として一字不変、現採用1299/source df8328eaへ証拠を拘束。見積・数量・通常単価・ミミ20%割引/解除後価格・凍結領収書・配送・通常UIの現実装を再使用。通常OS有料倉庫、手動4/12G、自動不足6/18G、draftと受理済設定、確定済/pending/funds/quarantine/Mimiの正式別PID読込を確認。実有料cargo8の549分保存をWriter8413/Reader9945で全typed再開、5/40/120分の全文一致、554分に一回配送完了、550分の正当な不足8/24G補充一回を保持。返送・停止/復帰・破壊・隔離・再送を現full rawと不足局所証拠へ対応。局所電力は導出flag同分gateと解除後fulltickであり自然持続停電ではない。負の検証履歴を保持し、未説明状態差0、正式4members/圧縮payload/全生証拠を照合。製品編集0・重複全回帰0、現版必須全回帰を再利用。本成果単位の不足0、正式完了7件。部分点なし、全ゲーム/原子要件/完成素材/全人物/Windows受入へ拡張しない。 P05-02「全費目と資金区分」、P05-03「金庫と銀行の通常営業」は未完。P05 package全体はin_progressを維持。 P05-02を現1299/source df8328eaで着手。原work/acceptanceと88成果単位を維持し、現全回帰の財務10試験655/0の本文・raw・実PID・marker・終了・private profileをRootと同1名が読取照合して再使用。現版は単一PLAYER_CASHで表金/裏金未接続。税/利息はaggregate0のみでproducer/category mapping未実装、D04本番条件と資金割当/旧Gold移行/支払規約の回答待ち。旧v5正式再開証拠は現版との差12refsにより自動転用しない。既存維持費/仕入れ/給与/元本契約は再使用可能だが全費目3営業日統合受入は未完。各費目を毎日非ゼロ/部分払/未払にする追加条件は設けない。既存確定費目の個別表示1ref候補を準備、本体未採用。製品変更0・重複全回帰0・正式完了追加0。Windows実機で本単位全体をブロックしない。 表示候補UI1refは本体未採用のまま、現1299に候補SHA19145a61を限定適用した隔離stageでparse・公開関連2suite142/0を確認。表示専用fixture31/0は異なる4費目、旧summary欠損key、日選択/refresh/最新への復帰を確認し4表示操作の全Runtime varbytes前後一致。値は本番税利息政策/実producer/3営業日証拠へ換算しない。通常OS Main PID24687で受付500G/資材8建設、通常completion、00:00維持費発生2G/支払2G/未払0/利益-2/手持4498を実観測。1280x720の借入なし1施設1決算で表/裏の4費目・施設別表示が可視。1461 paused front→back→frontは表示層active_layerの一byte0→1以外全literal一致、表への復帰全一致、通常close前後全一致。externalOStraceはRootの実CUA呼出忠実転記、独立hardwarecaptureではない。先行QA不備の負記録保持。同1名が有限証拠を独立受入、借入あり長表示/スクロールの有限確認は後段を参照、統合最終版全回帰は未実行。製品変更0/重複全回帰0/正式完了追加0、未確定資金規約・D04/全費目3日受入を保持。 同一表示候補SHA19145a61の隔離stageで、既存公開APIを用いた明示fixtureの借入元本1000G/返済400Gと有償倉庫2施設を準備し、実際の2締めを経て正式4member保存。native PID28624 14/0、通常OS Main PID29022でmanual_ui読込、日付選択、元本の折返し、4費目、施設別2行、実帳簿スクロール、返済100G未送信入力、上端復帰を確認。現金4692G/残元本600G、第1日維持費2G/利益-402G、第2日維持費6G/利益-6G/前日比396G。読込後7checkpointとpreclose/close全9varが600568bytesでliteral一致。実PNGとpure observer/strict native/正式保存handoff不変を同1名が独立レビュー。fixtureの元本額は本番融資条件ではなく、自然借入・税利息政策/producer・表裏資金・全費目3日受入へ換算しない。製品変更0/候補未採用/追加全回帰0/正式完了追加0。",
"historical_current_summary_before_P05_02_formal_cb183fd": "P05-01は正式受入済み、P05-02/03は未完。P04は通常候補b1810ff4で閉鎖し、原30/88の正式完了は17。次候補P05-02では全費目・表裏資金・3営業日照合・元本売上外を維持し、D04/資金配分・保存移行・支払規約の未確定条件と既存証拠の現版適用を実Opusが再監査して選定する。旧1299時点の候補・通常OS証拠は履歴として保全し、未検証の現版へ自動転用しない。新規実装は未起動。"
"historical_current_summary_before_P05_02_formal_cb183fd": "P05-01は正式受入済み、P05-02/03は未完。P04は通常候補b1810ff4で閉鎖し、原30/88の正式完了は17。次候補P05-02では全費目・表裏資金・3営業日照合・元本売上外を維持し、D04/資金配分・保存移行・支払規約の未確定条件と既存証拠の現版適用を実Opusが再監査して選定する。旧1299時点の候補・通常OS証拠は履歴として保全し、未検証の現版へ自動転用しない。新規実装は未起動。",
"historical_current_summary_before_P16_01_DISMISS_formal_4ca3bbe": "P05-01・P05-02は正式受入済み(P05-02はS8954914→Qcb183fd、独立Opus PASS、通常4ゲートPASS)。P05-03「金庫と銀行の通常営業」は未完(人への問い3件は回答待ち)で、package全体はin_progress。通常操作の裏金(back_gold)・退職金・UI low2・Windows/全ゲーム受入は未達を維持。"
},
{
"id": "P06",
@@ -6882,15 +6892,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 +7032,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",
@@ -8645,15 +8735,98 @@
},
"legacy_state": "planned",
"state_scope": "package-level; completed means whole package accepted; partial implementation is not completion",
"current_summary": "一般求人・配置・清掃等の基盤あり。全雇用区分と特性・費用は残る。",
"current_summary": "P16-01-DISMISS補助(原加算0)を正式完了(S f5fe219→Q 4ca3bbe、独立Opus PASS、通常4ゲートPASS)。一般従業員の通常操作での解雇(退職金A14・未払い保持・拒否R1〜R9・命令境目の後始末)は通常営業とMainの職員画面へ接続済み。原P16-01は求人募集・市場/借金での獲得・雇用区分ごとの費用、P16-02の特性と全職種、P16-03の労務の横断は未達。一般求人・配置・清掃等の基盤は既存のまま。",
"remaining_deliverables": [
{
"id": "P16-01",
"title": "獲得と費用の区分",
"work": "求人、市場、原本の雇用区分ごとの費用と獲得・退職を非描写状態として整理する。",
"acceptance": "求人から配置・精算・退職まで通常操作でき、ユニーク禁止操作を維持する。",
"state": "planned",
"evidence": []
"state": "in_progress",
"evidence": [
{
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/DISMISS_FORMAL_COMPLETION_4CA3BBE.json",
"bytes": 49987,
"sha256": "d1799f166f340f9500f60333fbf7ba01681d926dd8c0fd4d655051c2966a4721",
"scope": "P16-01-DISMISS auxiliary ten conditions formally completed; original P16-01 remains incomplete (recruitment, market/debt acquisition, cost per category); original credit zero",
"linked_at_jst": "2026-10-25T18:51:22.217493+09:00"
},
{
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/DISMISS_INDEPENDENT_REVIEW_F5FE219.json",
"bytes": 19385,
"sha256": "1f530e6c4e6158a469271d287763fc8a573295f44f04dd26bf9b5e6a93f8b737",
"scope": "Actual fresh independent Opus result for S f5fe219 (byte copy), pass, high/medium 0, low 5",
"linked_at_jst": "2026-10-25T18:51:22.217493+09:00"
},
{
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/DISMISS_ACCEPTANCE_4CA3BBE.md",
"bytes": 10904,
"sha256": "a0298259c8e2adeaaca7ae4fe9bd61410199080e094e7fa83d230a2df5bd14c9",
"scope": "Readable acceptance record of the DISMISS auxiliary completion with retained failures, record gaps and open residuals",
"linked_at_jst": "2026-10-25T18:51:22.217493+09:00"
}
],
"historical_before_P16_01_DISMISS_formal_4ca3bbe": {
"state": "planned",
"evidence_count": 0
},
"progress_summary": "P16-01-DISMISS補助(原加算0)を受入10件(F1〜F10)の別fresh Desktop Opus5.5独立レビューPASS(高/中0・low5)、通常validation/review/integration/final全PASSと実DB/traceability結合で正式完了。S f5fe219→Q 4ca3bbe(tree同一、修正2)。一般従業員の通常操作での解雇を、退職金(A14: 日給3日分、表金→不足は裏金)・未払い給与の保持・拒否R1〜R9・命令の境目の後始末とともに通常営業とMainの職員画面へ接続。測定はS(native12本1888・全roster201/24073)、Qは中身同一の照合で測定と書かない。原P16-01は求人募集・市場/借金での獲得(D04)・雇用区分ごとの費用が未達、in_progress。正式24/88・4/30、部分点なし。",
"remaining_gaps": [
"Ordinary recruitment operation (AR.staff.recruitment), market and debt acquisition (AR.staff.market_acquisition, debt_acquisition, D04) and the cost arrangement per employment category",
"Original P16-01 whole-unit acceptance (recruitment to placement, settlement and departure by ordinary operation) after those paths exist"
],
"updated_at_jst": "2026-10-25T18:51:22.217493+09:00",
"auxiliary_completions": [
{
"id": "P16-01-DISMISS",
"S": "f5fe2196740466a673879cb2950d96446fd99d6e",
"Q": "4ca3bbef553259e111b35856785ae83d34380287",
"M": "0b59d7c23c301cf014343e217827cb2d837cc11c",
"repairs": 2,
"all4_normal_gates_passed": true,
"acceptance_ids": [
"P16-01-DISMISS-F1",
"P16-01-DISMISS-F2",
"P16-01-DISMISS-F3",
"P16-01-DISMISS-F4",
"P16-01-DISMISS-F5",
"P16-01-DISMISS-F6",
"P16-01-DISMISS-F7",
"P16-01-DISMISS-F8",
"P16-01-DISMISS-F9",
"P16-01-DISMISS-F10"
],
"independent_review": {
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/DISMISS_INDEPENDENT_REVIEW_F5FE219.json",
"bytes": 19385,
"sha256": "1f530e6c4e6158a469271d287763fc8a573295f44f04dd26bf9b5e6a93f8b737"
},
"formal_proof": {
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/DISMISS_FORMAL_COMPLETION_4CA3BBE.json",
"bytes": 49987,
"sha256": "d1799f166f340f9500f60333fbf7ba01681d926dd8c0fd4d655051c2966a4721"
},
"acceptance_record": {
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/DISMISS_ACCEPTANCE_4CA3BBE.md",
"bytes": 10904,
"sha256": "a0298259c8e2adeaaca7ae4fe9bd61410199080e094e7fa83d230a2df5bd14c9"
},
"review_high_medium": 0,
"review_low": 5,
"original_credit": 0,
"completed_at_jst": "2026-10-25T18:51:22.217493+09:00"
}
],
"residuals_outside_unit_acceptance": [
"Dismissing the defender of an unfinished raid (R9 DEFENSE_LOCKED, a Root temporary boundary) stays unmet as an original requirement; no retreat or combat rule was made",
"Human trafficking (AR.staff.sale_*, D04) is not addressed",
"Review low: the patient-side result text of an interrupted rescue is not checked on screen",
"Review low: the panel guards against a stale or changed confirmation are not exercised by a test",
"Review low: the Event missing-actor policies REPLACE_WITH_ROLE and BRANCH_FAILURE are not tested at the dismissal boundary",
"Review low: the return value of _rollback_staff_dismissal is not used (the existing hire rollback form)",
"Record gaps kept (neither approved nor waived): the first a073ccb derive run bytes and the two stage-only UI diagnostic copies are not kept as files",
"The test input route is viewport input, not OS input; the same Windows device, IME, OS scaling and same-candidate device acceptance remain; the UI uses the current parts; long normal play and whole-game final acceptance remain"
]
},
{
"id": "P16-02",
@@ -8669,7 +8842,18 @@
"work": "勤務、休息、給与、気絶、救護、退職時参照除去を横断検証する。",
"acceptance": "人員上限、未払い、交代中の予約・移動が整合し、消失や二重精算がない。",
"state": "planned",
"evidence": []
"evidence": [],
"related_auxiliary_note": {
"auxiliary": "P16-01-DISMISS",
"proof": {
"path": "docs/plans/original-completion-2026-10-02/execution/p16-01-dismiss/DISMISS_FORMAL_COMPLETION_4CA3BBE.json",
"bytes": 49987,
"sha256": "d1799f166f340f9500f60333fbf7ba01681d926dd8c0fd4d655051c2966a4721"
},
"state_unchanged": "planned",
"noted_at_jst": "2026-10-25T18:51:22.217493+09:00",
"note": "The reference removal on the dismissal path (shift, schedule, assignment, path actor, elevator seat, rescue, Event lock, facility staffing) was accepted inside P16-01-DISMISS. The P16-03 work (cross-check of shift, rest, wages, collapse, rescue and reference removal on every departure) and its acceptance remain; nothing of P16-03 is claimed."
}
}
],
"estimate_person_days_historical_2026_09_29": [
@@ -8703,7 +8887,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_P16_01_DISMISS_formal_4ca3bbe": "一般求人・配置・清掃等の基盤あり。全雇用区分と特性・費用は残る。",
"updated_at_jst": "2026-10-25T18:51:22.217493+09:00"
},
{
"id": "P17",
+16
View File
@@ -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
+21 -2
View File
@@ -32,6 +32,13 @@ const LOAN_INTEREST_CATEGORY: StringName = &"LOAN_INTEREST"
const LOAN_INTEREST_PAYMENT_CATEGORY: StringName = &"LOAN_INTEREST_PAYMENT"
const NON_PROFIT_CATEGORIES: Array[StringName] = [TOP_UP_CATEGORY, RANK_TAX_PAYMENT_CATEGORY, LOAN_INTEREST_PAYMENT_CATEGORY]
## A14 (2026-10-23): dismissing a general employee pays three days of the effective daily
## wage, from the front fund with the hidden fund covering the rest. It is booked to its
## own expense account, apart from WAGE, and counted with the operating costs.
const SEVERANCE_EXPENSE: StringName = &"STAFF_SEVERANCE_EXPENSE"
const SEVERANCE_CATEGORY: StringName = &"STAFF_SEVERANCE"
const SEVERANCE_DAYS := 3
static func rank_tax(sales: int) -> int:
return maxi(sales, 0) / 100 * RANK_TAX_PERCENT + maxi(sales, 0) % 100 * RANK_TAX_PERCENT / 100
@@ -42,6 +49,10 @@ static func loan_interest(outstanding_principal: int) -> int:
+ maxi(outstanding_principal, 0) % 100 * LOAN_INTEREST_PERCENT_PER_DAY / 100
static func severance(daily_wage: int) -> int:
return maxi(daily_wage, 0) * SEVERANCE_DAYS
static func is_back_fund_service(tags: Array) -> bool:
for tag: StringName in BACK_FUND_TAGS:
if tag in tags:
@@ -66,12 +77,20 @@ static func prepare_accounts(economy: EconomyService) -> bool:
for account_id: StringName in expected:
if not economy.register_account(account_id, bool(expected[account_id])):
return false
return true
return _prepare_severance_account(economy)
for account_id: StringName in expected:
if not economy.has_account(account_id) \
or economy.account_allows_negative(account_id) != bool(expected[account_id]):
return false
return true
return _prepare_severance_account(economy)
## A save from before the severance gains its empty expense account the same way; the
## balances, the other accounts and the history are unchanged.
static func _prepare_severance_account(economy: EconomyService) -> bool:
if economy.has_account(SEVERANCE_EXPENSE):
return not economy.account_allows_negative(SEVERANCE_EXPENSE)
return economy.register_account(SEVERANCE_EXPENSE, false)
static func outstanding_principal(economy: EconomyService) -> int:
@@ -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
+169 -4
View File
@@ -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
+5 -1
View File
@@ -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"):
+20 -13
View File
@@ -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"]
+26 -1
View File
@@ -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
+223 -7
View File
@@ -9,12 +9,17 @@ signal status_requested(message: String, success: bool)
const ASSIGNMENT_COMMAND: StringName = &"set_staff_work_assignment"
const KEY_PREFIX := "ui.staff_assignment."
const DISMISSAL_COMMAND: StringName = &"dismiss_staff"
const DISMISSAL_KEY_PREFIX := "ui.staff_dismissal."
const ROLE_NAMES := {
&"attendant": "接客", &"bartender": "酒場", &"clerk": "会計",
&"cleaner": "清掃", &"cook": "調理", &"guard": "警備",
&"maintenance": "保守", &"masseur": "施術", &"medic": "医療",
&"porter": "物流", &"receptionist": "受付", &"innkeeper": "女将", &"cleric": "聖職者",
}
const TASK_NAMES := {
&"facility_clean": "清掃の作業", &"rescue_pickup": "救護の迎え", &"rescue_transport": "救護の搬送",
}
const STATE_NAMES := ["勤務外", "出勤中", "勤務中", "移動中", "作業中", "休憩中", "食事中", "睡眠中", "治療へ移動", "避難中", "行動不能", "ストライキ", "拘束中", "捕虜", "勤務不可"]
var summary_label: Label
@@ -25,6 +30,11 @@ var staff_detail_label: Label
var facility_option: OptionButton
var shift_option: OptionButton
var apply_button: Button
var dismiss_button: Button
var dismiss_confirm_box: VBoxContainer
var dismiss_confirm_label: Label
var dismiss_confirm_button: Button
var dismiss_cancel_button: Button
var result_label: Label
var selected_staff_id: StringName = &""
@@ -37,7 +47,10 @@ var _draft_facility_id: StringName = &""
var _draft_template_id: StringName = &"DAY"
var _draft_dirty := false
var _sequence := 1
var _dismissal_sequence := 1
var _pending_commands: Dictionary = {}
var _pending_dismissals: Dictionary = {}
var _confirming_dismissal_id: StringName = &""
var _queue_instance_id := 0
var _built := false
@@ -112,6 +125,42 @@ func _build_interface() -> void:
apply_button.pressed.connect(submit_assignment)
_style(apply_button)
editor_panel.add_child(apply_button)
# P16-01-DISMISS: the dismissal shows its severance and reopened Task before the
# confirming press; the Runtime command decides and records everything.
dismiss_button = Button.new()
dismiss_button.name = "DismissStaffButton"
dismiss_button.text = "解雇する…"
dismiss_button.custom_minimum_size.y = 32
dismiss_button.pressed.connect(request_dismissal)
_style(dismiss_button)
editor_panel.add_child(dismiss_button)
dismiss_confirm_box = VBoxContainer.new()
dismiss_confirm_box.name = "DismissStaffConfirm"
dismiss_confirm_box.add_theme_constant_override("separation", 3)
dismiss_confirm_box.visible = false
editor_panel.add_child(dismiss_confirm_box)
dismiss_confirm_label = _label("")
dismiss_confirm_label.name = "DismissStaffConfirmText"
dismiss_confirm_box.add_child(dismiss_confirm_label)
var confirm_row := HBoxContainer.new()
confirm_row.add_theme_constant_override("separation", 5)
dismiss_confirm_box.add_child(confirm_row)
dismiss_confirm_button = Button.new()
dismiss_confirm_button.name = "ConfirmDismissStaff"
dismiss_confirm_button.text = "解雇を確定"
dismiss_confirm_button.custom_minimum_size.y = 30
dismiss_confirm_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
dismiss_confirm_button.pressed.connect(confirm_dismissal)
_style(dismiss_confirm_button)
confirm_row.add_child(dismiss_confirm_button)
dismiss_cancel_button = Button.new()
dismiss_cancel_button.name = "CancelDismissStaff"
dismiss_cancel_button.text = "やめる"
dismiss_cancel_button.custom_minimum_size.y = 30
dismiss_cancel_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
dismiss_cancel_button.pressed.connect(cancel_dismissal)
_style(dismiss_cancel_button)
confirm_row.add_child(dismiss_cancel_button)
result_label = _label("")
result_label.name = "StaffAssignmentResult"
add_child(result_label)
@@ -124,6 +173,7 @@ func refresh() -> void:
if queue != null and int(queue.get_instance_id()) != _queue_instance_id:
_queue_instance_id = int(queue.get_instance_id())
_draft_dirty = false
_confirming_dismissal_id = &""
facility_option.get_popup().hide()
shift_option.get_popup().hide()
_synchronize_commands(queue)
@@ -178,6 +228,7 @@ func refresh() -> void:
func clear_selection() -> void:
selected_staff_id = &""
_draft_dirty = false
_confirming_dismissal_id = &""
if not _built:
return
result_label.text = ""
@@ -239,16 +290,164 @@ func _refresh_editor() -> void:
facility_option.select(selected_index)
shift_option.select(1 if _draft_template_id == &"NIGHT" else 0)
var pending := _pending_commands.values().has(selected_staff_id)
facility_option.disabled = pending
shift_option.disabled = pending
var dismissing := _pending_dismissals.values().has(selected_staff_id)
facility_option.disabled = pending or dismissing
shift_option.disabled = pending or dismissing
_refresh_dismissal(row, dismissing)
var can_edit := bool(row.get(&"can_edit", true))
apply_button.disabled = pending or not can_edit or facility_option.is_item_disabled(selected_index)
apply_button.disabled = pending or dismissing or not can_edit or facility_option.is_item_disabled(selected_index)
apply_button.tooltip_text = _assignment_block_text(row) if not can_edit else "配属先と毎日の勤務帯をまとめて変更します。"
if not can_edit:
staff_detail_label.text += "\n" + _assignment_block_text(row)
apply_button.text = "変更を受理済み…" if pending else "配属・勤務帯を変更"
## P16-01-DISMISS: the Runtime's preview decides whether the dismissal is offered;
## a refused row keeps the button disabled and shows the reason.
func _refresh_dismissal(row: Dictionary, dismissing: bool) -> void:
var preview: Dictionary = row.get(&"dismissal", {})
var allowed := bool(preview.get(&"can_dismiss", false))
dismiss_button.disabled = dismissing or not allowed
dismiss_button.text = "解雇を受理済み…" if dismissing else "解雇する…"
if allowed:
dismiss_button.tooltip_text = _dismissal_summary(row, preview)
else:
var reason := StringName(preview.get(&"reason", &""))
var reason_text := _dismissal_reason_text(reason, preview)
dismiss_button.tooltip_text = reason_text
if not dismissing and not reason_text.is_empty() and not staff_detail_label.text.contains(reason_text):
# UNIQUE keeps its existing contract line; the other reasons are labelled.
var line := reason_text if reason == &"UNIQUE" else "解雇不可:" + reason_text
staff_detail_label.text += "\n" + line
if _confirming_dismissal_id != selected_staff_id or dismissing or not allowed:
_confirming_dismissal_id = &""
dismiss_confirm_box.visible = not _confirming_dismissal_id.is_empty()
if dismiss_confirm_box.visible:
dismiss_confirm_label.text = _dismissal_summary(row, preview)
func request_dismissal() -> void:
if _runtime == null or selected_staff_id.is_empty():
return
refresh()
var row := _selected_row()
var preview: Dictionary = row.get(&"dismissal", {})
if row.is_empty() or not bool(preview.get(&"can_dismiss", false)) or _pending_dismissals.values().has(selected_staff_id):
var reason_text := "この従業員の解雇はすでに受理されています。" if _pending_dismissals.values().has(selected_staff_id) \
else _dismissal_reason_text(StringName(preview.get(&"reason", &"")), preview)
_set_result(reason_text if not reason_text.is_empty() else "この従業員は解雇できません。", false)
return
_confirming_dismissal_id = selected_staff_id
refresh()
func cancel_dismissal() -> void:
_confirming_dismissal_id = &""
refresh()
func confirm_dismissal() -> void:
if _runtime == null or selected_staff_id.is_empty():
return
var armed := _confirming_dismissal_id
# Re-read the roster first: a load or a changed employee never sends a stale request.
refresh()
var row := _selected_row()
var preview: Dictionary = row.get(&"dismissal", {})
var employee_name := String(row.get(&"display_name", selected_staff_id))
if not armed.is_empty() and armed == selected_staff_id and not row.is_empty() \
and not bool(preview.get(&"can_dismiss", false)):
_confirming_dismissal_id = &""
_set_result("%s:%s" % [employee_name, _dismissal_reason_text(StringName(preview.get(&"reason", &"")), preview)], false)
refresh()
return
if armed.is_empty() or armed != selected_staff_id or _confirming_dismissal_id != armed:
_set_result("解雇の確認は取り消されました。もう一度「解雇する…」を押してください。", false)
return
var staff_id := selected_staff_id
_confirming_dismissal_id = &""
var key := StringName("%s%08d" % [DISMISSAL_KEY_PREFIX, _dismissal_sequence])
_dismissal_sequence += 1
var result: Variant = _submit_ui_command(DISMISSAL_COMMAND, {&"staff_id": staff_id}, &"player", staff_id, 75, -1, key)
if result == null or not result.has_method(&"is_success"):
_set_result("解雇に応答がありません。", false)
refresh()
return
var pending := false
var queue: Variant = _runtime.get(&"command_queue")
if queue != null:
for command: Dictionary in queue.create_snapshot().get("pending", []):
if int(command.get("command_id", 0)) == int(result.command_id):
pending = true
break
if result.is_success() and pending:
_pending_dismissals[int(result.command_id)] = staff_id
_set_result("%s の解雇を受理しました。次の時間更新で反映します。" % employee_name, true)
else:
_show_dismissal_result(result, employee_name)
refresh()
func _show_dismissal_result(result: Variant, employee_name: String) -> void:
var details: Dictionary = result.details if result.details is Dictionary else {}
if not result.is_success():
var translated := _dismissal_reason_text(StringName(details.get(&"reason", details.get("reason", &""))), details)
_set_result("%s:%s" % [employee_name, translated if not translated.is_empty() else "解雇できませんでした:%s" % String(result.message)], false)
return
var text := "%s を解雇しました。退職金 G%d を払いました" % [employee_name, int(details.get(&"severance", 0))]
if int(details.get(&"severance_top_up", 0)) > 0:
text += "(不足 G%d は裏金から補填)" % int(details[&"severance_top_up"])
text += "。"
if int(details.get(&"unpaid_wages_preserved", 0)) > 0:
text += "未払い G%d は次の0時に精算します。" % int(details[&"unpaid_wages_preserved"])
if not (details.get(&"reopened_task_ids", []) as Array).is_empty():
text += "持っていた作業は開き直しました。"
if not (details.get(&"interrupted_rescue_task_ids", []) as Array).is_empty():
text += "関わっていた救護の作業は取り消しました。"
_set_result(text, true)
func _dismissal_summary(row: Dictionary, preview: Dictionary) -> String:
var severance := int(preview.get(&"severance", 0))
var top_up := maxi(severance - int(preview.get(&"front_cash", 0)), 0)
var text := "%sを解雇します。退職金 G%d(日給の3日分)を表金から払います。" % [row.get(&"display_name", selected_staff_id), severance]
if top_up > 0:
text += "不足 G%d は裏金から補います。" % top_up
if int(preview.get(&"unpaid_wages", 0)) > 0:
text += "未払い G%d は退職金と別に次の0時に精算します。" % int(preview[&"unpaid_wages"])
var task_names: Array[String] = []
for task_type: Variant in preview.get(&"reopened_task_types", []):
task_names.append(String(TASK_NAMES.get(StringName(task_type), "作業")))
if not task_names.is_empty():
text += "持っている%sは開き直して、ほかの従業員に回します。" % "・".join(task_names)
# A rescue the command interrupts is cancelled, not reopened (SPEC 6.2.1).
var roles: Array = preview.get(&"interrupted_rescue_roles", [])
if roles.has(&"carrier"):
text += "運んでいる救護の搬送は取り消し、患者の救護をやり直します。"
if roles.has(&"patient"):
text += "この従業員への救護は取り消し、救護係を外します。"
return text
func _dismissal_reason_text(reason: StringName, details: Dictionary = {}) -> String:
match reason:
&"":
return ""
&"UNIQUE":
return "ユニーク:雇用・維持費無料 / 解雇・売却不可"
&"INSUFFICIENT_FUNDS":
return "退職金 G%d を払えません(表金と裏金の合計 G%d)。" % [int(details.get(&"severance", 0)), int(details.get(&"available_funds", 0))]
var reasons := {
&"NOT_FOUND": "従業員が見つかりません。",
&"NOT_GENERAL_EMPLOYEE": "ギルドで雇った一般従業員だけを解雇できます。",
&"EXTERNAL_TASK": "物流の受け取り・配達・返送の途中です。終わってから解雇できます。",
&"DEFENSE_LOCKED": "襲撃の防衛に参加中は解雇できません。襲撃が終わってから解雇できます。",
&"RESERVATION_RELEASE_FAILED": "作業の予約を解けないため解雇できません。",
&"BANKRUPT": "運営終了後は解雇できません。",
}
return String(reasons.get(reason, ""))
## Runtime supplies canonical, owned presentation data after every normal
## load. The panel neither infers a unique identity from wages nor edits a save.
func _unique_contract_text(row: Dictionary) -> String:
@@ -373,15 +572,20 @@ func _assignment_reason_text(reason: StringName) -> String:
func _synchronize_commands(queue: Variant) -> void:
_pending_commands.clear()
_pending_dismissals.clear()
for metadata: Dictionary in queue.get_accepted_command_metadata():
var key := String(metadata.get("idempotency_key", ""))
if key.begins_with(KEY_PREFIX) and key.trim_prefix(KEY_PREFIX).is_valid_int():
_sequence = maxi(_sequence, int(key.trim_prefix(KEY_PREFIX)) + 1)
if key.begins_with(DISMISSAL_KEY_PREFIX) and key.trim_prefix(DISMISSAL_KEY_PREFIX).is_valid_int():
_dismissal_sequence = maxi(_dismissal_sequence, int(key.trim_prefix(DISMISSAL_KEY_PREFIX)) + 1)
for command: Dictionary in queue.create_snapshot().get("pending", []):
if StringName(command.get("command_type", &"")) != ASSIGNMENT_COMMAND:
var command_type := StringName(command.get("command_type", &""))
if command_type not in [ASSIGNMENT_COMMAND, DISMISSAL_COMMAND]:
continue
var payload: Dictionary = command.get("payload", {})
_pending_commands[int(command.get("command_id", 0))] = StringName(payload.get(&"staff_id", payload.get("staff_id", &"")))
var pending := _pending_commands if command_type == ASSIGNMENT_COMMAND else _pending_dismissals
pending[int(command.get("command_id", 0))] = StringName(payload.get(&"staff_id", payload.get("staff_id", &"")))
func _on_snapshot_published(_snapshot: Dictionary = {}, _digest: String = "") -> void:
@@ -390,6 +594,17 @@ func _on_snapshot_published(_snapshot: Dictionary = {}, _digest: String = "") ->
func _on_command_completed(result: Variant) -> void:
if _pending_dismissals.has(int(result.command_id)):
var dismissed_id := StringName(_pending_dismissals[int(result.command_id)])
_pending_dismissals.erase(int(result.command_id))
var employee_name := String(dismissed_id)
for row: Dictionary in _staff_rows:
if StringName(row.get(&"staff_id", &"")) == dismissed_id:
employee_name = String(row.get(&"display_name", dismissed_id))
break
_show_dismissal_result(result, employee_name)
refresh()
return
if not _pending_commands.has(int(result.command_id)):
return
var staff_id := StringName(_pending_commands[int(result.command_id)])
@@ -478,8 +693,9 @@ func _style(button: Button) -> void:
func _staff_drag_data(_position: Vector2, staff_id: StringName) -> Variant:
if selected_staff_id != staff_id:
select_staff(staff_id)
if _pending_commands.values().has(staff_id) or not bool(_selected_row().get(&"can_edit", false)):
_set_result("変更を処理中です。" if _pending_commands.values().has(staff_id) else _assignment_block_text(_selected_row()), false)
var busy := _pending_commands.values().has(staff_id) or _pending_dismissals.values().has(staff_id)
if busy or not bool(_selected_row().get(&"can_edit", false)):
_set_result("変更を処理中です。" if busy else _assignment_block_text(_selected_row()), false)
return null
var preview := Label.new()
preview.text = "%s:%s → 施設へ配置" % [_selected_row().get(&"display_name", staff_id), "夜勤" if _draft_template_id == &"NIGHT" else "日勤"]
+51 -9
View File
@@ -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"]
@@ -301,16 +302,24 @@ func _approved_runtime_migrations(expected: Dictionary, reader: Dictionary) -> D
fields.append("elevators_v1_to_v2")
proof["elevators"] = {"only_version_changed": same_otherwise}
# P05-02 A12: a save from before the funds were split gains the five empty fund
# accounts on load; every other economy field stays byte-identical.
var has_back_cash := func(economy: Variant) -> bool:
# accounts on load, and P16-01-DISMISS (A14) a save from before the severance gains
# its empty expense account; every other economy field stays byte-identical.
var has_account := func(economy: Variant, account_id: String) -> bool:
if not economy is Dictionary:
return false
for account: Variant in (economy as Dictionary).get(&"accounts", []):
if account is Dictionary and String((account as Dictionary).get(&"account_id", "")) == "BACK_CASH":
if account is Dictionary and String((account as Dictionary).get(&"account_id", "")) == account_id:
return true
return false
if not has_back_cash.call(old.get(&"economy")) and has_back_cash.call(now.get(&"economy")):
var added := {"BACK_CASH": false, "LOAN_INTEREST_EXPENSE": false, "LOAN_INTEREST_PAYABLE": true, "RANK_TAX_EXPENSE": false, "RANK_TAX_PAYABLE": true}
var fund_accounts := {"BACK_CASH": false, "LOAN_INTEREST_EXPENSE": false, "LOAN_INTEREST_PAYABLE": true, "RANK_TAX_EXPENSE": false, "RANK_TAX_PAYABLE": true}
var added := {}
if not has_account.call(old.get(&"economy"), "BACK_CASH") and has_account.call(now.get(&"economy"), "BACK_CASH"):
added.merge(fund_accounts)
var severance_added: bool = not has_account.call(old.get(&"economy"), "STAFF_SEVERANCE_EXPENSE") \
and has_account.call(now.get(&"economy"), "STAFF_SEVERANCE_EXPENSE")
if severance_added:
added["STAFF_SEVERANCE_EXPENSE"] = false
if not added.is_empty():
var economy: Dictionary = (old[&"economy"] as Dictionary).duplicate(true)
var accounts: Array = (economy[&"accounts"] as Array).duplicate(true)
for account_id: String in added:
@@ -320,8 +329,41 @@ func _approved_runtime_migrations(expected: Dictionary, reader: Dictionary) -> D
var only_accounts_added: bool = var_to_bytes(economy) == var_to_bytes(now[&"economy"])
ok = ok and only_accounts_added
old[&"economy"] = economy
fields.append("economy_A12_empty_fund_accounts")
proof["economy_A12"] = {"added_accounts": added.keys(), "only_accounts_added": only_accounts_added}
if added.has("BACK_CASH"):
fields.append("economy_A12_empty_fund_accounts")
proof["economy_A12"] = {"added_accounts": fund_accounts.keys(), "only_accounts_added": only_accounts_added}
if severance_added:
fields.append("economy_A14_empty_severance_account")
proof["economy_A14"] = {"added_accounts": ["STAFF_SEVERANCE_EXPENSE"], "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
+7 -2
View File
@@ -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")
+5 -1
View File
@@ -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()
+1 -1
View File
@@ -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
+304
View File
@@ -0,0 +1,304 @@
extends RefCounted
## P16-01-DISMISS shared scenes for tests/staff/test_general_dismissal.gd and its
## restart writer and reader. Every scene runs the production Runtime; a fixture
## only places employees, rescue states, Events and balances, never the dismissal.
const FRONT := &"PLAYER_CASH"
const BACK := &"BACK_CASH"
const FRONT_SALES := &"P16_FIXTURE_FRONT_SALES"
const BACK_SALES := &"P16_FIXTURE_BACK_SALES"
const PAYER := &"P16_FIXTURE_PAYER"
## The ledger day script (F4/F8): two hired general employees, a 00:00 close
## without funds (their first day's wages stay unpaid), sales in both funds,
## the dismissal of staff_akane at 01:00 of day 2 (severance 360 = 3 x 120: front
## 300, hidden 60) and the day 2 close that pays staff_eri and the departed
## obligation of staff_akane (120) once, the front fund topped up from the hidden one.
const LEDGER_SEED := 16_016
const DISMISSED := &"staff_akane"
const KEPT := &"staff_eri"
const DISMISS_MINUTE := 1500
const LAST_MINUTE := 2900
const SAVE_POINTS := [[1500, "before"], [1500, "after"], [1501, "before"], [2879, "before"], [2880, "before"], [2881, "before"]]
const RESCUE_SEED := 712
const HANDOFF_SCHEMA := "p16-01-dismiss-restart-v1"
const HANDOFF_PATH := "res://artifacts/p16-01-dismiss/restart-handoff.json"
const WRITER_SOURCE := "res://tests/staff/test_general_dismissal_restart_1_write.gd"
const WRITER_SAVE := "user://tests/p16-dismiss-writer-%s"
const READER_SAVE := "user://tests/p16-dismiss-reader-%s"
var _expect: Callable
func _init(expect: Callable) -> void:
_expect = expect
func check(condition: bool, message: String) -> void:
_expect.call(condition, message)
func new_fixture(seed: int, operations := false) -> 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()
check(bool(runtime.initialize(clock, state, seed).get(&"valid", false)), "fixture Runtime initializes (seed %d)" % seed)
clock.set_paused(true)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
if operations:
check(bool(runtime.enable_production_operations().get(&"ok", false)), "fixture enables production Operations")
for account: StringName in [FRONT_SALES, BACK_SALES, PAYER]:
runtime.economy_service.register_account(account)
check(runtime.economy_service.initialize_account(PAYER, 100000, clock.absolute_minute, &"p16.fixture.payer").is_success(), "fixture payer funded")
return {&"clock": clock, &"state": state, &"runtime": runtime}
func free_fixture(fixture: Dictionary) -> void:
for key: StringName in [&"runtime", &"clock", &"state"]:
if fixture.get(key) != null:
fixture[key].free()
func command(fixture: Dictionary, type: StringName, payload: Dictionary, key: StringName = &"") -> CommandResult:
return fixture[&"runtime"].submit_command(type, payload, &"player", &"", 0, -1, key)
func dismiss(fixture: Dictionary, staff_id: StringName, key: StringName = &"") -> CommandResult:
return command(fixture, &"dismiss_staff", {&"staff_id": staff_id}, key)
## A disclosed balance fixture: a sale swept into the front or the hidden fund.
func sale(fixture: Dictionary, fund: StringName, amount: int, key: String) -> bool:
var runtime: Variant = fixture[&"runtime"]
var minute: int = fixture[&"clock"].absolute_minute
var revenue := FRONT_SALES if fund == FRONT else BACK_SALES
var entries: Array = [
LedgerEntry.new().configure(&"", minute, revenue, PAYER, amount, &"SERVICE_SALE", &"p16.fixture"),
LedgerEntry.new().configure(&"", minute, fund, revenue, amount, &"SERVICE_CASH_SWEEP", &"p16.fixture"),
]
return runtime.economy_service.post(entries, StringName("p16.sale." + key)).is_success()
## A disclosed balance fixture: an operating cost that empties the front fund.
func drain_front(fixture: Dictionary, key: String) -> bool:
var runtime: Variant = fixture[&"runtime"]
var cash: int = runtime.economy_service.get_balance(FRONT)
if cash <= 0:
return true
return runtime.economy_service.transfer(FRONT, &"OPERATING_EXPENSE", cash, fixture[&"clock"].absolute_minute,
&"OPERATING_EXPENSE", &"p16.fixture", StringName("p16.drain." + key)).is_success()
func posted(fixture: Dictionary, key: String) -> bool:
return not fixture[&"runtime"].economy_service.get_transaction_id_for_idempotency_key(StringName(key)).is_empty()
## The ledger day script from `from_minute` to `to_minute`. `on_point` is called
## with (minute, "before") ahead of the minute's scripted step and (minute, "after")
## behind it. Each step is skipped when its effect is already in the loaded game.
func play_ledger(fixture: Dictionary, from_minute: int, to_minute: int, on_point: Callable) -> void:
var clock: Variant = fixture[&"clock"]
var runtime: Variant = fixture[&"runtime"]
if int(clock.absolute_minute) != from_minute:
check(false, "ledger script starts at minute %d (clock %d)" % [from_minute, clock.absolute_minute])
return
while int(clock.absolute_minute) < to_minute:
var minute := int(clock.absolute_minute)
if on_point.is_valid():
on_point.call(minute, "before")
if minute == 362 and not runtime.staff_service.has_staff(DISMISSED):
for candidate: StringName in [DISMISSED, KEPT]:
check(command(fixture, &"hire_staff_candidate", {&"candidate_id": candidate}).is_success(), "hire %s through the ordinary command" % candidate)
if minute == 1430 and not posted(fixture, "p16.drain.close1"):
check(drain_front(fixture, "close1"), "the front fund is empty before the first close")
if minute == 1470 and not posted(fixture, "p16.sale.front2"):
check(sale(fixture, FRONT, 300, "front2") and sale(fixture, BACK, 400, "back2"), "day 2 sales: front 300, hidden 400")
if minute == DISMISS_MINUTE and runtime.staff_service.has_staff(DISMISSED):
var result := dismiss(fixture, DISMISSED, &"p16.ledger.dismiss")
check(result.is_success() and int(result.details.get(&"severance", -1)) == 360 and int(result.details.get(&"severance_top_up", -1)) == 60,
"the scripted dismissal pays 360, 60 of it from the hidden fund (%s %s)" % [result.message, result.details])
if on_point.is_valid():
on_point.call(minute, "after")
clock.advance_minutes(1)
func new_ledger_fixture() -> Dictionary:
return new_fixture(LEDGER_SEED, true)
func count_category(runtime: Variant, category: StringName, source: StringName = &"") -> int:
var count := 0
for entry: LedgerEntry in runtime.economy_service.get_entries():
if entry.category == category and (source.is_empty() or entry.source_entity_id == source):
count += 1
return count
func economy_bytes(runtime: Variant) -> PackedByteArray:
return var_to_bytes(runtime.economy_service.create_snapshot())
## The rescue elevator world of tests/staff/test_rescue_elevator.gd: an infirmary,
## a service elevator, a collapsed medic upstairs (patient) and a medic below.
## Both are ordinary FREE_HIRE employees with a daily wage of 100 (severance 300).
func new_rescue_fixture(seed := RESCUE_SEED) -> Dictionary:
var fixture := new_fixture(seed)
for spec: Array in [[&"infirmary", Vector3i(-6, 0, 1)], [&"service_elevator", Vector3i(0, 0, 1)]]:
check(command(fixture, &"build_facility", {&"definition_id": spec[0], &"origin": spec[1]}).is_success(), "ordinary construction: %s" % spec[0])
fixture[&"clock"].advance_minutes(400)
for id: StringName in [&"patient", &"medic"]:
var options := {
&"current_cell": Vector3i(0, 1, 1) if id == &"patient" else Vector3i(5, 0, 1),
&"health": 0 if id == &"patient" else 100, &"state": StaffRuntime.State.AVAILABLE,
&"role_tags": [&"medic"], &"permitted_task_tags": [&"rescue"], &"daily_wage": 100,
}
check(command(fixture, &"register_staff", {&"staff_id": id, &"role_id": &"medic", &"options": options}).is_success(), "register rescue employee %s" % id)
return fixture
## The medic's rescue phase: task type, task state, elevator state and pairing.
func rescue_phase(runtime: Variant) -> String:
var medic: StaffRuntime = runtime.staff_service.get_staff(&"medic")
if medic == null:
return "gone"
var task: StaffTask = runtime.staff_service.get_task(medic.current_task_id)
var trip: Dictionary = runtime.elevators.get_actor_trip(&"staff:medic")
return "%s/%s/%s/%s" % [task.task_type if task != null else "-", StaffTask.State.keys()[task.state] if task != null else "-",
trip.get(&"state", "-"), "pair" if trip.has(&"companion_id") else "single"]
## Advances the rescue world minute by minute to the first minute of `phase`.
func advance_to_rescue_phase(fixture: Dictionary, phase: String, limit := 160) -> bool:
for tick: int in limit:
if rescue_phase(fixture[&"runtime"]) == phase:
return true
fixture[&"clock"].advance_minutes(1)
return rescue_phase(fixture[&"runtime"]) == phase
func register_event_definition(fixture: Dictionary, event_id: StringName, gold_cost: int) -> bool:
return command(fixture, &"register_event_definition", {&"definition": {
&"event_id": event_id, &"category": &"RELATIONSHIP", &"priority": 70, &"foreground": true,
&"transaction_policy": &"ROLLBACK_ALL", &"missing_actor_policy": &"CANCEL", &"content_rating": &"GENERAL",
&"choices": [{&"choice_id": &"purchase", &"text_key": StringName("event.%s.purchase" % event_id), &"cost": {&"gold": gold_cost},
&"actions": [{&"action_id": &"paid_choice_flag", &"type": &"SET_FLAG", &"payload": {&"flag_id": &"paid_choice_completed"}}]}],
}}).is_success()
## The Event scene of tests/s4/test_runtime_relationship_events.gd: a foreground
## Event that locks the employee and holds 80 Gold of reserved choice cost.
func new_event_fixture(seed: int) -> Dictionary:
var fixture := new_fixture(seed)
var runtime: Variant = fixture[&"runtime"]
check(command(fixture, &"register_staff", {&"staff_id": &"event_actor", &"role_id": &"service", &"options": {&"daily_wage": 80}}).is_success(), "register the Event actor")
check(register_event_definition(fixture, &"p16.event.actor", 80), "register the paid Event definition")
check(command(fixture, &"enqueue_event", {&"event_id": &"p16.event.actor", &"actor_ids": [&"event_actor"],
&"options": {&"event_instance_id": &"p16.event.instance"}}).is_success(), "enqueue the Event with the employee as its actor")
fixture[&"clock"].set_paused(false)
fixture[&"clock"].advance_one_minute()
var foreground: Variant = runtime.event_service.get_foreground_instance()
check(foreground != null and runtime.event_service.get_actor_locks().has(&"event_actor"), "the foreground Event locks the employee")
if foreground != null:
check(command(fixture, &"reserve_event_choice_cost", {&"event_instance_id": foreground.event_instance_id, &"choice_id": &"purchase"}).is_success(),
"the Event reserves its choice cost in escrow")
fixture[&"clock"].set_paused(true)
return fixture
## A foreground Event of two employees whose missing actor policy continues without
## the missing one (CONTINUE_WITHOUT).
func new_event_pair_fixture(seed: int) -> Dictionary:
var fixture := new_fixture(seed)
var runtime: Variant = fixture[&"runtime"]
for staff_id: StringName in [&"event_lead", &"event_partner"]:
check(command(fixture, &"register_staff", {&"staff_id": staff_id, &"role_id": &"service", &"options": {&"daily_wage": 80}}).is_success(), "register %s" % staff_id)
check(command(fixture, &"register_event_definition", {&"definition": {&"event_id": &"p16.event.pair", &"category": &"RELATIONSHIP", &"priority": 70,
&"foreground": true, &"transaction_policy": &"ROLLBACK_ALL", &"missing_actor_policy": &"CONTINUE_WITHOUT", &"content_rating": &"GENERAL",
&"choices": [{&"choice_id": &"wait", &"text_key": &"event.p16.pair.wait", &"actions": []}]}}).is_success()
and command(fixture, &"enqueue_event", {&"event_id": &"p16.event.pair", &"actor_ids": [&"event_lead", &"event_partner"],
&"options": {&"event_instance_id": &"p16.event.pair.instance"}}).is_success(), "a two-actor Event that continues without a missing actor")
fixture[&"clock"].set_paused(false)
fixture[&"clock"].advance_one_minute()
fixture[&"clock"].set_paused(true)
check(runtime.event_service.get_actor_locks().has(&"event_partner"), "the Event locks both employees")
return fixture
## Live references to a removed employee (SPEC 6.2/6.2.1): live Tasks, elevator
## trips, the path actor, facility staffing, Event locks and running Event actors.
## Terminal Task records keep their history.
func dangling_references(runtime: Variant, staff_id: StringName) -> Array[String]:
var found: Array[String] = []
if runtime.staff_service.has_staff(staff_id):
found.append("staff")
for task: StaffTask in runtime.staff_service.get_tasks():
if not task.is_terminal() and (task.target_entity_id == staff_id or task.assigned_staff_id == staff_id):
found.append("task " + String(task.task_id))
var actor := StringName("staff:" + String(staff_id))
if not runtime.elevators.get_actor_trip(actor).is_empty():
found.append("elevator trip")
for trip: Dictionary in runtime.elevators.get_report()[&"trips"]:
if StringName(trip.get(&"companion_id", &"")) == actor:
found.append("elevator companion")
if runtime.path_service.has_actor_projection(actor):
found.append("path actor")
for facility: FacilityInstance in runtime.facility_service.get_instances():
if staff_id in facility.active_staff_ids:
found.append("facility " + String(facility.instance_id))
if runtime.event_service.get_actor_locks().has(staff_id):
found.append("event lock")
for instance: EventInstance in runtime.event_service.get_instances():
if instance.state in [EventInstance.State.RUNNING, EventInstance.State.WAITING_CHOICE, EventInstance.State.INTERRUPTED] \
and staff_id in instance.actor_ids:
found.append("event actor " + String(instance.event_instance_id))
return found
## A transport-safe value of the state a dismissal touches, compared across
## processes (String keys, sorted, no StringName ordering).
func state_value(runtime: Variant, clock: Variant) -> Dictionary:
var staff := {}
for worker: StaffRuntime in runtime.staff_service.get_staff_members():
staff[String(worker.staff_id)] = [int(worker.state), var_to_str(worker.current_cell), String(worker.current_task_id), worker.unpaid_wages, worker.health]
var tasks := {}
for task: StaffTask in runtime.staff_service.get_tasks():
tasks[String(task.task_id)] = [String(task.task_type), int(task.state), String(task.state_reason), String(task.target_entity_id), String(task.assigned_staff_id), task.reservation_ids.size()]
var trips := {}
for trip: Dictionary in runtime.elevators.get_report()[&"trips"]:
trips[String(trip.get(&"actor_id", ""))] = [String(trip.get(&"state", "")), String(trip.get(&"companion_id", ""))]
var balances := {}
for account: Dictionary in runtime.economy_service.get_accounts():
balances[String(account[&"account_id"])] = int(account[&"balance"])
var departed := {}
for staff_id: StringName in runtime.staff_service.get_departed_staff_ids():
departed[String(staff_id)] = runtime.staff_service.get_departed_unpaid_wages(staff_id)
var locks := []
for actor_id: Variant in runtime.event_service.get_actor_locks():
locks.append(String(actor_id))
locks.sort()
var events := {}
for instance: EventInstance in runtime.event_service.get_instances():
events[String(instance.event_instance_id)] = [int(instance.state), instance.actor_ids.size()]
return {"minute": int(clock.absolute_minute), "staff": _sorted(staff), "tasks": _sorted(tasks), "trips": _sorted(trips),
"balances": _sorted(balances), "departed": _sorted(departed), "locks": locks, "events": _sorted(events),
"severance_entries": count_category(runtime, FinancePolicy.SEVERANCE_CATEGORY), "wage_entries": count_category(runtime, &"WAGE")}
func _sorted(value: Dictionary) -> Array:
var keys := value.keys()
keys.sort()
var rows := []
for key: Variant in keys:
rows.append([key, value[key]])
return rows
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://dr2mdns7nvdoj
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
uid://dbf85ttljo65h
@@ -0,0 +1,121 @@
extends SceneTree
## P16-01-DISMISS F7/F8, writer phase. Plays the ledger day script and writes
## ordinary save files before and right after the dismissal command (before the
## next minute), a minute later and around the day 2 00:00 close; and saves the
## command boundary of A6 (the medic dismissed while carrying the patient on
## foot), A7 (the patient dismissed while the medic rides the elevator alone) and
## A8 (the actor of a foreground Event with reserved cost dismissed). The reader
## suite (test_general_dismissal_restart_2_read.gd) loads them in a separate
## engine process. The handoff is the declared generated report
## res://artifacts/p16-01-dismiss/restart-handoff.json
## (tools/regression/suite-policy.json fixed_generated_report_routes).
const Scenes := preload("res://tests/helpers/p16_dismissal_fixture.gd")
const CONTINUE_MINUTES := 5
var _checks := 0
var _failures := 0
var _scenes: Scenes
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("P16-01-DISMISS: restart writer")
_scenes = Scenes.new(_expect)
var straight := _scenes.new_ledger_fixture()
_scenes.play_ledger(straight, 360, Scenes.LAST_MINUTE, Callable())
var straight_runtime: Variant = straight[&"runtime"]
_expect(_scenes.count_category(straight_runtime, FinancePolicy.SEVERANCE_CATEGORY) == 1 and straight_runtime.staff_service.get_departed_unpaid_wages(Scenes.DISMISSED) == 0,
"uninterrupted script: one severance and the held wages paid at the day 2 close")
var saving := _scenes.new_ledger_fixture()
var ledger_saves: Array = []
_scenes.play_ledger(saving, 360, Scenes.LAST_MINUTE, func(minute: int, phase: String) -> void:
if [minute, phase] in Scenes.SAVE_POINTS:
var name := "ledger-%d-%s" % [minute, phase]
var path := Scenes.WRITER_SAVE % name
_expect(bool(saving[&"runtime"].save_to_path(path).get(&"ok", false)), "save file %s" % name)
ledger_saves.append(_package(name, path, {"minute": minute, "phase": phase})))
_expect(_scenes.economy_bytes(saving[&"runtime"]) == _scenes.economy_bytes(straight_runtime), "writing the save files leaves the ledger as without saving")
var boundaries: Array = []
for scene: Array in [["A6", "rescue_transport/MOVING/-/single", &"medic"], ["A7", "rescue_pickup/MOVING/BOARDED/single", &"patient"],
["A6-pickup", "rescue_pickup/MOVING/-/single", &"medic"], ["A6-pair-aboard", "rescue_transport/MOVING/BOARDED/pair", &"medic"],
["A7-pair-aboard", "rescue_transport/MOVING/BOARDED/pair", &"patient"]]:
var fixture := _scenes.new_rescue_fixture()
_expect(_scenes.advance_to_rescue_phase(fixture, scene[1]), "%s: the rescue reaches %s" % [scene[0], scene[1]])
boundaries.append(_boundary(fixture, scene[0], scene[2]))
_scenes.free_fixture(fixture)
var event_fixture := _scenes.new_event_fixture(16_100)
boundaries.append(_boundary(event_fixture, "A8", &"event_actor"))
_scenes.free_fixture(event_fixture)
var pair_fixture := _scenes.new_event_pair_fixture(16_101)
boundaries.append(_boundary(pair_fixture, "A8-continue-without", &"event_partner"))
_scenes.free_fixture(pair_fixture)
var handoff := {
"schema": Scenes.HANDOFF_SCHEMA,
"writer_pid": OS.get_process_id(),
"writer_source_sha256": FileAccess.get_sha256(Scenes.WRITER_SOURCE),
"ledger_seed": Scenes.LEDGER_SEED,
"last_minute": Scenes.LAST_MINUTE,
"straight_economy_sha256": Scenes.sha256(_scenes.economy_bytes(straight_runtime)),
"straight_state_base64": Marshalls.raw_to_base64(var_to_bytes(_scenes.state_value(straight_runtime, straight[&"clock"]))),
"ledger_saves": ledger_saves,
"boundaries": boundaries,
}
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(Scenes.HANDOFF_PATH).get_base_dir())
var file := FileAccess.open(Scenes.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("P16_RESTART_WRITER pid=%d handoff_sha256=%s" % [OS.get_process_id(), FileAccess.get_sha256(Scenes.HANDOFF_PATH)])
_scenes.free_fixture(straight)
_scenes.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)
## Dismisses the employee, saves at once (the command boundary, before the next
## minute), then records the state then and CONTINUE_MINUTES later.
func _boundary(fixture: Dictionary, label: String, staff_id: StringName) -> Dictionary:
var runtime: Variant = fixture[&"runtime"]
var result := _scenes.dismiss(fixture, staff_id)
_expect(result.is_success(), "%s: the employee is dismissed (%s)" % [label, result.message])
var dangling: Array[String] = _scenes.dangling_references(runtime, staff_id)
_expect(dangling.is_empty(), "%s: no live reference remains at the command boundary %s" % [label, dangling])
var name := "boundary-%s" % label
var path := Scenes.WRITER_SAVE % name
_expect(bool(runtime.save_to_path(path).get(&"ok", false)), "%s: save right after the command" % label)
var at_command := _scenes.state_value(runtime, fixture[&"clock"])
fixture[&"clock"].advance_minutes(CONTINUE_MINUTES)
var later := _scenes.state_value(runtime, fixture[&"clock"])
return _package(name, path, {"label": label, "staff_id": String(staff_id), "continue_minutes": CONTINUE_MINUTES,
"at_command_base64": Marshalls.raw_to_base64(var_to_bytes(at_command)), "later_base64": Marshalls.raw_to_base64(var_to_bytes(later))})
func _package(name: String, path: String, extra: Dictionary) -> Dictionary:
var directory := ProjectSettings.globalize_path(path)
_expect(DirAccess.dir_exists_absolute(directory) and DirAccess.get_directories_at(directory).is_empty(), "save package %s holds only files" % name)
var files := {}
for file_name: String in DirAccess.get_files_at(directory):
var bytes := FileAccess.get_file_as_bytes(directory.path_join(file_name))
files[file_name] = {"sha256": Scenes.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" % name)
var package := extra.duplicate(true)
package["name"] = name
package["files"] = files
return package
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
@@ -0,0 +1 @@
uid://df5vm2ir115eb
@@ -0,0 +1,168 @@
extends SceneTree
## P16-01-DISMISS F7/F8, reader phase. Runs after the writer suite
## (test_general_dismissal_restart_1_write.gd) as a separate engine process. It
## loads every ledger save, continues the day script and requires exactly the
## uninterrupted ledger: one severance and one payment of the held wages,
## whether the save was made before or right after the dismissal command or
## around the day 2 00:00 close. It loads the A6, A7 and A8 command boundary
## saves, requires the writer's state and no live reference to the dismissed
## employee, and continues them to the writer's later state. A changed package
## is refused.
const Scenes := preload("res://tests/helpers/p16_dismissal_fixture.gd")
var _checks := 0
var _failures := 0
var _scenes: Scenes
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("P16-01-DISMISS: restart reader")
_scenes = Scenes.new(_expect)
var handoff := _read_handoff()
if not handoff.is_empty():
_check_ledger(handoff)
_check_boundaries(handoff)
_check_tampered(handoff)
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(Scenes.HANDOFF_PATH), "the writer phase left its handoff (run test_general_dismissal_restart_1_write.gd first)")
if not FileAccess.file_exists(Scenes.HANDOFF_PATH):
return {}
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(Scenes.HANDOFF_PATH))
_expect(parsed is Dictionary and str((parsed as Dictionary).get("schema", "")) == Scenes.HANDOFF_SCHEMA, "handoff schema")
if not parsed is Dictionary or str((parsed as Dictionary).get("schema", "")) != Scenes.HANDOFF_SCHEMA:
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(), "a separate engine process wrote the saves")
_expect(str(handoff.get("writer_source_sha256", "")) == FileAccess.get_sha256(Scenes.WRITER_SOURCE), "the handoff comes from the current writer suite")
_expect(int(handoff.get("ledger_seed", -1)) == Scenes.LEDGER_SEED and int(handoff.get("last_minute", -1)) == Scenes.LAST_MINUTE, "same fixture and day script")
print("P16_RESTART_READER pid=%d writer_pid=%d handoff_sha256=%s" % [OS.get_process_id(), writer_pid, FileAccess.get_sha256(Scenes.HANDOFF_PATH)])
return handoff
func _decode(text: Variant) -> Variant:
return bytes_to_var(Marshalls.base64_to_raw(str(text)))
func _check_ledger(handoff: Dictionary) -> void:
var straight := _scenes.new_ledger_fixture()
_scenes.play_ledger(straight, 360, Scenes.LAST_MINUTE, Callable())
var straight_runtime: Variant = straight[&"runtime"]
var straight_economy := _scenes.economy_bytes(straight_runtime)
var straight_state := _scenes.state_value(straight_runtime, straight[&"clock"])
_expect(Scenes.sha256(straight_economy) == str(handoff.get("straight_economy_sha256", "")), "the uninterrupted day script gives the same ledger in both processes")
_expect(straight_state == _decode(handoff.get("straight_state_base64", "")), "the uninterrupted day script gives the same employees, Tasks and balances in both processes")
var saves: Array = handoff.get("ledger_saves", [])
var points: Array = []
for save: Variant in saves:
points.append([int((save as Dictionary).get("minute", -1)), str((save as Dictionary).get("phase", ""))] if save is Dictionary else [])
_expect(points == Scenes.SAVE_POINTS, "the writer saved before and right after the dismissal, a minute later and around the day 2 close")
for save: Variant in saves:
if not save is Dictionary:
continue
var minute := int((save as Dictionary)["minute"])
var name := str((save as Dictionary)["name"])
var path := Scenes.READER_SAVE % name
if not _materialize(path, (save as Dictionary).get("files", {}), false):
continue
var resumed := _scenes.new_ledger_fixture()
var runtime: Variant = resumed[&"runtime"]
_expect(bool(runtime.load_from_path(path).get(&"ok", false)) and int(resumed[&"clock"].absolute_minute) == minute, "%s: this process loads the save at minute %d" % [name, minute])
var dismissed_before: bool = not runtime.staff_service.has_staff(Scenes.DISMISSED)
_expect(dismissed_before == (name != "ledger-1500-before"), "%s: the save holds the employee exactly when it was made before the command" % name)
_scenes.play_ledger(resumed, minute, Scenes.LAST_MINUTE, Callable())
_expect(_scenes.economy_bytes(runtime) == straight_economy, "%s: resuming gives exactly the uninterrupted ledger" % name)
_expect(_scenes.state_value(runtime, resumed[&"clock"]) == straight_state, "%s: resuming gives the same employees, Tasks and balances" % name)
_expect(_scenes.count_category(runtime, FinancePolicy.SEVERANCE_CATEGORY) == 1 and runtime.economy_service.get_balance(FinancePolicy.SEVERANCE_EXPENSE) == 360,
"%s: the severance is paid once" % name)
_expect(runtime.staff_service.get_departed_unpaid_wages(Scenes.DISMISSED) == 0
and int(runtime.operations_coordinator.get_day_summary(2)[&"settlement"][&"input_payments"][0][&"amount"]) == 120, "%s: the held 120 is paid once, at the day 2 close" % name)
_scenes.free_fixture(resumed)
_scenes.free_fixture(straight)
func _check_boundaries(handoff: Dictionary) -> void:
var boundaries: Array = handoff.get("boundaries", [])
var labels: Array = []
for boundary: Variant in boundaries:
labels.append(str((boundary as Dictionary).get("label", "")) if boundary is Dictionary else "")
_expect(labels == ["A6", "A7", "A6-pickup", "A6-pair-aboard", "A7-pair-aboard", "A8", "A8-continue-without"], "the writer saved the A6, A7 and A8 command boundaries")
for boundary: Variant in boundaries:
if not boundary is Dictionary:
continue
var name := str((boundary as Dictionary)["name"])
var staff_id := StringName(str((boundary as Dictionary)["staff_id"]))
var path := Scenes.READER_SAVE % name
if not _materialize(path, (boundary as Dictionary).get("files", {}), false):
continue
var fixture := _scenes.new_fixture(1)
var runtime: Variant = fixture[&"runtime"]
_expect(bool(runtime.load_from_path(path).get(&"ok", false)), "%s: this process loads the save made right after the command" % name)
var dangling: Array[String] = _scenes.dangling_references(runtime, staff_id)
_expect(dangling.is_empty(), "%s: no live reference to the dismissed employee after the load %s" % [name, dangling])
_expect(runtime.staff_service.validate_invariants()[&"ok"] and runtime.economy_service.validate_ledger()[&"ok"], "%s: ownership and the ledger are consistent after the load" % name)
_expect(_scenes.state_value(runtime, fixture[&"clock"]) == _decode((boundary as Dictionary)["at_command_base64"]), "%s: the loaded game is the writer's game at the command" % name)
var saved: Dictionary = runtime.create_snapshot()
_expect(runtime.restore_snapshot(saved) and runtime.create_snapshot() == saved, "%s: the loaded game saves and restores exactly" % name)
fixture[&"clock"].advance_minutes(int((boundary as Dictionary)["continue_minutes"]))
_expect(_scenes.state_value(runtime, fixture[&"clock"]) == _decode((boundary as Dictionary)["later_base64"]), "%s: it continues exactly as the writer's game" % name)
_scenes.free_fixture(fixture)
func _check_tampered(handoff: Dictionary) -> void:
var saves: Array = handoff.get("ledger_saves", [])
if saves.is_empty() or not saves[0] is Dictionary:
_expect(false, "a save package to change")
return
var path := Scenes.READER_SAVE % "tampered"
if not _materialize(path, (saves[0] as Dictionary).get("files", {}), true):
return
var fixture := _scenes.new_ledger_fixture()
var runtime: Variant = fixture[&"runtime"]
var before := _scenes.economy_bytes(runtime)
_expect(not bool(runtime.load_from_path(path).get(&"ok", true)), "a changed save package is refused")
_expect(_scenes.economy_bytes(runtime) == before and int(fixture[&"clock"].absolute_minute) == 360, "a refused load changes no Gold and no time")
_scenes.free_fixture(fixture)
# Writes the writer's exact package bytes into this process's own profile.
func _materialize(path: String, files: Variant, change_world: bool) -> bool:
var directory := ProjectSettings.globalize_path(path)
_expect(files is Dictionary and (files as Dictionary).has("manifest.json") and (files as Dictionary).has("world.bin"), "handoff package %s is complete" % path)
if not files is Dictionary:
return false
_expect(DirAccess.make_dir_recursive_absolute(directory) == OK, "package directory %s" % path)
for name: String in (files as Dictionary):
var pin: Dictionary = (files as Dictionary)[name]
var bytes := Marshalls.base64_to_raw(str(pin.get("base64", "")))
_expect(Scenes.sha256(bytes) == str(pin.get("sha256", "")) and bytes.size() == int(pin.get("bytes", -1)), "handoff bytes of %s are exact" % name)
if change_world and name == "world.bin" and not bytes.is_empty():
bytes[bytes.size() - 1] = bytes[bytes.size() - 1] ^ 0xff
var file := FileAccess.open(directory.path_join(name), FileAccess.WRITE)
_expect(file != null, "write %s into %s" % [name, path])
if file == null:
return false
file.store_buffer(bytes)
file.close()
return true
func _expect(condition: bool, message: String) -> void:
_checks += 1
if not condition:
_failures += 1
push_error(message)
@@ -0,0 +1 @@
uid://bcgouseodo7h6
+49
View File
@@ -438,6 +438,49 @@
"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
},
"tests/staff/test_general_dismissal.gd": {
"source_sha256": "f1f77bfd6320a3c8e0851915b150e95fc40f4a352876358393572a6d3f7c79ce",
"ordinary_cleanup_prepared": true,
"ordinary_control_labels": ["single"],
"old_bounded_teardown": false,
"new_audio_QA": false
},
"tests/staff/test_general_dismissal_restart_1_write.gd": {
"source_sha256": "b38745eccb4e51ddf4790051795e2f64faf10f875edc7b754ac2d04db2917a5b",
"ordinary_cleanup_prepared": false,
"ordinary_control_labels": [],
"old_bounded_teardown": false,
"new_audio_QA": false
},
"tests/staff/test_general_dismissal_restart_2_read.gd": {
"source_sha256": "4e13de2594096fab648fc8c418a1a4aa764ac72fc13fbd68cba1c6f05767fc83",
"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,10 +517,16 @@
"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"
],
"tests/staff/test_general_dismissal_restart_1_write.gd": [
"artifacts/p16-01-dismiss/restart-handoff.json"
],
"tests/transport/test_runtime_transport_comparison.gd": [
"artifacts/transport/runtime-comparison.json",
"artifacts/transport/runtime-comparison-1-trace.json",
+632
View File
@@ -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)
+631
View File
@@ -0,0 +1,631 @@
#!/usr/bin/env python3
"""Root-owned narrow P16-01-DISMISS (general employee dismissal with the A14 severance) collection using the
unchanged guarded phase protocol and strict verifier of tools/run_p07_weather_tests.py (itself the protocol of
tools/run_p05_funds_tests.py and tools/run_p18_relationship_tests.py). After the roster, the F8 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/staff/test_general_dismissal.gd',
'tests/ui/test_staff_management.gd',
'tests/ui/test_selection_departure.gd',
'tests/s3/test_runtime_staff_assignment.gd',
'tests/s3/test_staff_service.gd',
'tests/ui/test_anri_recruitment.gd',
'tests/ui/test_approved_free_unique_arrears_ui.gd',
'tests/s6/test_runtime_save_lifecycle.gd',
'tests/s6/test_runtime_snapshot_serialization.gd',
'tests/mvp/test_runtime_operations_integration.gd',
]
# P16-01-DISMISS F8 (SPEC section 9): the writer saves, the reader in the next phase's engine resumes.
RESTART = [
'tests/staff/test_general_dismissal_restart_1_write.gd',
'tests/staff/test_general_dismissal_restart_2_read.gd',
]
HANDOFF = 'artifacts/p16-01-dismiss/restart-handoff.json'
SUITES = ROSTER + RESTART
WRITER_LINE = r'(?m)^P16_RESTART_WRITER pid=([0-9]+) handoff_sha256=([0-9a-f]{64})$'
READER_LINE = r'(?m)^P16_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('p16_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-p16-01-dismiss-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('p16_diagnostics', shared.SUPPORT / 'diagnostic_gate.py')
owners = shared.module('p16_owners', shared.SUPPORT / 'ordinary_proof_gate.py')
audio = shared.module('p16_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-p16-01-dismiss-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-p16-01-dismiss-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-p16-01-dismiss-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-p16-01-dismiss-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_p16_dismiss_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('p16_verify_native', shared.SUPPORT / 'owned_diagnostic_process.py')
diagnostics = shared.module('p16_verify_diagnostics', shared.SUPPORT / 'diagnostic_gate.py')
owners = shared.module('p16_verify_owners', shared.SUPPORT / 'ordinary_proof_gate.py')
audio = shared.module('p16_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}/p16-dismiss-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('p16_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)