Save The Tree implementation and acceptance work in progress
Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://qpe5mfy3o1cw"
|
||||
path="res://.godot/imported/tree_theme_back_candidate.wav-1af8eac19cfe5ccb9a16a9a74b171ffd.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/bgm/tree_theme_back_candidate.wav"
|
||||
dest_files=["res://.godot/imported/tree_theme_back_candidate.wav-1af8eac19cfe5ccb9a16a9a74b171ffd.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=0
|
||||
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
[remap]
|
||||
|
||||
importer="wav"
|
||||
type="AudioStreamWAV"
|
||||
uid="uid://dr4atdrw3iv1o"
|
||||
path="res://.godot/imported/tree_theme_front_candidate.wav-dd40d47f468b616e1eb3517c0056cc0c.sample"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/bgm/tree_theme_front_candidate.wav"
|
||||
dest_files=["res://.godot/imported/tree_theme_front_candidate.wav-dd40d47f468b616e1eb3517c0056cc0c.sample"]
|
||||
|
||||
[params]
|
||||
|
||||
force/8_bit=false
|
||||
force/mono=false
|
||||
force/max_rate=false
|
||||
force/max_rate_hz=44100
|
||||
edit/trim=false
|
||||
edit/normalize=false
|
||||
edit/loop_mode=0
|
||||
edit/loop_begin=0
|
||||
edit/loop_end=-1
|
||||
compress/mode=0
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
|
||||
Interface Sounds (1.0)
|
||||
|
||||
Created/distributed by Kenney (www.kenney.nl)
|
||||
Creation date: 11-02-2020
|
||||
|
||||
------------------------------
|
||||
|
||||
License: (Creative Commons Zero, CC0)
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
This content is free to use in personal, educational and commercial projects.␍
|
||||
Support us by crediting Kenney or www.kenney.nl (this is not mandatory)
|
||||
|
||||
------------------------------
|
||||
|
||||
Donate: http://support.kenney.nl
|
||||
Patreon: http://patreon.com/kenney/
|
||||
|
||||
Follow on Twitter for updates:
|
||||
http://twitter.com/KenneyNL
|
||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="oggvorbisstr"
|
||||
type="AudioStreamOggVorbis"
|
||||
uid="uid://cg3ltmy3l1tut"
|
||||
path="res://.godot/imported/bong_001.ogg-036c448bb5d6a2a3d929d1a393030267.oggvorbisstr"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/ui/bong_001.ogg"
|
||||
dest_files=["res://.godot/imported/bong_001.ogg-036c448bb5d6a2a3d929d1a393030267.oggvorbisstr"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="oggvorbisstr"
|
||||
type="AudioStreamOggVorbis"
|
||||
uid="uid://b6gxkug03pmia"
|
||||
path="res://.godot/imported/click_001.ogg-faef30153c7c6ab499289b7b03f0bade.oggvorbisstr"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/ui/click_001.ogg"
|
||||
dest_files=["res://.godot/imported/click_001.ogg-faef30153c7c6ab499289b7b03f0bade.oggvorbisstr"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="oggvorbisstr"
|
||||
type="AudioStreamOggVorbis"
|
||||
uid="uid://csfga2avhq4ul"
|
||||
path="res://.godot/imported/confirmation_001.ogg-3e5a493cd453540435a25930de468964.oggvorbisstr"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/ui/confirmation_001.ogg"
|
||||
dest_files=["res://.godot/imported/confirmation_001.ogg-3e5a493cd453540435a25930de468964.oggvorbisstr"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="oggvorbisstr"
|
||||
type="AudioStreamOggVorbis"
|
||||
uid="uid://cwdh8pkwvyuf2"
|
||||
path="res://.godot/imported/error_008.ogg-41fd046ecd9f266bdea4813897127346.oggvorbisstr"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/ui/error_008.ogg"
|
||||
dest_files=["res://.godot/imported/error_008.ogg-41fd046ecd9f266bdea4813897127346.oggvorbisstr"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"official_page": "https://kenney.nl/assets/interface-sounds",
|
||||
"official_archive_sha256": "f2193d072726d6758a5f7871b2dcc54dcce0d5c35c6f0a62f92549b327c81232",
|
||||
"license": {
|
||||
"name": "Creative Commons Zero, CC0",
|
||||
"official_url": "https://creativecommons.org/publicdomain/zero/1.0/",
|
||||
"path": "assets/audio/ui/License.txt",
|
||||
"sha256": "f7966c773bbed0eca6a9c75081c44a178b38eae112724dbb5fdfbd4192d118a9",
|
||||
"original_byte_preserved": true
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"role": "click",
|
||||
"path": "assets/audio/ui/click_001.ogg",
|
||||
"sha256": "ccfb7fa0cccdd9faec0eb16033c732b1e308d139d80f799161495d58f7adcdb9",
|
||||
"bytes": 4876
|
||||
},
|
||||
{
|
||||
"role": "success",
|
||||
"path": "assets/audio/ui/confirmation_001.ogg",
|
||||
"sha256": "063564703b6094d70718a3e787a55cc9141611e4ecd6b6637f8828f79b4a8c3a",
|
||||
"bytes": 8968
|
||||
},
|
||||
{
|
||||
"role": "refusal",
|
||||
"path": "assets/audio/ui/error_008.ogg",
|
||||
"sha256": "eba17ecb2a426bfd4a8a6acff5f8a86202b6424a77af0fd842e37809a1ab6d81",
|
||||
"bytes": 9215
|
||||
},
|
||||
{
|
||||
"role": "notification",
|
||||
"path": "assets/audio/ui/bong_001.ogg",
|
||||
"sha256": "d21d0f0b782445db579d11e2506b24cd1ac9d664ee33aeaf807761aa7b6fd710",
|
||||
"bytes": 4848
|
||||
}
|
||||
],
|
||||
"usage": "UI click, committed action success/refusal, new foreground notification"
|
||||
}
|
||||
+296
-26
@@ -22,7 +22,9 @@ const WAGE_EXPENSE_ACCOUNT := &"WAGE_EXPENSE"
|
||||
const RUNTIME_SNAPSHOT_VERSION := 14
|
||||
const AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION := 14
|
||||
const EVENT_THREAT_RUNTIME_SNAPSHOT_VERSION := 13
|
||||
const AutoPurchasePlansType := preload("res://scripts/supply/supply_auto_purchase_plans.gd")
|
||||
const MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION := 15
|
||||
const PurchaseQuoteV2 := preload("res://scripts/supply/supply_purchase_quote_v2.gd")
|
||||
const AutoPurchasePlansType := preload("res://scripts/supply/supply_auto_purchase_quote_plans.gd")
|
||||
const CLEANING_RUNTIME_SNAPSHOT_VERSION := 12
|
||||
const WEATHER_RUNTIME_SNAPSHOT_VERSION := 11
|
||||
const FacilityCleanliness = preload("res://scripts/environment/facility_cleanliness_service.gd")
|
||||
@@ -292,6 +294,7 @@ func initialize(clock: SimulationKernel, state: Variant, seed := DEFAULT_WORLD_S
|
||||
_executors.clear()
|
||||
register_command_executor(&"change_resources", Callable(self, "_execute_change_resources"))
|
||||
register_command_executor(&"spend_resources", Callable(self, "_execute_spend_resources"))
|
||||
register_command_executor(&"repay_loan_principal", Callable(self, "_execute_repay_loan_principal"))
|
||||
register_command_executor(&"build_facility", Callable(self, "_execute_build_facility"))
|
||||
register_command_executor(&"cancel_construction", Callable(self, "_execute_cancel_construction"))
|
||||
register_command_executor(&"demolish_facility", Callable(self, "_execute_demolish_facility"))
|
||||
@@ -559,6 +562,8 @@ func create_snapshot() -> Dictionary:
|
||||
&"facility_infrastructure": facility_infrastructure_projector.create_snapshot(),
|
||||
},
|
||||
}
|
||||
if PurchaseQuoteV2.has_history(owned_systems[&"runtime"]):
|
||||
owned_systems[&"runtime"][&"runtime_schema_version"] = MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION
|
||||
# Every component above returns detached containers. Transfer this fresh
|
||||
# tree into the envelope instead of deep-copying all component snapshots a
|
||||
# second time. Keep generic capture's caller-owned-extra contract unchanged.
|
||||
@@ -616,8 +621,12 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
CLEANING_RUNTIME_SNAPSHOT_VERSION,
|
||||
EVENT_THREAT_RUNTIME_SNAPSHOT_VERSION,
|
||||
RUNTIME_SNAPSHOT_VERSION,
|
||||
MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION,
|
||||
]:
|
||||
return false
|
||||
var has_purchase_v2_history := PurchaseQuoteV2.has_history(runtime_data)
|
||||
if (runtime_schema_version == MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION) != has_purchase_v2_history:
|
||||
return false
|
||||
if runtime_schema_version < OPERATIONS_RUNTIME_SNAPSHOT_VERSION \
|
||||
and not _legacy_operations_history_is_absent(runtime_data):
|
||||
return false
|
||||
@@ -840,6 +849,8 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
var restored_economy := EconomyService.new()
|
||||
if not restored_economy.restore_snapshot(runtime_data[&"economy"]):
|
||||
return false
|
||||
if not _loan_principal_ledger_is_valid(restored_economy):
|
||||
return false
|
||||
if runtime_schema_version < OPERATIONS_RUNTIME_SNAPSHOT_VERSION:
|
||||
if not restored_economy.has_account(WAGE_EXPENSE_ACCOUNT) \
|
||||
and not restored_economy.register_account(WAGE_EXPENSE_ACCOUNT):
|
||||
@@ -1221,6 +1232,9 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo
|
||||
return false
|
||||
if restored_kitchen.current_minute != restored_runtime_domain_minute or not restored_kitchen.validate_crosslinks():
|
||||
return false
|
||||
if runtime_schema_version == MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION \
|
||||
and not PurchaseQuoteV2.valid_history(runtime_data, restored_economy):
|
||||
return false
|
||||
var restored_auto_purchase := AutoPurchasePlansType.new()
|
||||
if runtime_schema_version >= AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION:
|
||||
if not restored_auto_purchase.restore_snapshot(runtime_data[&"auto_purchase_plans"], restored_facilities, restored_economy, restored_runtime_domain_minute):
|
||||
@@ -2561,6 +2575,8 @@ func _build_operations_daily_financials(
|
||||
) -> Dictionary:
|
||||
if period_start < 0 or period_end_exclusive <= period_start or new_wages_total < 0:
|
||||
return {&"ok": false, &"reason": &"DAILY_FINANCIAL_PERIOD_INVALID"}
|
||||
if not _loan_principal_ledger_is_valid(economy_service):
|
||||
return {&"ok": false, &"reason": &"INVALID_LOAN_PRINCIPAL_LEDGER"}
|
||||
var category_totals := {
|
||||
&"revenue": 0,
|
||||
&"refunds": 0,
|
||||
@@ -2590,6 +2606,8 @@ func _build_operations_daily_financials(
|
||||
&"EVENT_COST_COMMITTED": &"cost_of_goods",
|
||||
&"DEFENSE_SKILL_COST": &"cost_of_goods",
|
||||
&"MAINTENANCE_ACCRUAL": &"maintenance",
|
||||
&"LOAN_PRINCIPAL_BORROWED": &"financing_principal_inflows",
|
||||
&"LOAN_PRINCIPAL_REPAID": &"financing_principal_outflows",
|
||||
}
|
||||
var operating_refund_total := 0
|
||||
for entry: LedgerEntry in economy_service.get_entries():
|
||||
@@ -2609,10 +2627,10 @@ func _build_operations_daily_financials(
|
||||
&"transaction_id": entry.transaction_id,
|
||||
}
|
||||
var category: StringName = category_mapping[entry.category]
|
||||
category_totals[category] = int(category_totals[category]) + entry.amount
|
||||
category_totals[category] = int(category_totals.get(category, 0)) + entry.amount
|
||||
if not entry.source_entity_id.is_empty() \
|
||||
and (facility_service.has_instance(entry.source_entity_id) or entry.category == &"MAINTENANCE_ACCRUAL") \
|
||||
and category != &"construction_cash_flow":
|
||||
and category not in [&"construction_cash_flow", &"financing_principal_inflows", &"financing_principal_outflows"]:
|
||||
if not facility_rows.has(entry.source_entity_id):
|
||||
facility_rows[entry.source_entity_id] = {
|
||||
&"revenue": 0, &"expenses": 0, &"net_profit": 0,
|
||||
@@ -3502,6 +3520,108 @@ func _operations_payroll_crosslinks_are_valid(
|
||||
return true
|
||||
|
||||
|
||||
## Existing liabilities only. This projection does not issue financing or infer terms.
|
||||
func get_loan_principal_report() -> Dictionary:
|
||||
if not _initialized or economy_service == null or _state == null:
|
||||
return {&"available": false, &"loans": []}
|
||||
var cash := economy_service.get_balance(PLAYER_CASH_ACCOUNT)
|
||||
var pending: Dictionary = {}
|
||||
for command: SimulationCommand in command_queue.get_pending_commands():
|
||||
if command.command_type != &"repay_loan_principal":
|
||||
continue
|
||||
var id: Variant = command.payload.get(&"loan_id", null)
|
||||
if typeof(id) not in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||
continue
|
||||
var key := StringName(id)
|
||||
if not pending.has(key): pending[key] = []
|
||||
pending[key].append(command.command_id)
|
||||
var loans: Array[Dictionary] = []
|
||||
for account: Dictionary in economy_service.get_accounts():
|
||||
var id := String(account[&"account_id"])
|
||||
if not id.begins_with("LOAN_LIABILITY:"):
|
||||
continue
|
||||
var loan := StringName(id.trim_prefix("LOAN_LIABILITY:"))
|
||||
loans.append({&"loan_id": loan, &"outstanding_principal": -economy_service.get_balance(StringName(id)), &"pending_command_ids": (pending.get(loan, []) as Array).duplicate()})
|
||||
loans.sort_custom(func(left: Dictionary, right: Dictionary) -> bool: return String(left[&"loan_id"]) < String(right[&"loan_id"]))
|
||||
var valid: bool = _state.get_resource(&"gold") == cash and (loans.is_empty() or _loan_principal_ledger_is_valid(economy_service))
|
||||
var ended: bool = _operations_enabled and operations_coordinator != null and operations_coordinator.is_terminal()
|
||||
return {&"available": true, &"ledger_valid": valid, &"terminal": ended, &"cash": cash, &"next_command_id": _next_command_id, &"loans": loans}
|
||||
|
||||
|
||||
## Atomic principal payment against an already attested loan liability.
|
||||
## Loan issuance, rates, due dates and bank eligibility belong to the bank
|
||||
## producer; this API does not create or approve any financing contract.
|
||||
func repay_loan_principal(
|
||||
loan_id: StringName,
|
||||
amount: Variant,
|
||||
idempotency_key: StringName
|
||||
) -> CommandResult:
|
||||
if not _initialized or economy_service == null or _state == null:
|
||||
return CommandResult.failed(CommandResult.Code.EXECUTOR_FAILED, 0, "Simulation runtime is not initialized.")
|
||||
if _operations_enabled and operations_coordinator != null and operations_coordinator.is_terminal():
|
||||
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Repayment is unavailable after the game ends.")
|
||||
if loan_id.is_empty() or idempotency_key.is_empty() or typeof(amount) != TYPE_INT \
|
||||
or amount <= 0 or amount > OperationsCoordinator.MAX_FINANCIAL_AMOUNT:
|
||||
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, 0, "A loan id, positive integer principal and idempotency key are required.")
|
||||
if not economy_service.get_transaction_id_for_idempotency_key(idempotency_key).is_empty():
|
||||
return CommandResult.failed(CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY, 0, "The principal payment key has already been posted.")
|
||||
if not _loan_principal_ledger_is_valid(economy_service):
|
||||
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "The principal ledger is inconsistent.")
|
||||
var liability := StringName("LOAN_LIABILITY:" + String(loan_id))
|
||||
if not economy_service.has_account(liability):
|
||||
return CommandResult.failed(CommandResult.Code.NOT_FOUND, 0, "The loan liability does not exist.")
|
||||
var remaining := -economy_service.get_balance(liability)
|
||||
var cash := economy_service.get_balance(PLAYER_CASH_ACCOUNT)
|
||||
if amount > remaining or amount > cash or _state.get_resource(&"gold") != cash:
|
||||
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, 0, "Principal payment exceeds the outstanding principal or available Gold, or its mirror is inconsistent.")
|
||||
# The single balanced posting is atomic. Defer receipts until the Gold
|
||||
# mirror is synchronized, also respecting any enclosing effect scope.
|
||||
economy_service.begin_notification_scope()
|
||||
var result := economy_service.transfer(
|
||||
PLAYER_CASH_ACCOUNT, liability, amount, _runtime_event_minute(),
|
||||
&"LOAN_PRINCIPAL_REPAID", loan_id, idempotency_key
|
||||
)
|
||||
if result.is_success():
|
||||
_state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT))
|
||||
result.details[&"loan_id"] = loan_id
|
||||
result.details[&"paid_principal"] = amount
|
||||
result.details[&"outstanding_principal"] = remaining - amount
|
||||
economy_service.end_notification_scope()
|
||||
return result
|
||||
|
||||
|
||||
func _loan_principal_ledger_is_valid(economy: EconomyService) -> bool:
|
||||
if economy == null:
|
||||
return false
|
||||
var outstanding: Dictionary = {}
|
||||
for entry: LedgerEntry in economy.get_entries():
|
||||
if entry.category not in [&"LOAN_PRINCIPAL_BORROWED", &"LOAN_PRINCIPAL_REPAID"]:
|
||||
continue
|
||||
if entry.source_entity_id.is_empty():
|
||||
return false
|
||||
var liability := StringName("LOAN_LIABILITY:" + String(entry.source_entity_id))
|
||||
if not economy.has_account(liability) or not economy.account_allows_negative(liability):
|
||||
return false
|
||||
var remaining := int(outstanding.get(liability, 0))
|
||||
if entry.category == &"LOAN_PRINCIPAL_BORROWED":
|
||||
if entry.debit_account != PLAYER_CASH_ACCOUNT or entry.credit_account != liability \
|
||||
or entry.amount > OperationsCoordinator.MAX_FINANCIAL_AMOUNT - remaining:
|
||||
return false
|
||||
remaining += entry.amount
|
||||
else:
|
||||
if entry.debit_account != liability or entry.credit_account != PLAYER_CASH_ACCOUNT \
|
||||
or entry.amount > remaining:
|
||||
return false
|
||||
remaining -= entry.amount
|
||||
outstanding[liability] = remaining
|
||||
for account: Dictionary in economy.get_accounts():
|
||||
var id: StringName = account[&"account_id"]
|
||||
if String(id).begins_with("LOAN_LIABILITY:"):
|
||||
if not economy.account_allows_negative(id) or economy.get_balance(id) != -int(outstanding.get(id, 0)):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _operations_expected_category_totals(
|
||||
economy: EconomyService,
|
||||
period_start: int,
|
||||
@@ -3511,6 +3631,8 @@ func _operations_expected_category_totals(
|
||||
if economy == null or period_start < 0 or period_end_exclusive <= period_start \
|
||||
or new_wages_total < 0:
|
||||
return null
|
||||
if not _loan_principal_ledger_is_valid(economy):
|
||||
return null
|
||||
var totals := {
|
||||
&"revenue": 0, &"refunds": 0, &"cost_of_goods": 0,
|
||||
&"wages": new_wages_total, &"maintenance": 0, &"utilities": 0,
|
||||
@@ -3531,6 +3653,8 @@ func _operations_expected_category_totals(
|
||||
&"EVENT_COST_COMMITTED": &"cost_of_goods",
|
||||
&"DEFENSE_SKILL_COST": &"cost_of_goods",
|
||||
&"MAINTENANCE_ACCRUAL": &"maintenance",
|
||||
&"LOAN_PRINCIPAL_BORROWED": &"financing_principal_inflows",
|
||||
&"LOAN_PRINCIPAL_REPAID": &"financing_principal_outflows",
|
||||
}
|
||||
var operating_refunds := 0
|
||||
for entry: LedgerEntry in economy.get_entries():
|
||||
@@ -3545,7 +3669,7 @@ func _operations_expected_category_totals(
|
||||
if not mapping.has(entry.category):
|
||||
return null
|
||||
var category: StringName = mapping[entry.category]
|
||||
totals[category] = int(totals[category]) + entry.amount
|
||||
totals[category] = int(totals.get(category, 0)) + entry.amount
|
||||
var offset := mini(operating_refunds, int(totals[&"cost_of_goods"]))
|
||||
totals[&"cost_of_goods"] = int(totals[&"cost_of_goods"]) - offset
|
||||
totals[&"revenue"] = int(totals[&"revenue"]) + operating_refunds - offset
|
||||
@@ -4438,6 +4562,23 @@ func _perform_checkpoint_save(
|
||||
&"message": "Simulation runtime is not initialized.",
|
||||
}
|
||||
var started_at_us := Time.get_ticks_usec()
|
||||
# A rejected Path projection is not a complete Runtime save. Refuse before
|
||||
# pausing or touching formal/tmp/backup files, and keep the refusal observable
|
||||
# without publishing a persisted event into the live snapshot.
|
||||
if path_service.create_snapshot().is_empty():
|
||||
var refused: Dictionary = {
|
||||
&"ok": false, &"code": &"INVALID_SNAPSHOT",
|
||||
&"message": "Incomplete Path snapshot; original save preserved.",
|
||||
&"operation": &"autosave" if use_autosave else &"save",
|
||||
&"save_path": save_path, &"reason": reason, &"request_id": request_id,
|
||||
&"checkpoint_absolute_minute": _clock.absolute_minute,
|
||||
&"runtime_duration_us": maxi(int(Time.get_ticks_usec() - started_at_us), 0),
|
||||
&"clock_pause_restored": true, &"formal_save_preserved": true,
|
||||
&"live_state_preserved": true,
|
||||
}
|
||||
latest_save_operation_result = _save_event_payload(refused)
|
||||
save_operation_completed.emit(refused.duplicate(true))
|
||||
return refused
|
||||
var previous_paused := _clock.paused
|
||||
_clock.set_paused(true)
|
||||
var snapshot := create_snapshot()
|
||||
@@ -5241,6 +5382,7 @@ func _configure_relationship_event_boundaries() -> void:
|
||||
Callable(self, "_on_relationship_threshold_candidate")
|
||||
)
|
||||
event_service.configure_relationship_service(relationship_service)
|
||||
event_service.configure_candidate_eligibility_guard(Callable(self, "_event_candidate_placement_permitted"))
|
||||
event_service.configure_callbacks(
|
||||
Callable(self, "_event_actor_exists"),
|
||||
Callable(self, "_execute_event_action_boundary"),
|
||||
@@ -5251,6 +5393,32 @@ func _configure_relationship_event_boundaries() -> void:
|
||||
Callable(self, "_event_path_exists")
|
||||
)
|
||||
|
||||
## Original 6.2/6.4: Tomari's placement prevents floor trouble. This does not
|
||||
## choose a joining date, probability, defense score, or narrative content.
|
||||
func is_floor_trouble_protected(floor_y: int) -> bool:
|
||||
var tomari := staff_service.get_staff(&"heroine_tomari")
|
||||
if tomari == null or tomari.employment_kind != StaffRuntime.EmploymentKind.UNIQUE:
|
||||
return false
|
||||
var assignment := staff_service.get_work_assignment(tomari.staff_id)
|
||||
var facility := facility_service.get_instance(StringName(assignment.get(&"facility_id", &"")))
|
||||
return facility != null and facility.origin.y == floor_y
|
||||
|
||||
|
||||
func _event_candidate_placement_permitted(definition: EventDefinition, context: Dictionary) -> bool:
|
||||
if definition.category != &"TROUBLE":
|
||||
return true
|
||||
# A business trouble identifies its existing target facility. Missing or
|
||||
# conflicting location data cannot silently bypass placement protection.
|
||||
var target: Variant = context.get(&"facility_id", definition.location_rule.get(&"facility_id", null))
|
||||
if typeof(target) not in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||
return false
|
||||
var declared: Variant = definition.location_rule.get(&"facility_id", target)
|
||||
if typeof(declared) not in [TYPE_STRING, TYPE_STRING_NAME] or StringName(declared) != StringName(target):
|
||||
return false
|
||||
var facility := facility_service.get_instance(StringName(target))
|
||||
return facility != null and not is_floor_trouble_protected(facility.origin.y)
|
||||
|
||||
|
||||
func _connect_relationship_signals() -> void:
|
||||
if relationship_service == null:
|
||||
return
|
||||
@@ -5267,6 +5435,8 @@ func _connect_event_signals() -> void:
|
||||
event_service.event_finished.connect(Callable(self, "_on_event_finished"))
|
||||
event_service.event_cancelled.connect(Callable(self, "_on_event_cancelled"))
|
||||
event_service.choice_resolved.connect(Callable(self, "_on_event_choice_resolved"))
|
||||
event_service.effect_transaction_started.connect(Callable(self, "_on_event_effect_transaction_started"))
|
||||
event_service.effect_transaction_finished.connect(Callable(self, "_on_event_effect_transaction_finished"))
|
||||
|
||||
func _initialize_s5_authority() -> void:
|
||||
authority_service = AuthorityService.new()
|
||||
@@ -5646,6 +5816,20 @@ func _execute_change_resources(command: SimulationCommand) -> CommandResult:
|
||||
&"faith_lifetime": int(progression_service.metrics[&"faith_lifetime"]),
|
||||
})
|
||||
|
||||
# Repay an existing attested liability through the ordinary command queue.
|
||||
# No rate, issuance, eligibility or due-date values are supplied here.
|
||||
func _execute_repay_loan_principal(command: SimulationCommand) -> CommandResult:
|
||||
var loan_id: Variant = command.payload.get(&"loan_id", null)
|
||||
if typeof(loan_id) not in [TYPE_STRING, TYPE_STRING_NAME] or String(loan_id).is_empty():
|
||||
return CommandResult.failed(CommandResult.Code.INVALID_PAYLOAD, command.command_id, "loan_id must be a nonempty String or StringName.")
|
||||
var result := repay_loan_principal(
|
||||
StringName(loan_id), command.payload.get(&"amount", null), command.idempotency_key
|
||||
)
|
||||
result.command_id = command.command_id
|
||||
result.target_id = StringName(loan_id)
|
||||
return result
|
||||
|
||||
|
||||
func _execute_spend_resources(command: SimulationCommand) -> CommandResult:
|
||||
var cost: Variant = command.payload.get(&"cost", command.payload.get("cost", null))
|
||||
if not cost is Dictionary:
|
||||
@@ -6428,6 +6612,17 @@ func _rollback_staff_candidate_hire(snapshot: Dictionary) -> bool:
|
||||
_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
|
||||
# Existing public-bath data uses a legacy job ID; the paid roster exposes
|
||||
# the canonical attendant. The other aliases likewise retain saved job IDs.
|
||||
if definition.id == &"public_bath" and role_id == &"attendant" and &"bath_attendant" in definition.required_roles:
|
||||
return true
|
||||
if definition.id == &"massage_room" and role_id == &"masseur" and &"therapist" in definition.required_roles:
|
||||
return true
|
||||
return definition.id == &"security_station" and role_id == &"guard" and &"security" in definition.required_roles
|
||||
|
||||
func _execute_set_staff_work_assignment(command: SimulationCommand) -> CommandResult:
|
||||
for key: StringName in [&"staff_id", &"facility_id", &"template_id"]:
|
||||
if typeof(command.payload.get(key, null)) not in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||
@@ -6456,7 +6651,7 @@ func _execute_set_staff_work_assignment(command: SimulationCommand) -> CommandRe
|
||||
var definition := definition_registry.get_facility(facility.definition_id)
|
||||
if definition == null or definition.staff_max <= 0:
|
||||
return _staff_assignment_failure(command.command_id, &"FACILITY_UNSTAFFED", "This facility does not accept Staff assignments.")
|
||||
if not definition.required_roles.is_empty() and staff.role_id not in definition.required_roles and not _is_anri_manager(staff):
|
||||
if not _staff_role_matches_facility(staff.role_id, definition) and not _is_anri_manager(staff):
|
||||
return _staff_assignment_failure(command.command_id, &"ROLE_MISMATCH", "Staff role does not match this facility.")
|
||||
if _staff_assignment_count(facility_id, template_id, staff_id) >= definition.staff_max:
|
||||
return _staff_assignment_failure(command.command_id, &"FACILITY_FULL", "This shift has reached the facility Staff limit.")
|
||||
@@ -7193,6 +7388,8 @@ func _execute_create_service_request(command: SimulationCommand) -> CommandResul
|
||||
options[&"quoted_price"] = quoted_price
|
||||
if not instance.operator_open or not instance.is_within_opening_hours(_runtime_event_minute()):
|
||||
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "This facility is closed to new service requests.", {&"reason": &"FACILITY_CLOSED"})
|
||||
if instance.requires_operating_kitchen() and not bool(facility_service.get_required_kitchen_report(facility_id).get(&"operating", false)):
|
||||
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "This dining facility requires an operating BACK kitchen.", {&"reason": &"FACILITY_CLOSED", &"dependency_reason": facility_service.get_required_kitchen_report(facility_id).get(&"reason", &"")})
|
||||
var payer_account := _guest_wallet_account(guest_ids[0])
|
||||
var committed_price := _outstanding_service_commitment(guest_ids[0])
|
||||
if not economy_service.account_allows_negative(payer_account) \
|
||||
@@ -7253,7 +7450,7 @@ func _execute_create_service_request(command: SimulationCommand) -> CommandResul
|
||||
if guest_service.has_guest(guest_id):
|
||||
runtime_guests.append(guest_id)
|
||||
if runtime_guests.is_empty():
|
||||
enqueue_result = facility_service.enqueue_request(request, _runtime_event_minute())
|
||||
enqueue_result = facility_service.enqueue_request(request, _runtime_event_minute(), not (_operations_enabled and _active_command_minute == _clock.absolute_minute + 1))
|
||||
else:
|
||||
var first_guest := runtime_guests[0]
|
||||
var queue_result := guest_service.enter_queue(
|
||||
@@ -7263,7 +7460,7 @@ func _execute_create_service_request(command: SimulationCommand) -> CommandResul
|
||||
_runtime_event_minute(),
|
||||
int(options.get(&"initial_patience", options.get("initial_patience", -1))),
|
||||
func(_guest_id: StringName, _facility_id: StringName, _request_id: StringName) -> CommandResult:
|
||||
return facility_service.enqueue_request(request, _runtime_event_minute())
|
||||
return facility_service.enqueue_request(request, _runtime_event_minute(), not (_operations_enabled and _active_command_minute == _clock.absolute_minute + 1))
|
||||
)
|
||||
if bool(queue_result.get(&"ok", false)):
|
||||
for guest_index in range(1, runtime_guests.size()):
|
||||
@@ -7598,7 +7795,8 @@ func _capture_infrastructure_demolition_state() -> Dictionary:
|
||||
|
||||
func _restore_infrastructure_demolition_state(snapshot: Dictionary) -> bool:
|
||||
for key: StringName in [
|
||||
&"game_state", &"economy", &"world", &"path", &"elevators", &"legacy_stairs", &"construction",
|
||||
&"game_state", &"economy", &"facility_upkeep", &"upkeep_settlement",
|
||||
&"world", &"path", &"elevators", &"legacy_stairs", &"construction",
|
||||
&"excavation", &"spatial_subslots", &"route_projection",
|
||||
&"facility_infrastructure", &"facilities", &"facility_cleanliness", &"mana", &"movement_credits",
|
||||
&"domain_events", &"id_generator",
|
||||
@@ -7607,6 +7805,8 @@ func _restore_infrastructure_demolition_state(snapshot: Dictionary) -> bool:
|
||||
return false
|
||||
var restored: bool = bool(_state.restore_snapshot(snapshot[&"game_state"]))
|
||||
restored = economy_service.restore_snapshot(snapshot[&"economy"]) and restored
|
||||
restored = facility_upkeep.restore_snapshot(snapshot[&"facility_upkeep"]) and restored
|
||||
restored = upkeep_settlement.restore_snapshot(snapshot[&"upkeep_settlement"], economy_service) and restored
|
||||
restored = world_service.restore_snapshot(snapshot[&"world"]) and restored
|
||||
restored = spatial_subslot_service.restore_snapshot(snapshot[&"spatial_subslots"]) and restored
|
||||
var route_snapshot: Dictionary = snapshot[&"route_projection"]
|
||||
@@ -7645,6 +7845,8 @@ func _restore_infrastructure_demolition_state(snapshot: Dictionary) -> bool:
|
||||
func _infrastructure_demolition_state_matches(snapshot: Dictionary) -> bool:
|
||||
return _strict_variants_equal(_state.create_snapshot(), snapshot.get(&"game_state", {})) \
|
||||
and _strict_variants_equal(economy_service.create_snapshot(), snapshot.get(&"economy", {})) \
|
||||
and _strict_variants_equal(facility_upkeep.create_snapshot(), snapshot.get(&"facility_upkeep", {})) \
|
||||
and _strict_variants_equal(upkeep_settlement.create_snapshot(), snapshot.get(&"upkeep_settlement", {})) \
|
||||
and _strict_variants_equal(world_service.create_snapshot(), snapshot.get(&"world", {})) \
|
||||
and _strict_variants_equal(path_service.create_snapshot(), snapshot.get(&"path", {})) \
|
||||
and _strict_variants_equal(elevators.create_snapshot(), snapshot.get(&"elevators", {})) \
|
||||
@@ -9020,6 +9222,10 @@ func _project_guest_candidates(guest_id: StringName) -> Array:
|
||||
var candidates: Array = []
|
||||
if guest == null:
|
||||
return candidates
|
||||
var query_goals: Array = []
|
||||
for instance: FacilityInstance in facility_service.get_instances():
|
||||
query_goals.append_array(_facility_access_candidates(instance))
|
||||
path_service.begin_guest_path_query_batch(guest.current_cell, query_goals)
|
||||
for facility: FacilityInstance in facility_service.get_instances():
|
||||
var definition := definition_registry.get_facility(facility.definition_id)
|
||||
if definition == null or &"staff_only" in definition.tags:
|
||||
@@ -9058,6 +9264,7 @@ func _project_guest_candidates(guest_id: StringName) -> Array:
|
||||
&"graph_revision": path_service.graph_revision,
|
||||
},
|
||||
})
|
||||
path_service.end_guest_path_query_batch()
|
||||
return candidates
|
||||
|
||||
func _facility_access_candidates(facility: FacilityInstance) -> Array:
|
||||
@@ -9089,7 +9296,7 @@ func _find_facility_access_path(start: Vector3i, facility: FacilityInstance) ->
|
||||
var path := path_service.find_path(start, candidate)
|
||||
if path.is_empty():
|
||||
continue
|
||||
if best_path.is_empty() or path_service.calculate_path_cost(path) < path_service.calculate_path_cost(best_path):
|
||||
if best_path.is_empty() or path_service.calculate_guest_batch_path_cost(start, candidate, path) < path_service.calculate_guest_batch_path_cost(start, best_cell, best_path):
|
||||
best_path.assign(path)
|
||||
best_cell = candidate
|
||||
return {&"reachable": not best_path.is_empty(), &"path": best_path, &"target_cell": best_cell}
|
||||
@@ -9689,7 +9896,11 @@ func _execute_event_action_boundary(
|
||||
token[&"defense"] = defense_service.create_snapshot()
|
||||
var result := _apply_event_action(action, context)
|
||||
if not bool(result.get(&"ok", false)):
|
||||
_restore_event_action_token(token)
|
||||
var rollback := _restore_event_action_token(token)
|
||||
if not bool(rollback.get(&"ok", false)):
|
||||
result[&"fatal"] = true
|
||||
result[&"rollback"] = rollback
|
||||
result[&"error"] = "Event Action failed and rollback failed: %s; %s" % [result.get(&"error", ""), rollback.get(&"error", "")]
|
||||
return result
|
||||
result[&"token"] = token
|
||||
return result
|
||||
@@ -9886,8 +10097,19 @@ func _reserve_event_cost(cost: Dictionary, context: Dictionary) -> Dictionary:
|
||||
normalized[resource_id] = amount
|
||||
var economy_before := economy_service.create_snapshot()
|
||||
var state_before: Dictionary = _state.create_snapshot()
|
||||
var ids_before := id_generator.create_snapshot()
|
||||
var instance_id := StringName(context.get(&"event_instance_id", ""))
|
||||
var reservation_id: StringName = &""
|
||||
for amount: int in normalized.values():
|
||||
if amount > 0:
|
||||
if id_generator.get_counter(&"event_cost") >= StableIdGenerator.MAX_COUNTER:
|
||||
return {&"ok": false, &"error": "Event cost reservation IDs are exhausted."}
|
||||
reservation_id = id_generator.next_id(&"event_cost")
|
||||
break
|
||||
var escrow_account := StringName("EVENT_ESCROW:%s" % instance_id)
|
||||
var reservation := {&"event_instance_id": instance_id, &"cost": normalized, &"escrow_account": escrow_account}
|
||||
if not reservation_id.is_empty():
|
||||
reservation[&"reservation_id"] = reservation_id
|
||||
var gold := int(normalized.get(&"gold", 0))
|
||||
if gold > 0:
|
||||
if not economy_service.has_account(escrow_account):
|
||||
@@ -9895,23 +10117,21 @@ func _reserve_event_cost(cost: Dictionary, context: Dictionary) -> Dictionary:
|
||||
var transfer := economy_service.transfer(
|
||||
PLAYER_CASH_ACCOUNT, escrow_account, gold, _runtime_event_minute(),
|
||||
&"EVENT_COST_RESERVED", instance_id,
|
||||
StringName("event.cost.reserve.%s" % instance_id)
|
||||
_event_cost_ledger_key(reservation, &"reserve")
|
||||
)
|
||||
if not transfer.is_success():
|
||||
economy_service.restore_snapshot(economy_before)
|
||||
_state.restore_snapshot(state_before)
|
||||
id_generator.restore_snapshot(ids_before)
|
||||
return {&"ok": false, &"error": transfer.message}
|
||||
var other_costs := normalized.duplicate(true)
|
||||
other_costs.erase(&"gold")
|
||||
if not other_costs.is_empty() and not _state.spend(other_costs):
|
||||
economy_service.restore_snapshot(economy_before)
|
||||
_state.restore_snapshot(state_before)
|
||||
id_generator.restore_snapshot(ids_before)
|
||||
return {&"ok": false, &"error": "Event Choice resources are unavailable."}
|
||||
return {&"ok": true, &"token": {
|
||||
&"event_instance_id": instance_id,
|
||||
&"cost": normalized,
|
||||
&"escrow_account": escrow_account,
|
||||
}}
|
||||
return {&"ok": true, &"token": reservation}
|
||||
|
||||
func _release_event_cost(token: Variant, _context: Dictionary) -> Dictionary:
|
||||
if token is Dictionary and token.is_empty():
|
||||
@@ -9926,7 +10146,7 @@ func _release_event_cost(token: Variant, _context: Dictionary) -> Dictionary:
|
||||
var released := economy_service.transfer(
|
||||
escrow_account, PLAYER_CASH_ACCOUNT, gold, _runtime_event_minute(),
|
||||
&"EVENT_COST_RELEASED", instance_id,
|
||||
StringName("event.cost.release.%s" % instance_id)
|
||||
_event_cost_ledger_key(token, &"release")
|
||||
)
|
||||
if not released.is_success():
|
||||
return {&"ok": false, &"error": released.message}
|
||||
@@ -9949,10 +10169,17 @@ func _commit_event_cost(token: Variant, _context: Dictionary) -> Dictionary:
|
||||
var committed := economy_service.transfer(
|
||||
StringName(token.get(&"escrow_account", "")), OPERATING_EXPENSE_ACCOUNT,
|
||||
gold, _runtime_event_minute(), &"EVENT_COST_COMMITTED", instance_id,
|
||||
StringName("event.cost.commit.%s" % instance_id)
|
||||
_event_cost_ledger_key(token, &"commit")
|
||||
)
|
||||
return {&"ok": committed.is_success(), &"error": committed.message}
|
||||
|
||||
func _event_cost_ledger_key(token: Dictionary, operation: StringName) -> StringName:
|
||||
var reservation_id := StringName(token.get(&"reservation_id", ""))
|
||||
if not reservation_id.is_empty():
|
||||
return StringName("event.reserved_cost.%s.%s" % [operation, reservation_id])
|
||||
# Existing pending save tokens keep the original ledger operation identity.
|
||||
return StringName("event.cost.%s.%s" % [operation, token.get(&"event_instance_id", "")])
|
||||
|
||||
func _on_relationship_threshold_candidate(candidate: Dictionary) -> void:
|
||||
var candidate_id := StringName(candidate.get(&"candidate_id", ""))
|
||||
if candidate_id.is_empty() or event_service.get_instance(candidate_id) != null:
|
||||
@@ -10096,7 +10323,7 @@ func _validate_infrastructure_crosslinks(
|
||||
or (require_minute_alignment \
|
||||
and excavation.current_minute != construction.current_minute) \
|
||||
or not _same_vector3i_set(
|
||||
adapter.get_base_walkable_cells(), world.get_walkable_cells()
|
||||
adapter.get_base_walkable_cells(false), world.get_walkable_cells(false)
|
||||
) \
|
||||
or not _same_vector3i_set(
|
||||
adapter.get_projected_route_cells(), spatial.get_route_cells()
|
||||
@@ -10181,7 +10408,7 @@ func _validate_runtime_crosslinks(
|
||||
var assigned_staff := staff.get_staff(StringName(assignment[&"staff_id"]))
|
||||
var definition := definition_registry.get_facility(assigned_facility.definition_id)
|
||||
if assigned_staff == null or definition == null or definition.staff_max <= 0 \
|
||||
or (not definition.required_roles.is_empty() and assigned_staff.role_id not in definition.required_roles and not _is_anri_manager(assigned_staff)):
|
||||
or (not _staff_role_matches_facility(assigned_staff.role_id, definition) and not _is_anri_manager(assigned_staff)):
|
||||
return false
|
||||
var queued_links: Dictionary = {}
|
||||
var service_links: Dictionary = {}
|
||||
@@ -11482,6 +11709,22 @@ func _complete_service_transaction(
|
||||
request: ServiceRequest,
|
||||
facility: FacilityInstance,
|
||||
absolute_minute: int
|
||||
) -> Variant:
|
||||
# A sale is only observable after its inventory, rewards and cleanliness
|
||||
# effects commit. Rollback removes provisional receipts before scope exit.
|
||||
economy_service.begin_notification_scope()
|
||||
var result: Variant = _apply_service_transaction(service, request, facility, absolute_minute)
|
||||
if result is bool and result:
|
||||
_state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT))
|
||||
economy_service.end_notification_scope()
|
||||
return result
|
||||
|
||||
|
||||
func _apply_service_transaction(
|
||||
service: ServiceInstance,
|
||||
request: ServiceRequest,
|
||||
facility: FacilityInstance,
|
||||
absolute_minute: int
|
||||
) -> Variant:
|
||||
for guest_id: StringName in service.guest_ids:
|
||||
var guest := guest_service.get_guest(guest_id)
|
||||
@@ -12022,6 +12265,18 @@ func _on_relationship_route_lock_changed(
|
||||
&"route_id": route_id,
|
||||
})
|
||||
|
||||
func _on_event_effect_transaction_started() -> void:
|
||||
economy_service.begin_notification_scope()
|
||||
|
||||
|
||||
func _on_event_effect_transaction_finished() -> void:
|
||||
# A nested action batch ends before the enclosing choice cost commits.
|
||||
# Keep the authoritative mirror current without releasing observer receipts.
|
||||
if economy_service.notification_scope_depth() == 1:
|
||||
_state.set_resource(&"gold", economy_service.get_balance(PLAYER_CASH_ACCOUNT))
|
||||
economy_service.end_notification_scope()
|
||||
|
||||
|
||||
func _on_event_enqueued(instance: EventInstance) -> void:
|
||||
_publish_actor_event(
|
||||
&"EVENT_ENQUEUED", &"event_service", instance.actor_ids,
|
||||
@@ -12043,7 +12298,8 @@ func _on_event_finished(instance: EventInstance) -> void:
|
||||
|
||||
func _on_event_cancelled(instance: EventInstance, reason: StringName) -> void:
|
||||
_consume_relationship_candidate_from_event(instance)
|
||||
if event_service.get_foreground_instance() == null and not event_service.pause_requested:
|
||||
# Placement rejection occurs before an Event starts and never owns a pause.
|
||||
if reason != &"PLACEMENT_PROTECTION" and event_service.get_foreground_instance() == null and not event_service.pause_requested:
|
||||
_clock.set_paused(false)
|
||||
_publish_actor_event(&"EVENT_CANCELLED", &"event_service", instance.actor_ids, {
|
||||
&"reason": reason,
|
||||
@@ -12241,10 +12497,13 @@ func get_business_management_report() -> Dictionary:
|
||||
elif entry.category in [&"OPERATING_EXPENSE", &"STAFF_SIGNING_COST", &"EVENT_ACTION", &"EVENT_COST_COMMITTED", &"DEFENSE_SKILL_COST", &"AUTHORITY_FINE", &"REFUND"]:
|
||||
totals[entry.source_entity_id][&"expenses"] += entry.amount
|
||||
var rows: Array[Dictionary] = []
|
||||
var kitchens: Array[Dictionary] = []
|
||||
for facility: FacilityInstance in facility_service.get_instances():
|
||||
var definition := definition_registry.get_facility(facility.definition_id)
|
||||
if definition == null:
|
||||
continue
|
||||
if facility.definition_id == &"kitchen" and facility.origin.z == WorldService.LAYER_BACK:
|
||||
kitchens.append({&"facility_id": facility.instance_id, &"display_name": definition.display_name, &"origin": Vector2i(facility.origin.x, facility.origin.y), &"layer": facility.origin.z, &"operation_state": StringName(FacilityInstance.OperationState.keys()[facility.operation_state])})
|
||||
var amounts: Dictionary = totals.get(facility.instance_id, {})
|
||||
var base := _business_base_price(facility)
|
||||
var service_pricing := get_facility_service_pricing_readmodel(facility)
|
||||
@@ -12260,6 +12519,7 @@ func get_business_management_report() -> Dictionary:
|
||||
&"origin": Vector2i(facility.origin.x, facility.origin.y),
|
||||
&"layer": facility.origin.z,
|
||||
&"state": StringName(FacilityInstance.State.keys()[facility.state]),
|
||||
&"kitchen_dependency": facility_service.get_required_kitchen_report(facility.instance_id),
|
||||
&"business_open": facility.operator_open,
|
||||
&"price_mode": StringName(facility.policy_current.get(&"price_mode", &"standard")),
|
||||
&"base_price": base,
|
||||
@@ -12279,7 +12539,7 @@ func get_business_management_report() -> Dictionary:
|
||||
&"today_revenue": int(amounts.get(&"revenue", 0)),
|
||||
&"today_expenses": int(amounts.get(&"expenses", 0)),
|
||||
})
|
||||
return {&"minute": minute, &"modes": FacilityPricing.modes(), &"facilities": rows}
|
||||
return {&"minute": minute, &"modes": FacilityPricing.modes(), &"facilities": rows, &"kitchens": kitchens}
|
||||
|
||||
|
||||
func _guest_has_checked_in(guest: GuestRuntime) -> bool:
|
||||
@@ -12538,9 +12798,16 @@ func is_staff_in_transit(staff_id: StringName) -> bool:
|
||||
|
||||
func _estimated_actor_path_minutes(path: Array, access: Array[StringName]) -> int:
|
||||
for index: int in range(1, path.size()):
|
||||
if elevators.owns_connector(path_service.get_step_connector(path[index - 1], path[index], access)) \
|
||||
or _step_movement_credit(path_service, path[index - 1], path[index], access) > MOVEMENT_CREDIT_SCALE:
|
||||
return ceili(float(path_service.calculate_path_cost(path, access)) / PathService.ORTHOGONAL_MOVE_COST)
|
||||
var connector_id := path_service.get_step_connector(path[index - 1], path[index], access)
|
||||
if elevators.owns_connector(connector_id) \
|
||||
or _step_movement_credit_for_connector(path_service, path[index - 1], path[index], connector_id) > MOVEMENT_CREDIT_SCALE:
|
||||
var cost: int
|
||||
if access.is_empty() and path_service.get_script().resource_path == "res://scripts/world/path_service.gd" \
|
||||
and path[0] is Vector3i and path[path.size() - 1] is Vector3i:
|
||||
cost = path_service.calculate_guest_batch_path_cost(path[0], path[path.size() - 1], path)
|
||||
else:
|
||||
cost = path_service.calculate_path_cost(path, access)
|
||||
return ceili(float(cost) / PathService.ORTHOGONAL_MOVE_COST)
|
||||
return maxi(path.size() - 1, 0)
|
||||
|
||||
|
||||
@@ -12571,7 +12838,10 @@ func _prune_expired_staff_elevator_waits(minute: int) -> void:
|
||||
## one-step walking behavior, and elevator time is owned by the cabin service.
|
||||
static func _step_movement_credit(path: PathService, from: Vector3i, to: Vector3i,
|
||||
access: Array[StringName]) -> int:
|
||||
var connector_id := path.get_step_connector(from, to, access)
|
||||
return _step_movement_credit_for_connector(path,from,to,path.get_step_connector(from,to,access))
|
||||
|
||||
|
||||
static func _step_movement_credit_for_connector(path: PathService, from: Vector3i, to: Vector3i, connector_id: StringName) -> int:
|
||||
if String(connector_id).begins_with("infra.connector:") and from.x == to.x \
|
||||
and from.z == to.z and absi(from.y - to.y) == 1:
|
||||
var connector := path.get_connector(connector_id)
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# 資産・Windows証拠の再監査 2026-10-02
|
||||
|
||||
判定: **未完了**。P01-02 / P02-03 / P22の静的証拠再監査。通常制作コピーを読み、旧成果は保持した。新規ゲーム/Godot/Windows EXEの起動、配布、素材導入は行っていない。
|
||||
|
||||
基準HEAD: `285528b7a3b1b46a2a67af5aa90d9085539b42f4`。最初の読取では採用1396マップと実ファイル差分0。その後の並行作業中に記録した観測時刻 `2026-10-02T01:03:35.152951+09:00` では下記差分を認めた。資産SHAは変化していない。採用状況はrootの証拠で判定する。
|
||||
|
||||
- 基準マップとの差分: `autoload/simulation_runtime.gd`
|
||||
- 基準マップとの差分: `scripts/core/save_service.gd`
|
||||
|
||||
この監査は基準資産の再利用性を記録する。作業中の変更を採用済みと断定しない。
|
||||
|
||||
## 集計
|
||||
|
||||
| 確認項目 | 再監査値 | 受入で欠けているもの |
|
||||
|---|---|---|
|
||||
| 媒体 | 290(Kenney 272、参考mockups 11、旧生成テーマ6、icon 1) | 利用枠ごとの完成承認 |
|
||||
| リテラル参照 | 8、欠損0 | 動的参照と自然な到達性 |
|
||||
| 原本施設 | 30施設、360利用枠 | 全Lv1〜3・表裏・外観/内装の対応と画面 |
|
||||
| コード候補 | 16施設・192枠に同一コード候補 | 専用差分の完成認定 |
|
||||
| ヒロイン | 7名・旧84枠群 | 固有Portrait/SD/表情/衣装/汚れ/破損/刻印 |
|
||||
| その他 | 旧17枠群と分割不足9群 | CG、ライバル、効果、UI、音などの必要量 |
|
||||
| 音 | ファイル0、AudioStream等の接続記号0 | BGMペア、crossfade、環境音、操作音、声、原本SE |
|
||||
| 完成承認記録 | 0 | 作品、権利、受入画像、担当 |
|
||||
|
||||
利用枠は画像枚数でも最終原子要件の分母でもない。360 + 84 + 17は旧台帳の利用単位を保全した表示。追加不足群は重複分割を含むため機械加算しない。総合実装率/受入率は未算出。
|
||||
|
||||
## 既存成果の再利用
|
||||
|
||||
既存 `tools/build_original_asset_inventory.py` をOUTだけメモリ上で変更し、`asset_inventory_reaudit/`へ再実行した。旧台帳290媒体のSHA差分0。旧1394配布マップと現1396基準でもassets関連600ファイル・pixel描画関連6ファイルのSHA差分0。資産側の静的調査は再利用できるが、現版の通常操作・完成素材・Windows受入へ旧試験を転用しない。
|
||||
|
||||
`PixelTowerArt.room` は施設ID/層/rectを受けるコード描画でlevelを受けない。`inactive_layer_rooms.gd` は同じ内装を低透明度で再利用する。`PixelPeople.pose` は汎用mode/frame/variant/staffを受け、`world_preview.gd`で人物keyのhashから色を選ぶ。7人物固有外観/職種別歩行の完成証拠にはならない。`LayerInterfaceSkin`の表裏配色変更は部分成果として保持する。
|
||||
|
||||
旧1394版macOS PCKの `actor-restored.png` を目視した。操作パネルと汎用コード人物の表示は確認できるが、全施設・7人物の専用差分や現Windows受入画像ではない。
|
||||
|
||||
## 原本30施設
|
||||
|
||||
| 原本施設 | definition ID | 資産区分 | 欠損 |
|
||||
|---|---|---|
|
||||
| 一般客室 | standard_room | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| 上級客室 | premium_room | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| VIPスイート | 未定義 | 専用資産対応なし | 12利用枠の完成/差分/画面受入 |
|
||||
| ドミトリー | 未定義 | 専用資産対応なし | 12利用枠の完成/差分/画面受入 |
|
||||
| 大衆食堂 | cafeteria | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| レストラン | 未定義 | 専用資産対応なし | 12利用枠の完成/差分/画面受入 |
|
||||
| 酒場 | tavern | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| 屋台 | 未定義 | 専用資産対応なし | 12利用枠の完成/差分/画面受入 |
|
||||
| 大浴場 | public_bath | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| カジノ | 未定義 | 専用資産対応なし | 12利用枠の完成/差分/画面受入 |
|
||||
| 闘技場 | 未定義 | 専用資産対応なし | 12利用枠の完成/差分/画面受入 |
|
||||
| 礼拝堂 | chapel | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| 風俗個室 | adult_private_room | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| 高級ソープ | 未定義 | 専用資産対応なし | 12利用枠の完成/差分/画面受入 |
|
||||
| SM拷問室 | 未定義 | 専用資産対応なし | 12利用枠の完成/差分/画面受入 |
|
||||
| 闇市場 | black_market | 専用資産対応なし | 12利用枠の完成/差分/画面受入 |
|
||||
| 厨房 | kitchen | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| 倉庫 | warehouse | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| 待機室 | staff_waiting_room | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| 従業員寮 | staff_dormitory | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| 警備員詰所 | security_station | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| 清掃員室 | cleaning_room | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| 医務室 | infirmary | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| 覗き部屋 | 未定義 | 専用資産対応なし | 12利用枠の完成/差分/画面受入 |
|
||||
| 地下牢 | 未定義 | 専用資産対応なし | 12利用枠の完成/差分/画面受入 |
|
||||
| マナ炉 | 未定義 | 専用資産対応なし | 12利用枠の完成/差分/画面受入 |
|
||||
| 蔓の昇降機 | guest_elevator | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| 業務用EV | service_elevator | コード描画候補 | 12利用枠の完成/差分/画面受入 |
|
||||
| ワープ | 未定義 | 専用資産対応なし | 12利用枠の完成/差分/画面受入 |
|
||||
| 魔法砲台 | 未定義 | 専用資産対応なし | 12利用枠の完成/差分/画面受入 |
|
||||
|
||||
## 人物と原本の不足枠
|
||||
|
||||
| ヒロイン | 専用Portrait | 専用SD | 差分数/担当 |
|
||||
|---|---|---|
|
||||
| アンリ | 対応なし | 汎用コード候補のみ | 未設定/未割当 |
|
||||
| フィーネ | 対応なし | 汎用コード候補のみ | 未設定/未割当 |
|
||||
| トマリ | 対応なし | 汎用コード候補のみ | 未設定/未割当 |
|
||||
| アメリア | 対応なし | 汎用コード候補のみ | 未設定/未割当 |
|
||||
| リズ | 対応なし | 汎用コード候補のみ | 未設定/未割当 |
|
||||
| ミミ | 対応なし | 汎用コード候補のみ | 未設定/未割当 |
|
||||
| サツキ | 対応なし | 汎用コード候補のみ | 未設定/未割当 |
|
||||
|
||||
ライ、リーゼンバルト、ソウ等は別集合。旧 `other_characters` 1群では人物/状態別必要量が未分離。アンリとライを2/7ヒロイン完成と数えない。
|
||||
|
||||
旧台帳では刻印、ルート別立ち絵演出、人物×ルートCG差分、モブスチル、職種別歩行、呼吸等、成人向けSD/SEが個別化されていない。JSONでは9不足群と7×3のCG利用群を記録した。21利用群は21枚CGの確定ではなく、必要差分数/脚本/担当は未設定。原本の必須内容を対象外の名目で分母から落とさない。
|
||||
|
||||
## 出典とライセンス
|
||||
|
||||
Tiny Town/Tiny Dungeonの同梱License.txtにCC0を確認し、公式URL/取得日記録/原アーカイブSHA/License SHAをJSONに保持。272画像は出典確認済みだが、完成利用枠の認定とは別。旧テーマ6画像はREADMEに組込画像生成ツールとpromptの記録がある。制作方法は再利用できるが、権利所有者/個別許諾/品質承認のレコードは未確認。参考mockups11件は完成配布資産へ計上しない。
|
||||
|
||||
2026-10-02に以下の公式ページで候補とCC0記載を確認した。素材は導入していない。
|
||||
|
||||
| 候補 | 対象 | 残る確認 |
|
||||
|---|---|---|
|
||||
| [Interface Sounds](https://kenney.nl/assets/interface-sounds) | UI操作音、100ファイル | 試聴/選定/音量/連打/ミュート/License同梱 |
|
||||
| [Input Prompts Pixel](https://kenney.nl/assets/input-prompts-pixel) | 16pxキー/マウス表示、800ファイル | 操作対応/視認性/License同梱 |
|
||||
| [UI Pack - Adventure](https://kenney.nl/assets/ui-pack-adventure) | 表UI枠/ボタン、130ファイル | 色調/文字/伸縮/完成承認 |
|
||||
| [UI Pack - Sci-Fi](https://kenney.nl/assets/ui-pack-sci-fi) | 裏UI枠/ボタン、130ファイル | 世界観/黒鉄monitor表現/完成承認 |
|
||||
| [Music Jingles](https://kenney.nl/assets/music-jingles) | 短い通知、85ファイル | 原本の同曲表裏BGMペアの合格証拠にはしない |
|
||||
|
||||
## Windowsの版一致
|
||||
|
||||
最新存在確認した開発ZIPは `build/windows/TheTree-Windows-accepted-unique-contract-ui-2026-10-01-r02.zip`。読取でCRC、同梱9ファイルSHA、BUILD_MANIFEST一致を再検査して合格。
|
||||
|
||||
| 対象 | SHA-256 |
|
||||
|---|---|
|
||||
| ZIP | `30d86f426c5d1f5c2bef6a5aef2dfa5821143c750f7f86eb482cd323b8012c24` |
|
||||
| EXE | `c52c04ae1283256102b446df50499420d29ab11758041b8abfc043b61dd270a1` |
|
||||
| PCK | `8b9ec8d80dc108d331636766b71c1286f49834075544497ebff32576415ffa8f` |
|
||||
| source map(1394) | `513ce23f329c0aaa4fc5d9b249a513dc7f524cf18fc2134eeb25abd3027e7a2e` |
|
||||
|
||||
現1396基準との差分はRuntime、FacilityPricing、BusinessManagementPanelの変更3件とpending-pricing試験/UIDの追加2件。旧版のmacOS PCK 23 + 4 freshprocess、4,323 + 166 checks等の履歴はあるがmanifestで `windows_native=false`、`performance_budget_pass=false`。現1396版Windows配布/同一EXE実機受入は欠損。`CURRENT_WINDOWS_PACKAGE.md`はさらに古いevent-defense-labels版を指すため、現在配布エイリアスも未確定。
|
||||
|
||||
`list_projects`はThe Treeのローカル制作コピーを返し、Windows側The Treeプロジェクトを返さなかった。公開されたhandoff tool定義にDESKTOP-D2UGDR4が候補として載るが、接続可能/OS/CPU/GPU/RAM/倍率/解像度/実機上配布SHAの証拠はない。全項目を未確認として記録。Windowsへの書込/配布/起動は行っていない。
|
||||
|
||||
## 次の低依存作業
|
||||
|
||||
1. 意味ID/施設/人物/Lv/層/状態、寸法/原点/frame順/時間、出典/担当/受入画像の契約を台帳へ追記。受入: 仮素材/コード候補/参考/完成を再計算でき、未設定を推測で埋めない。
|
||||
2. Kenney Interface Soundsの試聴と通常UI操作音差替契約。受入: 個別SHA/原License/音量/連打/ミュート/保存再開/Windows音を対応。
|
||||
3. 既存16施設をLv1-3/表裏/外内contact sheetへ並べ不足だけ制作。受入: コード描画を昇格する場合も各差分の画面と承認を保持。
|
||||
4. 7人物とライバル3名の固有外観/Portrait/SD/職種別動作briefと担当/予算案を分離。受入: 衣装/表情/刻印/ルート差分数を捏造しない。
|
||||
5. rootへ現採用ソースWindows生成/対象機記録/同一EXE検証を提案。受入: 同一Windows EXEの通常操作/音/保存再開/描画性能を実測。
|
||||
|
||||
この監査でP22/P02-03や全体を完了へ昇格しない。詳細は [assets_platform_audit.json](assets_platform_audit.json)、再生成台帳と整合性検査は `asset_inventory_reaudit/`。基準ソースの並行変更2件を記録したためinventory_validationの総合passedはfalse、媒体/ライセンス/旧ZIPの検査は合格。
|
||||
@@ -0,0 +1,33 @@
|
||||
# P01 原子要件台帳 v1
|
||||
|
||||
2026-10-02に指定原本の保存版を分割した。対象はローカル制作コピーであり、新たなDrive取得はしていない。
|
||||
|
||||
既知条件v1の機能・内容分母は **1,291原子**。原本に直接対応する889原子と、追加・承認のみを出典とする402原子を含む。工程4群は18個の期日付き条件へ分割し、機能分母から除外した。これは必要な条件の件数であり、工数の割合ではない。
|
||||
|
||||
原本258行・75群、30施設、施設追加F01〜F10、7人物と3ルート計21件、明示ライバル3人、承認A01〜A05を対応付けた。原本で有限に指定された施設素材は30施設×Lv1〜3×表裏×外観/内装=360枠として個別化した。施設や人物のキーは原本の監査IDであり、現在の本番リソースが存在することを意味しない。
|
||||
|
||||
**最終分母は確定していない。** 全エンド、図鑑対象、章、サブ人物、イベントCG/表情/衣装/音の差分量には未指定部分がある。集合条件を1件記録したことを、全対象の個別網羅の代用にしない。D08/D09の対象確定時には追加・削除・統合ID、出典、理由、判定者を記録して分母を改訂する。明示値のない条件を捏造して分割しない。
|
||||
|
||||
第10章30施設以外にも受付/精算所・金庫室・購買部・闇銀行(原本190〜193行)、客/裏通路・従業員ドア・階段・リネンシュート・隠し扉(46/49/50/86/176/177行)がある。通常機能条件は台帳に存在するが、F01〜F10やLv1〜3/表裏/外内素材の適用範囲は未確定である。open_expansion_registerに対象例と適用範囲の確定・分母改訂義務を記録した。これらを除外した30施設の展開を、最終全施設網羅と認定しない。一律12素材枠を追加することも避ける。
|
||||
|
||||
`atomic_requirements.json`が条件台帳、`source_atomic_coverage.json`が行・群・施設・人物・承認・追加の逆引き、`atomic_denominator_freeze.json`が分母/分子IDと集計契約である。原本の繰返しは同じ意味IDへ複数出典を付けて統合し、承認が数値を精緻化する場合も加点を重ねない。
|
||||
|
||||
8観点すべてを`unverified`とし、実装状態は`not_reaudited`に置いた。現在の全観点認定済み原子は0、受入認定済み原子は0である。この0は既存実装の不存在を示すものではない。通常UI入口は受入で使う予定入口であり、実入口の存在確認ではない。実装パス・証拠はP01-02の再監査結果を結合する。既存のテスト総数や過去群の部分状態を原子合格に転記しない。
|
||||
|
||||
適用範囲は全件`conservative_pending_review`である。適用外を付ける場合には、その観点へ理由・原本または明示判断の参照・受入判定者を記録する。全観点を無条件に適用外にして達成としない。判定者の役割を保持しているが、個人は未割当であり独立レビュー待ちである。
|
||||
|
||||
施設人数や料金の必要値は未設定のまま残件へ含める。一方、D03の休止復帰・複数関連先/AND/ORは、明示未設定の拡張なので`informational_explicitly_unset_optional_extension`で管理する。単一関連先なしは表現できる。これらの未設定を、新規仕様の補完や独立作業の停止理由にしない。A04/A05の値は`approved_initial_trial_value`として原本の値と区別する。
|
||||
|
||||
原本の内容や素材について中立的な条件名を用いている。台帳の記録だけで本来の内容・完成資産の受入を置き換えることはできない。未制作の内容は未制作として残す。
|
||||
|
||||
既存計画の進捗追記で出典SHAが変わらないよう、計画の原初本文は`execution/IMPLEMENTATION_PLAN.initial.md`へそのまま保存した。原子台帳の計画ピンはこの履歴コピーを参照する。
|
||||
|
||||
検証は次で再実行できる。
|
||||
|
||||
```sh
|
||||
python3 docs/plans/original-completion-2026-10-02/validate_atomic_requirements.py --self-test
|
||||
```
|
||||
|
||||
1,291原子と工程18原子の構造・出典・逆引き・有限列挙・分母再計算が合格し、13種の意図的な破損を拒否した。これは台帳整合性の検証であり、ゲーム受入の合格ではない。結果は`atomic_requirements_validation.json`に保存する。
|
||||
|
||||
`build_atomic_requirements.py`は今回の分割を再生成するための作成記録である。再監査・判定を結合した後に無条件に再生成すると判定を初期化するため、その後は現台帳を版管理して更新する。証拠オーバーレイは`requirements[].id`と観点名(`source_condition`, `definition`, `runtime`, `normal_ui`, `save_migration`, `boundary_verification`, `assets_platform`, `evidence`)で結合できる。
|
||||
@@ -0,0 +1,25 @@
|
||||
# 原子要件1291件の独立意味監査
|
||||
|
||||
最新台帳SHA `82f4b4c0fbf32bd296a83e6c565dd2b64d2449336c9ada7be11742761d4a5cfb` を、指定原本258行、承認A01〜A05、保持された施設追加仕様と対照した。ゲーム機能1291件と工程18件の分離、原本71機能群+4工程群、7人物×3ルート、chapter10の30施設×13値項目と360素材利用slotが保持されている。17件の承認値精緻化mergeに追加の重複計上問題は見つからなかった。チェックの根拠とSHAは `atomic_semantic_review.json` に記録した。
|
||||
|
||||
これは要件台帳の監査であり、実装・受入へ昇格した項目は0件。1291は既知条件の分母で、全制作対象の最終分母ではない。`final_denominator_established=false`、全1291件の証拠未確認を維持する扱いは適切である。
|
||||
|
||||
## 未確定の適用対象
|
||||
|
||||
原本chapter10の有限30施設以外に、受付/精算所・金庫室・購買部・闇銀行(190〜193行)、客用/裏通路・階段・職員ドア(46、49、50、86、176〜177行)、リネンシュート・隠し扉(49行)が名指しされる。通常機能の要件は台帳にあるが、追加F01〜F10とLv1〜3/表裏/外内素材(248行)の全適用範囲は未確定である。
|
||||
|
||||
この指摘は最新 `open_expansion_register` の `additional_named_facilities_and_infrastructure_applicability` に取り込まれた。30施設の展開から全施設追加項目と全素材の最終網羅を合格にしてはいけない。設備を全部同じ施設型と扱い、一律12素材slotや全項目を推測追加することもしない。分類と適用対象を固定し、対象が増える場合は原子ID/分母改訂履歴へ記録する。
|
||||
|
||||
## 図鑑・全エンド・実制作の判定
|
||||
|
||||
原本9行の全エンドと図鑑コンプは独立した集合要件として残り、D08未確定になっている。対象catalog、個別ID、取得条件がない間、汎用の完了flagや部分ルート一覧で合格にできない。承認した対象集合に対し、個々の通常進行到達・取得・保存終了再起動を確認する必要がある。
|
||||
|
||||
7人物と21ルート利用は有限で確認できるが、それを21枚のCGと読み替えない。メイン立ち絵や他ルート色調の集合要件は、全既知人物と原本235行の各ルート/陰影/表情の軸を全部満たすことが必要である。衣装・表情・CG・ボイスの総数は未指定で、捏造できない。D08/D09の個別制作台帳と全軸AND受入を必要とする運用注意を残した。ゲーム状態やパラメータだけの実装は実シーン・素材の制作を証明しない。
|
||||
|
||||
## 重複と未設定値
|
||||
|
||||
A03のアンリ補正、A04/A05の寸法・層・金額・人数等は同じIDへの精緻化で保持され、二重に重みを加えていない。LvUPの価格設定、刻印、職種歩行/ループの繰り返し原本文も統合されている。同じ文の観察対応が3件あるが、VIPスイート・大浴場・風俗個室という独立対象なので重複ではない。
|
||||
|
||||
追加F08は全30施設で最低職員数と上限を別の値項目にしている。F07の人数専用上限は汎用capacityから推定せず、待機者と利用中顧客を分ける契約を保持する。必要値の未設定は台帳表示として許されても、営業受入の代用にならない。単一関連先なしと未承認の休止復帰/複数関連条件は、必須営業値の未確定と分けて扱う。
|
||||
|
||||
現時点の判定は「既知条件台帳を、適用対象と内容catalogの未確定を残して意味監査済み」である。全原本実装、全施設最終網羅、図鑑/全エンド/全素材の最終受入は引き続き未成立である。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
# Godot import Unicode 2行の原因と表記修正
|
||||
|
||||
guard検証用fresh stageのreference/candidate両方で発生した `Unexpected NUL character` 2行は、`tests/performance/test_digest_performance.gd` の2つの `\u0000` リテラルから発生した。両stageの対象スクリプトは同一SHAで、テキストの物理NULと不正UTF-8を走査して0件だった。候補guard変更と独立したimport時診断である。元stageを変更していない。
|
||||
|
||||
Godot 4.7.2の独立した小規模fresh projectを3種類作り、それぞれ新importと値のprobeを実行した。実験6プロセスはすべてexit 0。
|
||||
|
||||
| 表記 | fresh import のNUL診断 | 2つの値のcodepoint/UTF-8/Variant bytes |
|
||||
|---|---:|---|
|
||||
| 元の `\u0000` リテラル2個 | 2 | 元値 |
|
||||
| 実行時 `String.chr(0)` 2個 | 0 | 元値と完全一致 |
|
||||
| 実行時 `String.chr(0xfffd)` 2個 | 0 | 元値と完全一致 |
|
||||
|
||||
元の実行値はNULを内包せず、U+FFFDとなる。日本語と絵文字の値はcodepoints `[21416,25151,65533,127795]`、UTF-8 `e58ea8e688bfefbfbdf09f8cb3`、Variant bytes `040000000d000000e58ea8e688bfefbfbdf09f8cb3000000`。単文字の値はcodepoints `[65533]`、UTF-8 `efbfbd`、Variant bytes `0400000003000000efbfbd00`。これらが3表記で一致した。
|
||||
|
||||
Godotのtokenizerはunicode escapeを文字列へ追加する。[4.7.2 tokenizer source](https://raw.githubusercontent.com/godotengine/godot/4.7.2-stable/modules/gdscript/gdscript_tokenizer.cpp) の1006〜1007行が対象。`String::append_utf32(0)` は診断を出してreplacement characterへ置換する。[4.7.2 string source](https://raw.githubusercontent.com/godotengine/godot/4.7.2-stable/core/string/ustring.cpp) の187〜194行が対象。`String::chr()` も同じ追加処理を使う。[4.7.2 string header](https://raw.githubusercontent.com/godotengine/godot/4.7.2-stable/core/string/ustring.h) の411〜414行を確認した。この一次ソースと実験ログが置換の因果根拠である。
|
||||
|
||||
root承認に従い、本体テストの2リテラルだけを `String.chr(0xfffd)` で構成する表記へ変更し、理由コメントを付けた。ゲームコードは変更していない。テスト内の実行時 `String.chr(0)` と `PackedByteArray([0,127,255])` の境界試験は維持している。**NUL境界試験を削除してエラー行を隠した変更ではない。** GodotがNULを受け付けず置換する事実を踏まえた表記修正である。
|
||||
|
||||
修正後スクリプトSHAは `363772fc43975f12c9e2056e2b01beb4cd8b2d7544db7528f80c6bd127710c1f`。本体で焦点実行した結果は `Digest performance: 83 checks, 0 failures`、exit 0。実行時NUL診断321行は保持した境界試験の63+64+65+129回と一致する。焦点runの前後SHAは同一だった。
|
||||
|
||||
証拠は `artifacts/original-completion-2026-10-02/unicode-nul-cause-repro-r01/verification.json` と各import/probe log、fixture copy、`focused-verification.json` と `focused-test.log` に保存した。Godot実行binary SHAは `c7cccbf8fb143e34e02fd6521e09be2c2b974f0d5db080b19071c9c570718ccf`。本体fresh importでの2行除去と全回帰の確認はrootが別に担当し、この焦点結果だけから全体受入を主張しない。
|
||||
@@ -0,0 +1,98 @@
|
||||
# P09 施設必須値の再監査(2026-10-02)
|
||||
|
||||
原本30施設について、定義あり17、通常登録16、定義のみ1(black_market)、未定義13を静的に確認した。現在の全`.tres`は37件で、そのうち原本第10章の対応外は20件。これは最終の全施設数でも、実装・受入完了率でもない。今回のエンジン実行は0件、本体変更は0件。
|
||||
|
||||
機械可読台帳は `facility_value_contracts.json`。各値は原本文、A01〜A05、別冊A作成時の値と由来、現`.tres`明示値またはクラス既定値を別欄に保存し、出典行とSHAを保持する。生成は `python3 docs/plans/original-completion-2026-10-02/build_facility_value_contracts.py`。検証は `validate_facility_value_contracts.py`。承認差異の静的一致からRuntime/UI/保存のPASSを作らない。
|
||||
|
||||
原本を最上位とし、A01〜A05と会話追加10項目を重ねる。別冊Aは旧実装の完全一覧であり、数値承認書ではない。冒頭3・10・14〜19・76・106行が既定値/旧状態/仮値/異なる処理系を区別している。参照先 `docs/FACILITY_UNIT_PARAMETERS.md` は現ローカルに存在しないため、その内容を推定補完しなかった。既存の保存版TRACEABILITY 978〜995行を追加指定の記録に用いた。
|
||||
|
||||
## 30施設の現値と根拠
|
||||
|
||||
表のmin/max、人数専用、capacity、料金は現実装の観測値である。礼拝堂・清掃員室以外の数値を承認済みに扱わない。未設定は人数専用-1、未定義は独立定義なしを表す。
|
||||
|
||||
| 原本施設 | 定義/通常登録 | 原本サイズ・層 | min/max現値 | 人数専用/汎用capacity現値 | 料金現値 | 根拠 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 一般客室 | normal_registered | 2×1 表 | 0/0 | 未設定/2 | 120 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| 上級客室 | normal_registered | 4×1 表 | 0/0 | 2/2 | 220 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| VIPスイート | no_definition | 6×2 表 | 未定義/未定義 | 未定義/未定義 | 未定義 | 原本の寸法等のみ確定 |
|
||||
| ドミトリー | no_definition | 4×1 表 | 未定義/未定義 | 未定義/未定義 | 未定義 | 原本の寸法等のみ確定 |
|
||||
| 大衆食堂 | normal_registered | 4×2 表 | 2/4 | 未設定/12 | 25 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| レストラン | no_definition | 6×2 表 | 未定義/未定義 | 未定義/未定義 | 未定義 | 原本の寸法等のみ確定 |
|
||||
| 酒場 | normal_registered | 4×2 表 | 2/4 | 未設定/14 | 40 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| 屋台 | no_definition | 2×1 表 | 未定義/未定義 | 未定義/未定義 | 未定義 | 原本の寸法等のみ確定 |
|
||||
| 大浴場 | normal_registered | 6×2 表 | 1/2 | 未設定/12 | 20 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| カジノ | no_definition | 6×3 表 | 未定義/未定義 | 未定義/未定義 | 未定義 | 原本の寸法等のみ確定 |
|
||||
| 闘技場 | no_definition | 8×4 表 | 未定義/未定義 | 未定義/未定義 | 未定義 | 原本の寸法等のみ確定 |
|
||||
| 礼拝堂 | normal_registered | 4×3 表 | 1/2 | 8/8 | 20 | A04初期値 |
|
||||
| 風俗個室 | normal_registered | 2×1 表 | 1/1 | 未設定/2 | 250 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| 高級ソープ | no_definition | 4×2 表 | 未定義/未定義 | 未定義/未定義 | 未定義 | 原本の寸法等のみ確定 |
|
||||
| SM拷問室 | no_definition | 4×2 表 | 未定義/未定義 | 未定義/未定義 | 未定義 | 原本の寸法等のみ確定 |
|
||||
| 闇市場 | definition_only | 4×2 表 | 1/2 | 未設定/8 | 50 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| 厨房 | normal_registered | 4×2 裏 | 1/3 | 未設定/20 | 0 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| 倉庫 | normal_registered | 4×2 裏 | 1/3 | 未設定/100 | 0 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| 待機室 | normal_registered | 4×2 裏 | 0/0 | 未設定/8 | 0 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| 従業員寮 | normal_registered | 2×1 裏 | 0/0 | 0/12 | 0 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| 警備員詰所 | normal_registered | 2×2 裏 | 1/3 | 未設定/3 | 0 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| 清掃員室 | normal_registered | 2×1 裏 | 1/2 | 0/0 | 0 | A05初期値 |
|
||||
| 医務室 | normal_registered | 4×2 裏 | 0/2 | 未設定/0 | 0 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| 覗き部屋 | no_definition | 2×1 裏 | 未定義/未定義 | 未定義/未定義 | 未定義 | 原本の寸法等のみ確定 |
|
||||
| 地下牢 | no_definition | 4×2 裏 | 未定義/未定義 | 未定義/未定義 | 未定義 | 原本の寸法等のみ確定 |
|
||||
| マナ炉 | no_definition | 6×3 裏 | 未定義/未定義 | 未定義/未定義 | 未定義 | 原本の寸法等のみ確定 |
|
||||
| 蔓の昇降機 | normal_registered | 2×2 表 | 0/0 | 未設定/4 | 0 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| 業務用EV | normal_registered | 3×2 裏 | 0/0 | 未設定/8 | 0 | 数値は旧/現実装・個別根拠未確認 |
|
||||
| ワープ | no_definition | 2×2 表 | 未定義/未定義 | 未定義/未定義 | 未定義 | 原本の寸法等のみ確定 |
|
||||
| 魔法砲台 | no_definition | 未指定 外壁 | 未定義/未定義 | 未定義/未定義 | 未定義 | 原本の寸法等のみ確定 |
|
||||
|
||||
原本で寸法のある現定義17施設は寸法と層が静的一致する。一般的な「表」は地上専用を意味しない。地下や高層の指定は個別に保存し、魔法砲台の幅・高さ・層は捏造しない。premium_room、staff_dormitory、cleaning_room、chapelの通常登録は今回変更ではなく現基準の既存成果である。
|
||||
|
||||
## 10項目と必須値の区別
|
||||
|
||||
1. 配置コスト: 原本は資金/資材と建設ルールを指定するが、施設別コストと工期の数値はA04/A05以外に個別承認を確認できない。現値は保存しても正式値へ格上げしない。
|
||||
2. 運用コスト: A01の保有按分、休業/休止中計上、00:00精算、不足繰越とA02の0.5%初期試験率は確定。既存ceil/最低1Gを出典付きで保持する。礼拝堂6G/日、清掃員室2G/日は個別上書き。率の承認は建築費自体の個別承認を意味しない。闇市場の現maintenance_gold=既定0は、現1600Gに率を当てた条件付き8Gと異なる。未登録定義を登録する前にこの差異を解消する契約が必要。施設別マナ量は原本では未指定。原本183行のマナ0で全停止を別に保持する。
|
||||
3. 利用料金: 礼拝堂20G寄付、清掃員室利用料なしを承認初期値として保持。他施設の単価や5段階60/80/100/150/250%は旧実装値。D01の不足基準、加算額/率、属性割当は未設定。元の四欲求0〜100や清掃予約75を不足基準に流用しない。A03の20%補正は承認済みだが、新しい一致加算との順序/丸めはP09-02接続前に固定する。
|
||||
4. サイズ: 29施設の原本寸法と30対象の層/地下/高層/外壁指定を追跡。魔法砲台寸法は未指定のまま。
|
||||
5. 役割: 職種とサービス機能の原文、required_rolesとstaff_minを分離。A04聖職者min1/max2、A05清掃員min1/max2は確定。原本の記録係配置などの役割と人数の数値は同一視しない。
|
||||
6. 稼働状態: INACTIVE/OPERATING/SUSPENDEDと破壊/摘発後の持続休止は会話追加の確定契約。復帰条件D03はユーザー指定の未設定拡張であり、必須値欠損に数えない。
|
||||
7. 顧客同時利用最大人数: 承認値は礼拝堂8、清掃員室0のみ。上級客室2・従業員寮0は現明示値だが根拠未確認。残る定義13件は-1継承、未定義13件には値なし。待機者は利用人数とは別。厨房capacity20/profile生産4、倉庫capacity100、従業員寮capacity12などをcustomer_capacityへ転記しない。
|
||||
8. 配置可能従業員最大数: staff_maxはstaff_minと別。2施設の個別承認、15施設の旧/現値、13施設の未定義を保持する。Lv別変更もD02の未確定に残す。
|
||||
9. 関連施設/稼働フラグ: 原本168・173行の食堂/レストラン/酒場と厨房セット、52・174行の覗き対象などを個別追跡。現汎用単一依存APIは循環拒否と実稼働評価を持つが、建設からのcreate_instanceにはrelated_facility_idを渡していない。原本の対応を自動設定済みと認定しない。D03の複数依存AND/ORは未設定維持が許され、必須欠損へ数えない。
|
||||
10. 属性: 許可型appetite/relaxation/sexual/gamblingは確定、施設への割当は全37定義で空文字。Runtimeのhunger/sleep/hygiene/medical/leisure/intimacy効果と提供属性は別の名前空間・用途。効果タグから勝手に属性を選ばない。不一致の利用は許可する。
|
||||
|
||||
## Lv1〜3と解放
|
||||
|
||||
全30施設にLv1/2/3を別レコードとして置き、価格・人数・staff_min/max・解放条件・Policy解放割当を未確定として追跡する。A04/A05の初期値をLv1候補へ参照するが、承認本文にLv指定はなく、Lv2/3で不変とも自動増加とも承認されていない。原本165行のLvMAXに数値はない。計画P09-03のLv1〜3と現既定max_level3を混同しない。
|
||||
|
||||
現FacilityDefinitionにレベル別料金/人数の表はない。厨房などのupgrade_costsは旧実装の費用列であり、レベル別営業値ではない。unlock_rank既定1は正式な個別解放承認ではなく、現通常建設コマンドにunlock_rank/research_requirementsの評価もない。別冊Aの世界樹ランク表や初回Day案内は派生計画/実装欄に保全し、原本施設別解放条件へ昇格しない。原本179行の価格、食材、混浴、ハード設定、イカサマ率のLv解放は機能要件として残り、どのLvでどの値にするかを捏造しない。
|
||||
|
||||
## 未定義13施設から抽出できる範囲
|
||||
|
||||
| 未定義施設 | 確定できる項目 | 原本行 |
|
||||
|---|---|---|
|
||||
| VIPスイート | 6×2; VIP宿泊・覗き対応・要高景観; FRONT高層 | 199 |
|
||||
| ドミトリー | 4×1; 地下の宿泊施設; FRONT地下 | 199 |
|
||||
| レストラン | 6×2; 飲食・厨房とセット運用; FRONT | 201 |
|
||||
| 屋台 | 2×1; 飲食施設; FRONT | 201 |
|
||||
| カジノ | 6×3; 娯楽欲解消・イカサマ率設定をLvUPで解放; FRONT地下 | 203 |
|
||||
| 闘技場 | 8×4; 娯楽施設; FRONT地下 | 203 |
|
||||
| 高級ソープ | 4×2; 地下アングラ施設; FRONT地下 | 205 |
|
||||
| SM拷問室 | 4×2; 地下アングラ施設; FRONT地下 | 205 |
|
||||
| 覗き部屋 | 2×1; VIP/浴場/風俗の裏限定・裏通路寸断・記録係配置で脅迫ネタ収集; BACK | 209 |
|
||||
| 地下牢 | 4×2; 地下特殊施設; BACK地下 | 209 |
|
||||
| マナ炉 | 6×3; 地下特殊施設・奴隷消費; BACK地下 | 209 |
|
||||
| ワープ | 2×2; 瞬間移動・マナコスト大; FRONT | 211 |
|
||||
| 魔法砲台 | 寸法未指定; 外壁の防衛設備; 外壁 | 211 |
|
||||
|
||||
これらはsource-only blueprintとして先行定義できる。費用・人数・職員上限・マナ量・属性・個別解放・Lv2/3の値が未確定のまま、FacilityDefinitionの既定100G/1資材/60分などを適用して通常建設へ公開しない。既存地下客室をドミトリー、地下VIPサロンをVIPスイート、魔力制御室をマナ炉の完成代替と扱わない。
|
||||
|
||||
## 次に進める低依存作業
|
||||
|
||||
- A04/A05の承認済み人数・最少/最大職員・料金の契約を通常UIと保存へつなぐ境界検証を先行。current値を変更する前にsource/value確定と表示を区別。
|
||||
- customer_capacity=-1とservice_attribute空文字をUIで未設定と表示し、現在明示2/0も承認由来の別表示にする。generic容量・人数・待機人数を分ける。
|
||||
- 13未定義施設のsource-only blueprint/設計カタログを追加。確定寸法/層/地下/高層/外壁/厨房/覗き制約だけを保持し、未確定の値はnullとする。通常建設へ登録しない。
|
||||
- 食堂・酒場の厨房セット運用を現単一関連APIと実稼働判定へ接続する契約/候補検証を用意。厨房停止・破壊・未配置・保存復元時の動作を検証。
|
||||
- Lv1–3の明示tableと未設定guardを作り、Lv2/3料金や人数をbase値の複写で確定しない。既存upgrade_costsとrank1は旧実装欄に保全。
|
||||
- 欲求加算はD01確定まで請求を接続せず、nonmatch利用可・未解決見積拒否・財布/収入一致の計算契約と比較例を準備する。
|
||||
|
||||
この監査は値の出典と欠損を整理したもので、P09や各施設の受入完了ではない。通常操作、グループ利用中変更、日跨ぎ、途中保存、修正後同一ソース回帰の証拠は引き続き必要である。第9章経済施設、通路/階段/ドア/リネンシュート/隠し扉へのF各施設/Lv素材適用範囲はatomicのopen expansion registerと同じ未確定状態を維持する。
|
||||
|
||||
静的データの追加観測として、医務室定義には同値のstaff_max/capacity二重代入と、FacilityDefinitionに宣言がないqueue_staff_minがある。台帳のdefinition_anomaliesへ保存し、今回Runtimeへの影響を推測したり本体を修正したりしない。validatorは49出典ファイルのSHA、30施設/780値/90Lvの構造、全出典行、D03除外、架空値拒否を確認する。PASSは台帳の整合性だけを意味する。
|
||||
@@ -0,0 +1,36 @@
|
||||
# P28-01 実 Runtime 代表負荷の再監査
|
||||
|
||||
指定の20,000マス・200客・50職員・150施設に行列・物流・イベントを付けた、現版の実 Runtime 全体の受入証拠は未確認である。現在の通常 Operations の人口上限は100で、200客の通常営業受入へそのまま進めない。人口上限200への生改変や保存基準値変更は採用していない。
|
||||
|
||||
この文書はソースの読み取りと driver 案である。新しい性能測定は実施しておらず、root の全157回帰実行中に CPU を競合させていない。読み取ったファイルのSHA、時刻、詳細は `performance_audit.json` に記録した。
|
||||
|
||||
## 既存 fixture の再利用範囲
|
||||
|
||||
| 既存成果 | 確認済みの範囲 | 代表負荷との差 |
|
||||
|---|---|---|
|
||||
| `perf_mvp_001_harness.gd` | 実サービスを別々に作り、20000/200/50/150、行列500、未完物流200、イベント50を独自ループで処理 | SimulationRuntime/GameClock/実保存パッケージ/UI/GPUを通さない。200×50×2のマップはRank7正規境界に合わない |
|
||||
| `profile_production_scene.gd` | 実Runtimeとmain画面、Tick phase、x1/x3、snapshot/digest、UI同期更新を測る | PixelTowerDemoの7サービス施設・4職員という小シーン。自動保存無効、描画/GPU対象外 |
|
||||
| `test_runtime_actor_cycle.gd` | 公開commandで建設・職員・客を登録し、実Tickで業務・物流・サービスとsnapshot replayを検証 | 小規模。200客の実保存・読込・持続負荷を証明しない |
|
||||
| `test_save_preflight.gd` | 大型Worldの保存サイズ測定 | `full_runtime_20000_cell_acceptance=false` を明示する |
|
||||
|
||||
モデルの測定値と小Runtime測定値を、指定の全Runtime負荷の合格値へ読み替えない。
|
||||
|
||||
## 100客上限と200客診断
|
||||
|
||||
`OperationsCoordinator.DEFAULT_CONFIG.population_cap` は100(`scripts/operations/operations_coordinator.gd:83`)。Runtimeの `enable_production_operations()` は正規既定値で生成し、上限overrideを受け取らない(`autoload/simulation_runtime.gd:1446`)。有効時の `spawn_guest` は上限を拒否する(5892)。保存復元は正規configとの厳密一致を要求する(3160、3828)。従って手動cap200変更は正規保存契約と一致しない。
|
||||
|
||||
Operationsを無効のまま初期化する診断ではこの上限guardが適用されない。公開 `spawn_guest` は客・財布・経路actor・進行生涯数を接続し、GameClockは実Runtimeの登録phaseを走らせる。`save_to_path`(3991)と `load_from_path`(4019)も利用できる。200客の保存+実Tickは静的には実施候補になるが、まだ200客を作成・保存・読込していない。**production Operations enabled mixed load未受入**を全結果に付ける。
|
||||
|
||||
Rank7の正規境界はx=-60〜60、y=-40〜56、2層で23,474座標slot。20,000はこの境界内で準備する。Rank6は13,774slotで足りない。GameStateだけRank7へ書き換えたり、モデルの200×50境界をコピーすると、Progression/World/経路投影の復元検証と一致しない。公開昇格commandの条件を満たすfixture、または同版で検証済みのRank7保存が必要で、その既存成果は未確認である。
|
||||
|
||||
## 最小 driver 案
|
||||
|
||||
1. root採用版のソース地図、Godot binary、機種、seed、driver、専用一時保存先をpinする。
|
||||
2. 実GameClock/GameState/SimulationRuntimeからOperations無効の診断を作り、正規昇格履歴と境界内20,000マスを準備する。世界・SpatialSubslot・RouteAdapter・施設インフラ投影の全相互参照を検証する。
|
||||
3. 公開commandで150施設を完成させ、50職員へシフト・業務を設定、200客へ実財布を作成する。行列、在庫、未完物流、イベント候補を実サービスへ接続し、開始時と終了時の実数を報告する。診断用の在庫・資源補給は通常プレイ証拠へ昇格させない。
|
||||
4. 測定前にRuntimeの実保存パッケージを作成し、別Runtimeで読み込み、完全snapshot/digest一致を要求する。失敗したfixtureでは時間だけ測って合格にしない。
|
||||
5. 同一checkpointへ戻してx1/x3の全Tick、phase、経路、snapshot、digest、UI同期更新、最大停止、実保存/読込を別々に測る。平均/P95/最大/標本数/区間/速度を保存し、対象が減ったら減少数も報告する。
|
||||
6. Operations有効は100客以下の正規診断として別に測る。200客有効の本番代表負荷は未受入として残す。回帰と同時測定せず、同条件を反復する。
|
||||
7. 描画とWindowsは別の実機受入とする。同一EXE/PCK/ZIP、CPU/GPU/RAM、解像度/拡大率、D10の描画目標が必要である。
|
||||
|
||||
現コード予算はx1平均4ms、x3 P95 12ms、経路P95 2ms、最大main-thread停止50ms、保存5秒、読込8秒(`scripts/core/performance_budget.gd:17`)。新測定はまだない。200客の本番上限、正規Rank7 fixture、通常営業に必要な未確定施設値、持続する行列/物流/イベント、Windows実機と描画目標が受入の残件である。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
# 作業計画の改訂とレビュー
|
||||
|
||||
作成日: 2026-10-02。対象: 同日の原本全実装計画。計画書、作業台帳、対応表、基準と承認台帳の整合性を確認する。ゲームの新規受入は行っていない。
|
||||
|
||||
## 前計画からの修正
|
||||
|
||||
1. 原本の旧75群監査と現在の証拠を分離し、総合割合の未算出を明示した。
|
||||
2. 予定扱いで部分実装がある8パッケージを着手中へ修正した。完了への昇格は行っていない。
|
||||
3. 原本30施設の16/1/13を40入力ファイルの現SHA一致で確認し、登録率と機能完成率を分離した。
|
||||
4. 最新155スイートと12,310チェックの合格、1396本体、旧1394Windows、未採用候補を区別した。
|
||||
5. P20に図鑑コンプを明記し、人物7名とライバルを別集計にした。
|
||||
6. 既承認の維持費、アンリ、礼拝堂、清掃を再確認不要とし、未確定は依存箇所だけに限定した。
|
||||
7. パッケージ全体の完了依存と、必要契約がそろった機能単位の先行着手を分けた。
|
||||
8. 正常/異常の意味を保った軽量化、現在版のWindows、全件受入後の通しデバッグを独立した条件にした。
|
||||
9. 過去180〜390人日を残工数として再利用せず、原子分割と内容量の確定後に再見積する。
|
||||
|
||||
## 確認と制約
|
||||
|
||||
原本範囲と実装証拠について別担当が読み取り監査を行い、施設入力40件と採用ソース1396件の一致を確認した。計画作成では新規Godot実行、最新Windows実機試験、Drive再取得を行っていない。既存原子要件の現在全受入が未確定であるため、全体割合を推定値で補っていない。
|
||||
|
||||
原子台帳、素材必要量、未確定の必須値、対象Windows描画目標、必要な内容制作が今後の不確実性として残る。これらは計画から除外せず、担当パッケージと局所的な固定地点を設定した。
|
||||
|
||||
## 独立レビューで修正した点
|
||||
|
||||
完了条件を示す旧JSONキーを `cross_cutting_done` から `cross_cutting_acceptance` へ改名し、未着手項目の完了証拠と誤読されないよう適用範囲を明記した。旧Windows配布物の確認はZIP・PCK検査とmacOS実行であり、Windows実機ではないと現在地に追記した。過去の性能値は9月30日に計測した候補の値とし、現HEADの測定と区別した。
|
||||
|
||||
シフト・気絶・通勤の原本B-3.3は、基準試験のP02に加えて実業務を担当するP12とP16にも対応付け、本文・台帳・原本対応表の双方向整合を確認する。
|
||||
|
||||
進捗更新の検証は、初期状態の固定検査と通常の更新可能な検査を分離した。着手・保留・証拠付き完了を認め、旧初期状態は履歴として残す。旧ソースの全件SHA比較は明示オプションにし、将来の進捗を初期スナップショットへ固定しない。承認根拠4件の参照を旧計画の実ファイルへ解決できる相対パスに補正した。
|
||||
|
||||
最終の独立読み取りレビューで指摘残件の解消を確認した。検証器について、着手への更新を許可し、作業単位およびパッケージの証拠なし完了を拒否する3ケースをメモリ上の入力で確認した。記録は [検証器の更新確認](validator_update_checks.json)。ゲーム実行や本体変更はない。
|
||||
@@ -0,0 +1,57 @@
|
||||
# Runtime・UI・保存移行 再監査 2026-10-02
|
||||
|
||||
ローカル制作コピーの基準HEAD `285528b7a3b1b46a2a67af5aa90d9085539b42f4` を再監査し、P03の保存事前確認と破損拒否を修正した。P03全体・原本全実装は未完了。機械可読の根拠、SHA、行番号、未受入一覧は [runtime_save_audit.json](runtime_save_audit.json) に記録した。
|
||||
|
||||
## 証拠の版と適用範囲
|
||||
|
||||
開始時に採用後1396ファイルのSHAを照合して差分0を確認した。155スイート/12,310チェックは旧採用証拠の記録であり、本担当で全回帰を新たに実行した値ではない。今回は本体3ファイルと新保存試験を変更したため、統合全回帰と新しいWindows配布版受入を別途必要とする。
|
||||
|
||||
旧 `snapshot-opening-restart-2026-09-29` の283パスは、開始時49パス、今回の保存修正後51パスが不一致となる。現版の受入へ直接転用しない。rootの今回の描画付き開業再起動はwriter83/reader20チェック、両プロセスPASS・実行中ソース差分0だったが、その後の保存3ファイルとPathの計4ファイルが変わった。修正後の同一版へ再実行するまでは変更前の基準証拠として扱う。
|
||||
|
||||
## P03で実装した修正
|
||||
|
||||
`SaveService.inspect_snapshot()` は指定された正式セーブだけを検証し、中断保存の取引を復旧・昇格・削除しない。寸法移行プレビューをこのAPIに接続した。正式セーブが `.bak` に移された中断状態は `SAVE_NOT_FOUND` で拒否し、`.tmp`・`.bak`・取引マーカーを保全する。通常 `load_snapshot()` の中断復旧は維持する。
|
||||
|
||||
修正前に、プレビューで取引マーカーが削除されることを実行再現した。さらに形式版 `true`・`"1"`・`1.5` がすべて1として読込成功することを確認した。manifestの版とサイズは有限の整数値のJSON数値だけを許容し、チェックサムはSHA-256文字列を要求する。整数値のfloatはGodotのJSON数値互換のため許容し、文字列・bool・小数の強制変換は拒否する。
|
||||
|
||||
一般envelopeにも同じ型方針を適用した。schema、時刻、速度、pause、snapshot_versionが破損したときはclock/stateを復元する前に拒否する。旧版の省略されたpause/versionの既定値と旧x4→x3は保った。`SimulationKernel.restore_clock()` の直接呼出契約は変更していない。
|
||||
|
||||
技術的な資源上限として、worldは圧縮・展開それぞれ256 MiB、manifestは4 MiBとした。ファイル長とバイナリheaderの展開サイズを一括読取・解凍前に検査し、超過は `SAVE_SIZE_LIMIT_EXCEEDED` を返す。writerにも同じ上限を適用し、読み戻せない候補を正式保存へ昇格させない。これらはゲーム数値の追加仕様ではなく、破損saveの資源保護方針である。
|
||||
|
||||
現初期Runtimeの実サイズはraw 61,040 B/圧縮7,511 B。既存PERFの2万セルWorldのみの実サイズはraw 640,488 B/圧縮18,873 Bで、world上限には約419倍の余裕がある。全人物・全サービス・全施設を含む最大Runtimeの受入は未検証。正当な旧セーブでも上限を超える場合は拒否されるリスクが残る。
|
||||
|
||||
## 実行した受入と履歴
|
||||
|
||||
最終修正後の7スイートは485チェック、失敗0、実行中ソース差分0。
|
||||
|
||||
| 最終r02試験 | チェック | 区分 |
|
||||
|---|---:|---|
|
||||
| save preflight | 128 | 新規の読取・型・未知版・破損・サイズ・原状保持 |
|
||||
| save operations | 127 | 世代、復旧、旧形式移行、失敗時保持、symlink |
|
||||
| Runtime save lifecycle | 96 | checkpoint、復旧候補、domain検証とwhitelist修復 |
|
||||
| S0 kernel/save | 73 | clock、envelope、保存復元 |
|
||||
| transport save | 29 | 実役者・EV座席所有、旧attendance、crosslinks |
|
||||
| dimension migration UI | 12 | 実メニューのsignal handler、取消、stale-source、backup |
|
||||
| supply save migration | 20 | 旧供給状態と復元 |
|
||||
|
||||
証拠は `artifacts/original-completion-2026-10-02/p03-save-preflight/`。全保存候補と取引artifactのファイルSHAを比較し、拒否時・プレビュー時のバイトと配置を確認した。上限ちょうどの広告サイズは独立したmanifest不一致で解凍前に拒否し、上限超過はsize-limitで先に拒否する境界を試験した。
|
||||
|
||||
修正中の失敗履歴を保持した。初期85チェックではmalformed JSONの正しい拒否でもengine ERRORが出たため受入から除外し、JSONの返却エラー処理へ修正した。続く128チェック中1失敗は整数float速度がArrayの型一致で拒否される互換試験で、型検証後のint比較へ修正した。r02を合格根拠とする。寸法batch23・weighted migration63・寮task/rest25・selection autosave75は最終比較修正前の関連PASSとして保持し、最終版485に合算していない。
|
||||
|
||||
UI試験は実画面や公開コマンド・signal handlerを用いるが、加速clockと制御fixtureを含む。モデル試験、通常画面のhandler経路、OS pointer入力、Windows実機を別状態として扱う。
|
||||
|
||||
## 保存契約と更新順の現在地
|
||||
|
||||
保存はpackage format1、一般envelope schema1、Runtime schema13の三層である。Runtimeは既存1〜13の明示分岐を持ち、v5以上の現在形状ではcanonical key集合を厳格に検証する。会計・維持費・施設状態と人数・マナ・客職員・移動credit・EV・配送・清掃・イベント・防衛・建設・掘削などを保存し、別サービス候補と相互参照の検証後にliveを交換する。未知版・追加key・必須aggregate欠損を無断で補完しない。
|
||||
|
||||
順序はコマンド適用→clock前進と日付signal→天候/維持費/営業境界→マナ→建設→物流→施設完了→欲求環境→事件防衛関係イベント→queue→AI→snapshotとcheckpoint。最後にclockのsnapshot_versionと時刻通知が更新される。通常日次autosaveは06:00で保存済みdayキーにより重複を防ぐ。承認済み維持費精算は00:00で別契約。全同時境界の受入は未完了で、D05のEV停止・費用時点・日跨ぎの未確定を補っていない。
|
||||
|
||||
## 残件
|
||||
|
||||
- P03-01: 全原本機能の保存契約、未実装の欲求加算・全人物・章・エンド・図鑑を含む新状態の保存条件。
|
||||
- P03-02: 移動中役者、EV乗車、救護搬送、物流、複数同時建設、占有再配置とtask retargetの全移行受入。現行は安全な候補だけを受け、失う位置・経路・task destinationは拒否する。
|
||||
- D07: 現5関係値trust/respect/affection/fear/dependenceと原本3値の意味対応、既存routeの扱い。旧値を保全し、推測変換しない。
|
||||
- P03-03: 営業途中の現開業基準に加えて、配送・清掃・防衛・人物進行の途中を実プロセス終了/再起動して同じ操作列の財布・台帳・役者・イベントへ比較する受入。旧開業試験は測定時間を含むdomain event streamを明示除外し、他の状態を比較する限定証拠である。
|
||||
- P02/P28/P29: 修正後同一版での描画・pointer・Windows実機・最大規模・長期通し受入。
|
||||
|
||||
原子分母の固定は別担当のP01台帳へ委ねる。試験件数や保存キー数から総合実装率・受入率を計算しない。
|
||||
+1262
File diff suppressed because it is too large
Load Diff
+6482
File diff suppressed because it is too large
Load Diff
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"facility_mapping": "docs/plans/original-completion-2026-09-29/execution/original_facility_mapping.json",
|
||||
"facility_mapping_sha256": "df0c933220449c8bb3e0fd942ad6f1dee49075abdd6587c4856022e12e3665cf",
|
||||
"baseline_sha256": "75e4bdbdeb379c9269553f0c776bdb9cc96e01f2d6aa5ca650d132c63a8aeb1a",
|
||||
"media_files": 290,
|
||||
"literal_referenced_media_files": 8,
|
||||
"audio_files": 0,
|
||||
"facility_count": 30,
|
||||
"facility_usage_slots": 360,
|
||||
"named_heroines": 7,
|
||||
"character_usage_slot_groups": 84,
|
||||
"other_slot_groups": 17,
|
||||
"approved_assets": 0,
|
||||
"missing_literal_references": [],
|
||||
"scope": "Inventory and candidate mappings only. No asset is promoted to complete by file existence, procedural fallback, or a passing gameplay test. Variant counts and production ownership remain unresolved."
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"scope": "hash/inventory/archive consistency only, not game/quality/native acceptance",
|
||||
"checks": {
|
||||
"fresh_media_hashes": true,
|
||||
"unique_media_paths": true,
|
||||
"original_facilities_30": true,
|
||||
"facility_usage_slots_360": true,
|
||||
"heroines_7": true,
|
||||
"unique_facility_slots": true,
|
||||
"current_reference_map_matches": false,
|
||||
"old_assets_unchanged": true,
|
||||
"Kenney_licenses_present": true,
|
||||
"old1394_zip_crc_SHA": true,
|
||||
"no_false_finish": true
|
||||
},
|
||||
"passed": false
|
||||
}
|
||||
+4294
File diff suppressed because it is too large
Load Diff
+192
@@ -0,0 +1,192 @@
|
||||
[
|
||||
{
|
||||
"slot_id": "background.front",
|
||||
"requirement": [
|
||||
"B-13.3"
|
||||
],
|
||||
"candidate": "scenes/main/world_preview.gd",
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "background.back",
|
||||
"requirement": [
|
||||
"B-13.3"
|
||||
],
|
||||
"candidate": "scenes/main/world_preview.gd",
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "ui.front_skin",
|
||||
"requirement": [
|
||||
"B-13.4"
|
||||
],
|
||||
"candidate": "assets/themes/japanese_fantasy/visual-theme-v1.tres",
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "ui.back_skin",
|
||||
"requirement": [
|
||||
"B-13.4"
|
||||
],
|
||||
"candidate": "assets/themes/japanese_fantasy/visual-theme-v1.tres",
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "ui.emotion_graph",
|
||||
"requirement": [
|
||||
"B-13.4"
|
||||
],
|
||||
"candidate": null,
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "ui.camera_overlay",
|
||||
"requirement": [
|
||||
"B-13.4"
|
||||
],
|
||||
"candidate": null,
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "effect.shield",
|
||||
"requirement": [
|
||||
"B-13.2"
|
||||
],
|
||||
"candidate": null,
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "effect.smoke",
|
||||
"requirement": [
|
||||
"B-13.2"
|
||||
],
|
||||
"candidate": null,
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "effect.mana_light",
|
||||
"requirement": [
|
||||
"B-13.2"
|
||||
],
|
||||
"candidate": null,
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "audio.front_bgm",
|
||||
"requirement": [
|
||||
"B-13.5"
|
||||
],
|
||||
"candidate": null,
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "audio.back_arrangement",
|
||||
"requirement": [
|
||||
"B-13.5"
|
||||
],
|
||||
"candidate": null,
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "audio.front_ambience",
|
||||
"requirement": [
|
||||
"B-13.5"
|
||||
],
|
||||
"candidate": null,
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "audio.back_ambience",
|
||||
"requirement": [
|
||||
"B-13.5"
|
||||
],
|
||||
"candidate": null,
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "audio.ui",
|
||||
"requirement": [
|
||||
"B-13.5"
|
||||
],
|
||||
"candidate": null,
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "audio.character_voice",
|
||||
"requirement": [
|
||||
"B-13.5"
|
||||
],
|
||||
"candidate": null,
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "other_characters",
|
||||
"requirement": [
|
||||
"B-13.1"
|
||||
],
|
||||
"candidate": null,
|
||||
"status": "unverified_required_slot",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
},
|
||||
{
|
||||
"slot_id": "restricted_content_unproduced",
|
||||
"requirement": [
|
||||
"B-12.3",
|
||||
"B-13.1",
|
||||
"B-13.2",
|
||||
"B-13.5"
|
||||
],
|
||||
"candidate": null,
|
||||
"status": "not_produced_outside_current_content_scope",
|
||||
"creator": "unassigned",
|
||||
"required_variant_count": null,
|
||||
"approved_asset": null
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"validator": "P01 register structure and source coverage, not game acceptance",
|
||||
"passed": true,
|
||||
"checked_at_jst": "2026-10-02T01:24:08+09:00",
|
||||
"source_head": "285528b7a3b1b46a2a67af5aa90d9085539b42f4",
|
||||
"known_condition_v1_denominator": 1291,
|
||||
"historical_schedule_atoms": 18,
|
||||
"original_lines": 258,
|
||||
"original_groups": 75,
|
||||
"original_facilities": 30,
|
||||
"heroines": 7,
|
||||
"heroine_routes": 21,
|
||||
"implemented_whole_atoms": 0,
|
||||
"accepted_whole_atoms": 0,
|
||||
"all_evidence_unverified": true,
|
||||
"final_denominator_established": false,
|
||||
"errors": [],
|
||||
"negative_checks": [
|
||||
{
|
||||
"case": "duplicate_id",
|
||||
"rejected": true,
|
||||
"first_error": "duplicate requirement ID"
|
||||
},
|
||||
{
|
||||
"case": "source_line_deleted",
|
||||
"rejected": true,
|
||||
"first_error": "coverage has missing/duplicate source lines"
|
||||
},
|
||||
{
|
||||
"case": "source_hash_changed",
|
||||
"rejected": true,
|
||||
"first_error": "baseline pin mismatch"
|
||||
},
|
||||
{
|
||||
"case": "frozen_denominator_changed",
|
||||
"rejected": true,
|
||||
"first_error": "frozen denominator mismatch"
|
||||
},
|
||||
{
|
||||
"case": "unsupported_pass",
|
||||
"rejected": true,
|
||||
"first_error": "AR.art.anime_portrait: unsupported PASS runtime"
|
||||
},
|
||||
{
|
||||
"case": "unsupported_not_applicable",
|
||||
"rejected": true,
|
||||
"first_error": "AR.art.anime_portrait: unsupported N/A runtime"
|
||||
},
|
||||
{
|
||||
"case": "partial_implementation_counted",
|
||||
"rejected": true,
|
||||
"first_error": "AR.art.anime_portrait: incomplete implementation counted"
|
||||
},
|
||||
{
|
||||
"case": "unverified_acceptance_counted",
|
||||
"rejected": true,
|
||||
"first_error": "AR.art.anime_portrait: incomplete acceptance counted"
|
||||
},
|
||||
{
|
||||
"case": "all_content_final_claim",
|
||||
"rejected": true,
|
||||
"first_error": "final denominator claimed despite unresolved catalogs"
|
||||
},
|
||||
{
|
||||
"case": "facility_removed",
|
||||
"rejected": true,
|
||||
"first_error": "register denominator changed/duplicated"
|
||||
},
|
||||
{
|
||||
"case": "heroine_route_removed",
|
||||
"rejected": true,
|
||||
"first_error": "register denominator changed/duplicated"
|
||||
},
|
||||
{
|
||||
"case": "schedule_counted_as_function",
|
||||
"rejected": true,
|
||||
"first_error": "register denominator changed/duplicated"
|
||||
},
|
||||
{
|
||||
"case": "altered_percentage",
|
||||
"rejected": true,
|
||||
"first_error": "implementation percentage not recomputable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"reviewed_at_jst": "2026-10-02T01:29:36.666245+09:00",
|
||||
"reviewer": "/root/assets_platform_audit",
|
||||
"review_scope": "independent read-only semantic review: source conditions, duplicate identities, encyclopedia/endings, named facility scope, undefined content",
|
||||
"review_limits": "This is a register review, not implementation or game acceptance. Final denominator and final catalog coverage remain unresolved.",
|
||||
"reviewed_files": {
|
||||
"atomic_requirements.json": "82f4b4c0fbf32bd296a83e6c565dd2b64d2449336c9ada7be11742761d4a5cfb",
|
||||
"atomic_denominator_freeze.json": "467e960eb0fe47241edb217f5fa2044f2de304441daf9e51a7b0cb46ffa9194d",
|
||||
"source_atomic_coverage.json": "2ab7b9e6a2536ef248d4f7ccd695bf75a082b47018aca7a5a1b6b706fe1907bc",
|
||||
"atomic_requirements_validation.json": "8dc300b099cfd90fda862f138b6a528d01e9c488a2c67fe07a00a92a70f99175",
|
||||
"build_atomic_requirements.py": "6d6ea9e4216372dbd0f9fed3f885a4481b21a3f539a26f64cf98a6830af984c5"
|
||||
},
|
||||
"source_pins": {
|
||||
"docs/traceability/original/BASELINE.txt": {
|
||||
"expected": "75e4bdbdeb379c9269553f0c776bdb9cc96e01f2d6aa5ca650d132c63a8aeb1a",
|
||||
"observed": "75e4bdbdeb379c9269553f0c776bdb9cc96e01f2d6aa5ca650d132c63a8aeb1a",
|
||||
"matches": true
|
||||
},
|
||||
"docs/traceability/original/requirements.json": {
|
||||
"expected": "eb8ec13aefc3a814644280ebf4dcf8f9c8965c6e2ab24b33f69be65f58b98a57",
|
||||
"observed": "eb8ec13aefc3a814644280ebf4dcf8f9c8965c6e2ab24b33f69be65f58b98a57",
|
||||
"matches": true
|
||||
},
|
||||
"docs/plans/original-completion-2026-10-02/decisions.json": {
|
||||
"expected": "ad0673051b0fa601e525f05128e0b11e2dddd9d4bae6a3e0170bbf47da35efe9",
|
||||
"observed": "ad0673051b0fa601e525f05128e0b11e2dddd9d4bae6a3e0170bbf47da35efe9",
|
||||
"matches": true
|
||||
},
|
||||
"docs/traceability/original/audit-2026-09-29/TRACEABILITY.md": {
|
||||
"expected": "caed024fe5b35a4636975646a32bb3f0960e24eabfb5e9688d0d78f8592d5b5f",
|
||||
"observed": "caed024fe5b35a4636975646a32bb3f0960e24eabfb5e9688d0d78f8592d5b5f",
|
||||
"matches": true
|
||||
},
|
||||
"docs/plans/original-completion-2026-10-02/execution/IMPLEMENTATION_PLAN.initial.md": {
|
||||
"expected": "f9cb3f61e4cb020e37f792c615dfbd4794763884a8ec528de14c3469cf4e5248",
|
||||
"observed": "f9cb3f61e4cb020e37f792c615dfbd4794763884a8ec528de14c3469cf4e5248",
|
||||
"matches": true
|
||||
}
|
||||
},
|
||||
"checks": {
|
||||
"feature_atom_count_1291": true,
|
||||
"historical_schedule_atom_count_18": true,
|
||||
"denominator_ids_unique_and_match_feature_rows": true,
|
||||
"freeze_matches_register": true,
|
||||
"source_pins_current": true,
|
||||
"source_condition_coverage_all_258_lines": true,
|
||||
"source_feature_groups_71_schedule_groups_4": true,
|
||||
"chapter10_facilities_30": true,
|
||||
"chapter10_all_thirteen_value_fields_present": true,
|
||||
"chapter10_visual_slots_360_present": true,
|
||||
"heroines_7_and_route_usages_21_present": true,
|
||||
"17_approved_refinement_merges": true,
|
||||
"no_final_content_denominator_claim": true,
|
||||
"all_unaccepted_evidence_retained": true,
|
||||
"supplement_applicability_open_expansion_retained": true,
|
||||
"external_validator_reports_no_errors": true
|
||||
},
|
||||
"all_scoped_checks_passed": true,
|
||||
"known_condition_denominator": 1291,
|
||||
"historical_schedule_atoms_excluded_from_game_percent": 18,
|
||||
"final_denominator_approved": false,
|
||||
"game_acceptance_added": false,
|
||||
"requirement_files_modified_by_review": false,
|
||||
"count_semantics": {
|
||||
"thirty_facility_value_fields": 390,
|
||||
"thirty_facility_visual_usage_slots": 360,
|
||||
"known_heroine_route_usages": 21,
|
||||
"route_usage_is_exact_cg_file_count": false,
|
||||
"visual_usage_slot_is_unique_image_file": false
|
||||
},
|
||||
"exact_condition_text_duplicates_reviewed": [
|
||||
{
|
||||
"condition": "対象施設は原本指定の観察機能に対応する。",
|
||||
"ids": [
|
||||
"AR.facility.adult_private_room.observation",
|
||||
"AR.facility.public_bath.observation",
|
||||
"AR.facility.vip_suite.observation"
|
||||
],
|
||||
"review": "different facility identity; do not merge"
|
||||
}
|
||||
],
|
||||
"missing_checks": {
|
||||
"facility_fields": [],
|
||||
"visual_slots": [],
|
||||
"routes": []
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"id": "ASR-01",
|
||||
"priority": "required_before_final_coverage",
|
||||
"topic": "chapter10 finite thirty versus all named facilities and infrastructure",
|
||||
"source_lines": [
|
||||
46,
|
||||
49,
|
||||
50,
|
||||
86,
|
||||
176,
|
||||
177,
|
||||
190,
|
||||
191,
|
||||
192,
|
||||
193,
|
||||
248
|
||||
],
|
||||
"finding": "Chapter10 thirty-facility field and visual expansion is complete as a finite subset. The original additionally names reception/settlement, vault, purchasing, dark bank, corridors, stairs, staff doors, linen chute and hidden doors. Their normal features exist as atoms, but applicability of F01-F10 and Lv/front/back/exterior/interior visuals is unresolved.",
|
||||
"resolution": "Retain open applicability expansion; do not certify final all-facility field/art coverage, and do not automatically infer twelve slots or thirteen fields for every infrastructure item.",
|
||||
"current_state": "recorded in updated open_expansion_register; final denominator remains unresolved"
|
||||
},
|
||||
{
|
||||
"id": "ASR-02",
|
||||
"priority": "acceptance_operating_note",
|
||||
"topic": "set contracts and finite known usage identities versus unspecified artwork quantities",
|
||||
"source_lines": [
|
||||
9,
|
||||
30,
|
||||
125,
|
||||
235,
|
||||
242,
|
||||
243,
|
||||
245,
|
||||
252
|
||||
],
|
||||
"finding": "The seven identities and twenty-one route usages are explicit. Main portrait and other-route tone asset atoms remain set contracts: every required known identity and both stated tone axes must pass. Individual scene, expression, outfit, voice and CG counts are not defined. A main portrait set or route flag alone cannot certify actual content.",
|
||||
"resolution": "Maintain per-usage asset/content mapping under D08/D09; require all members and all source tone axes. Split finite known usage assertions during future denominator revision if independently adjudicated; do not invent asset counts or equate twenty-one route usages to twenty-one CG files.",
|
||||
"current_state": "open D08/D09 and content-not-produced rejection are retained; no denominator revision requested in this review"
|
||||
},
|
||||
{
|
||||
"id": "ASR-03",
|
||||
"priority": "required_before_full_clear_acceptance",
|
||||
"topic": "all endings and encyclopedia complete",
|
||||
"source_lines": [
|
||||
9
|
||||
],
|
||||
"finding": "AR.progression.all_endings and AR.progression.encyclopedia_complete preserve source set obligations with D08 pending. No endpoint/encyclopedia target catalog was supplied. They cannot be accepted solely from a generic flag, partial route list, or collection implementation.",
|
||||
"resolution": "Pin approved target catalogs, individual IDs, acquisition conditions, normal-route evidence and save/restart checks before accepting the set contract; retain missing targets as unresolved.",
|
||||
"current_state": "correctly unverified; final_denominator_established=false"
|
||||
},
|
||||
{
|
||||
"id": "ASR-04",
|
||||
"priority": "reviewed_no_duplicate_issue",
|
||||
"topic": "duplicates and refinements",
|
||||
"source_lines": [
|
||||
118,
|
||||
161,
|
||||
165,
|
||||
179,
|
||||
235,
|
||||
242
|
||||
],
|
||||
"finding": "Seventeen A03/A04/A05 refinements retain the prior identity rather than add a second weight. Repeated source references for price unlocking, markings and SD walk/loops are merged. Three identical observation condition texts apply to distinct named facilities and are not redundant.",
|
||||
"resolution": "Preserve entity identity when comparing conditions; generic contracts and configured per-facility values remain separate duties.",
|
||||
"current_state": "no additional mandatory duplicate correction identified in this scoped review"
|
||||
}
|
||||
],
|
||||
"completion_verdict": "known-condition register reviewed with unresolved applicability and content catalogs; no final all-original completion verdict"
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"plan_date_jst": "2026-10-02",
|
||||
"source_head": "285528b7a3b1b46a2a67af5aa90d9085539b42f4",
|
||||
"production_root": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部",
|
||||
"original_source_priority": "specified basic source text, explicit user amendments, annex reference, derived docs",
|
||||
"source_pins": {
|
||||
"docs/traceability/original/BASELINE.txt": "75e4bdbdeb379c9269553f0c776bdb9cc96e01f2d6aa5ca650d132c63a8aeb1a",
|
||||
"docs/traceability/original/baseline_metadata.json": "2c672eef85ee46e7f12e3b9cc07a0513031167b57869ac33c9f287255ee7d0b6",
|
||||
"docs/traceability/original/requirements.json": "eb8ec13aefc3a814644280ebf4dcf8f9c8965c6e2ab24b33f69be65f58b98a57",
|
||||
"docs/plans/original-completion-2026-09-29/work_packages.json": "4c2cd6aecd232ab4e1d1662f4cd43bf86fe29fd5b98a8786ff1d116418a19d0d",
|
||||
"docs/plans/original-completion-2026-09-29/requirement_coverage.json": "2b5b57432157f1674e5021f79295835f57a7138832a49ffeabc9effc72afbfa4",
|
||||
"artifacts/original-annex-A-drive-retrieval-2026-09-30-r01/retrieved-readable-annex-A.md": "93e4d644068386d63071332d9787f59184bc81bdc2efdc0b420da244f350bb8c",
|
||||
"artifacts/original-annex-A-schema-comparison-audit-2026-09-30/summary.json": "01217815ce8b18e9b5a39f4b761f912a14e01960ac99eefc9e86e6a08eb2ceb4",
|
||||
"artifacts/facility-need-pricing-production-scale-adoption-2026-10-01-r01/receipt.json": "63b59cd28f563278bb3bf55e2bd9ffc94afdc5542d570a90c79f118c31bb1cfd",
|
||||
"artifacts/facility-need-pricing-production-scale-adoption-2026-10-01-r01/after.json": "44e2752aa435479188f20beaa9929d518623670c98d1a415db6c63aef828ee33"
|
||||
},
|
||||
"source_inventory_files_verified": 1396,
|
||||
"source_inventory_changed_paths": [],
|
||||
"facility_audit_current_pins_verified": {
|
||||
"scripts/data/facility_definition.gd": "e90d56f7eab69c2a75238de465925c1c6df57ee6d44b3b81e7f6fcfd069af0a1",
|
||||
"scripts/data/facility_catalog.gd": "7bb0c5085ddb361d384314cb219620d1483840942bc3f0f46dcb9dfa29d56e29",
|
||||
"docs/plans/original-completion-2026-09-29/execution/original_facility_mapping.json": "df0c933220449c8bb3e0fd942ad6f1dee49075abdd6587c4856022e12e3665cf",
|
||||
"data/facilities/adult_private_room.tres": "d8501586416b5c57a8b3b3b2afd1c0143428d8a4fa1df7112e995bd36649df6d",
|
||||
"data/facilities/cafeteria.tres": "86d540fe2d78b9dcdbe5a0af6e4a001f4cec54934636ed01deb25a943c79128a",
|
||||
"data/facilities/expansion/black_market.tres": "c209d734c82487f6df4c03714618865d6a5f1d2cf991dc0056fe4bbad8f03a42",
|
||||
"data/facilities/expansion/chapel.tres": "9b30a7c51712e7bb99c17870d1f03e3429ea9279851d88284545bb2eb5042fcb",
|
||||
"data/facilities/expansion/cleaning_room.tres": "ae366cf67cbf117db7989c6b76092eb5a65229b93d35999a40d19b8260f95ad8",
|
||||
"data/facilities/expansion/clinic.tres": "339cf22c68d7ab3cb1514b9e8d2ea63dc0772fe93918d70767c11cf7b6ee0e16",
|
||||
"data/facilities/expansion/cold_storage.tres": "1aac824147b52b41e124d2ab2ce63c3bac58de2ec7fc447a5806a1cf9fa9ad99",
|
||||
"data/facilities/expansion/general_store.tres": "76872e0375648b43f05fdc5581ccabe4aa6a166674c3b523980dd1cf0b6e6dc1",
|
||||
"data/facilities/expansion/guild_counter.tres": "93e8736da4aab8a17b1c8e7831c9a7b8de7f32d0ec251aa9aa5c8cf859c8fd7b",
|
||||
"data/facilities/expansion/laundry.tres": "2518ea2e5da12cf4f2997502bf50f00dd8145caca153fb3486b44b99881fdab4",
|
||||
"data/facilities/expansion/maintenance_workshop.tres": "8bd10332a9f45a252febdc626f22f90507317fb4cff5b18b2526aacd1f1a176b",
|
||||
"data/facilities/expansion/mana_control_room.tres": "55e4c52a049ff61ddf3d081ccb990a8572b64bc7cf37d3a0d52ca5a08cc32f25",
|
||||
"data/facilities/expansion/premium_room.tres": "6665090e041f36df5809314cd171be228a94bf77d7e93f5b70be648db4705d98",
|
||||
"data/facilities/expansion/private_bath.tres": "241b807e7fa3a1b9dc4bda0d2495e637155cdaea89a4234e37a121f00098499c",
|
||||
"data/facilities/expansion/secret_tavern.tres": "f0a1372931078fa0f93dd5b9cec6df6b137975479aa706633fdf6a9be4cf1f62",
|
||||
"data/facilities/expansion/staff_dormitory.tres": "8d7c7cd6dfe98d137f7fef62e35eb31f82f1ca282216a40df54ac81728101271",
|
||||
"data/facilities/expansion/tea_lounge.tres": "77d1ace36c5e84f80c0e6d563ae973c7ff1cd12b56956afd4895cdcdd5771a67",
|
||||
"data/facilities/expansion/underground_guest_room.tres": "cdfd0f8cd21812a9972485926dff27daaa6718af16aac1651894728b087bef9e",
|
||||
"data/facilities/expansion/vip_private_salon.tres": "1c94a7a70393262fba82112c3f4a0cb67ad22728664f5ca408f450049d3657a7",
|
||||
"data/facilities/expansion/waste_processing.tres": "9954855c7a07e425376a220c589989beb6ec95da3c5331316ef3615a60b45f1d",
|
||||
"data/facilities/guest_corridor.tres": "d77ccf267e20a26ed70527d29e8d54e6fcab1df213f02d5a4e28ead4f705276b",
|
||||
"data/facilities/guest_elevator.tres": "ad4fb3c439ac4ed90fe52f07e29ed7b4ff654c54d1c400c7c8e226da77127af7",
|
||||
"data/facilities/infrastructure/guest_stairs.tres": "757c687b1ebe2a3f237fb28092cd5f01591f15b2bcd1b4e639042a24635a92c6",
|
||||
"data/facilities/infrastructure/service_stairs.tres": "637217d867bcc99669d27c54b6d2c391c82a4a17356ab9a80c897ccaee38622c",
|
||||
"data/facilities/kitchen.tres": "38197aa8a3e90c03a82e9ccdc88bb444af4eeb0349c41a39e0bdd7b188e7d5d3",
|
||||
"data/facilities/massage_room.tres": "75ecf845fb3cfa8d3150f6b0dc90e901953469af512611384b70cc4878d21e85",
|
||||
"data/facilities/public_bath.tres": "8ec259f48ed75c8ab2e5154a1caccf19f6be04331adbe774b877f0a2d8b2f778",
|
||||
"data/facilities/reception.tres": "bed3de120a0e98c539771d1bb189111d095712f8ae25dfdf7cb2983efc0dee9f",
|
||||
"data/facilities/rescue/infirmary.tres": "c3e861533971e369068ae9e42f71b660fa4ab3893c7c1f798fc367893befd943",
|
||||
"data/facilities/security_station.tres": "69f3f518df7714d00c8e954e9c7405aa44b683dd274181cc3da832cc956815b3",
|
||||
"data/facilities/service_corridor.tres": "5d79fbde201d657740efcba924f595bd7075e069b344d708e424dbebbdc2f254",
|
||||
"data/facilities/service_elevator.tres": "35d552d5b96065413feccb50847e3cd7173e673896ce65afba238f90abc25132",
|
||||
"data/facilities/staff_door.tres": "a993058bfd809ccdbbb9a61b9850259d7bf6e60003e57ccfa3972f6c29170752",
|
||||
"data/facilities/staff_waiting_room.tres": "5c7a2f7f7c508102c4ee89401ad6348191b57d1ff65538acb9d450e464a3a46f",
|
||||
"data/facilities/standard_room.tres": "89f0a3a861d66afa4c1338ff1ec919685cf338b93b8da9a2387ce85e7e5bc336",
|
||||
"data/facilities/tavern.tres": "eb494ae6ab0d1b9e88ae0a38944609702e43c81a370a29cfb030af9ae46f523c",
|
||||
"data/facilities/warehouse.tres": "d0fcf0528da66da7b354ab5efaafe2483a7a76e05b324db255700ef73cdfc791"
|
||||
},
|
||||
"original_requirement_groups": 75,
|
||||
"feature_or_content_groups": 71,
|
||||
"historical_schedule_groups": 4,
|
||||
"legacy_package_state_counts": {
|
||||
"completed": 1,
|
||||
"in_progress": 17,
|
||||
"planned": 12
|
||||
},
|
||||
"revised_package_state_counts": {
|
||||
"completed": 1,
|
||||
"in_progress": 25,
|
||||
"planned": 4
|
||||
},
|
||||
"full_regression": {
|
||||
"suites": 155,
|
||||
"checks": 12310,
|
||||
"test_failures": 0,
|
||||
"evidence": "artifacts/facility-need-pricing-production-scale-adoption-2026-10-01-r01/receipt.json",
|
||||
"new_engine_runs_in_plan_creation": 0
|
||||
},
|
||||
"facility_counts": {
|
||||
"normal_catalog_declared": 16,
|
||||
"missing_definition": 13,
|
||||
"definition_only": 1
|
||||
},
|
||||
"overall_atomic_implementation_percent": null,
|
||||
"overall_atomic_acceptance_percent": null,
|
||||
"atomic_denominator": null,
|
||||
"candidate_only_not_adopted": [
|
||||
"P05 paid auto-purchase corrected20 files and QA driver",
|
||||
"Path invalid transit snapshot partial-success guard"
|
||||
],
|
||||
"current_head_windows_native_verified": false,
|
||||
"current_head_windows_package_available": false,
|
||||
"long_play_acceptance_verified": false,
|
||||
"performance_acceptance_passed": false,
|
||||
"goal_scope_document": "/Users/korizatou114/.codex/attachments/d0e14500-1529-41c7-9c95-b818de1b4b06/pasted-text-1.txt",
|
||||
"source_recapture_note": "Uses retained readable snapshots; no fresh Drive fetch in this planning turn",
|
||||
"revised_state_note": "planned with recorded partial execution becomes in_progress; no package promoted to completed",
|
||||
"scope_note": "Plan and static evidence reconciliation; not a new game acceptance run"
|
||||
}
|
||||
@@ -0,0 +1,734 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the reviewed source-condition register, without awarding game acceptance.
|
||||
Only the new P01 deliverables are written. Stable semantic IDs support external evidence joins.
|
||||
"""
|
||||
import hashlib, json, subprocess
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
OUT = Path(__file__).resolve().parent
|
||||
BASE = 'docs/traceability/original/BASELINE.txt'
|
||||
GROUPS = 'docs/traceability/original/requirements.json'
|
||||
DECISIONS = 'docs/plans/original-completion-2026-10-02/decisions.json'
|
||||
SUPPLEMENT = 'docs/traceability/original/audit-2026-09-29/TRACEABILITY.md'
|
||||
PLAN = 'docs/plans/original-completion-2026-10-02/execution/IMPLEMENTATION_PLAN.initial.md'
|
||||
VIEWS = ['source_condition','definition','runtime','normal_ui','save_migration','boundary_verification','assets_platform','evidence']
|
||||
sha = lambda path: hashlib.sha256((ROOT/path).read_bytes()).hexdigest()
|
||||
pins = {p:sha(p) for p in [BASE,GROUPS,DECISIONS,SUPPLEMENT,PLAN]}
|
||||
lines = (ROOT/BASE).read_text().splitlines()
|
||||
groups = json.loads((ROOT/GROUPS).read_text())
|
||||
coverage_groups = json.loads((OUT/'requirement_coverage.json').read_text())
|
||||
group_by_id={g['id']:g for g in groups}
|
||||
records={}; merges=[]
|
||||
head=subprocess.check_output(['git','rev-parse','HEAD'],cwd=ROOT,text=True).strip()
|
||||
now=datetime.now(timezone(timedelta(hours=9))).isoformat(timespec='seconds')
|
||||
normal_entries={
|
||||
'P00':'タイトル/起動・配布情報','P01':'要件別受入と内容台帳','P03':'保存/読込/終了/再起動',
|
||||
'P04':'時間経過と日付境界','P05':'通常営業/資金/仕入れ/決算','P06':'マナ管理/通常営業',
|
||||
'P07':'天候/環境/流行と通常営業','P08':'建設パレット/配置/移動/撤去','P09':'施設詳細/営業管理/通常利用',
|
||||
'P10':'宿泊/飲食施設の建設と通常営業','P11':'衛生/娯楽/礼拝施設の建設と通常営業',
|
||||
'P12':'裏層施設の建設と業務','P13':'特殊施設の建設と運用','P14':'通常移動とEV/階段/ワープ',
|
||||
'P15':'客来店から行動観察','P16':'雇用/シフト/休息/救護','P17':'従業員画面/人物加入/配置/成長',
|
||||
'P18':'人物詳細/関係/決闘','P19':'人物イベントと分岐','P20':'新規ゲーム/章進行/エンド/図鑑',
|
||||
'P21':'防衛開始から結果','P22':'素材台帳/必要量/差替仕様','P23':'断面マップとユニット表示',
|
||||
'P24':'通常画面とインスペクター','P25':'完成ドット素材/施設差分/音を使う通常画面',
|
||||
'P26':'完成シナリオ/立ち絵/CG/ボイスを使う人物・イベント画面','P27':'同一候補版での要件別受入',
|
||||
'P28':'新規から本編/全要素と実測','P29':'同一Windows配布版の実機起動/操作/保存再開'}
|
||||
|
||||
def add(key, condition, line_nums=None, package='P27', group_ids=None, source=BASE,
|
||||
authority='designated_original_saved_copy', decision=None, supplement=None,
|
||||
pending=None, entity=None, scope='feature_or_content', replace=False, acceptance=None):
|
||||
rid='AR.'+key
|
||||
line_nums=list(line_nums or [])
|
||||
if group_ids is None:
|
||||
group_ids=[g['id'] for g in groups if source==BASE and set(line_nums)&set(g['source_lines'])]
|
||||
if rid in records:
|
||||
rec=records[rid]
|
||||
if rec['condition'] != condition:
|
||||
merges.append({'kept_id':rid,'alias_source':source,'alias_lines':line_nums,
|
||||
'alias_condition':condition,'reason':'same specification condition; source refinement is counted once'})
|
||||
if replace: rec['condition']=condition; rec['acceptance']['condition_assertion']=condition
|
||||
else:
|
||||
rec={
|
||||
'id':rid,'condition':condition,'scope':scope,'mandatory':True,'equal_count_weight':1,
|
||||
'source_anchors':[],'source_sha256':pins[BASE],'user_amendment_refs':[],
|
||||
'source_group_ids':[],'supplement_ids':[],'entity':entity,
|
||||
'source_head':head,'primary_package':package,'package':package,
|
||||
'work_packages':[package],'responsible_role':'実装担当・素材担当(機能別担当の割当は要再監査)',
|
||||
'reviewer':{'required_role':'実装者と独立した受入判定者','assigned':None,'assignment_state':'unassigned'},
|
||||
'normal_entry':normal_entries[package], 'implementation_paths':[],
|
||||
'save_contract':{'state':'not_reaudited','requirement':'関連状態を保存/終了/再起動で維持し、旧版拒否時は原状を保持する。適用外は判定者が出典付きで判断する。'},
|
||||
'applicable_completion_views':VIEWS.copy(),'applicability_state':'conservative_pending_review',
|
||||
'view_results':{v:{'state':'unverified','implementation_state':'not_reaudited','evidence_refs':[],'reviewer':None,'not_applicable_reason':None} for v in VIEWS},
|
||||
'acceptance':{'condition_assertion':acceptance or condition,
|
||||
'normal_operation':'指定通常入口から到達し条件の成立を現Runtimeと画面で照合する。',
|
||||
'persistence':'関連状態の保存/終了/再起動と旧版・欠損・破損拒否を確認する。',
|
||||
'boundaries':'条件の直前/一致/直後、停止/中断/重複、参照喪失の適用範囲を試験設計する。',
|
||||
'source_and_assets':'同一採用版のソースSHA、必要完成素材、配布対象を関連づける。',
|
||||
'evidence_policy':'旧群の状態と過去テスト件数から合格を転記しない。モデル試験、通常操作、Windows実機は区別する。'},
|
||||
'evidence_refs':[],'historical_evidence_refs':[],
|
||||
'implementation_state':'not_reaudited','acceptance_state':'unverified','status':'unverified',
|
||||
'pending_decision_ids':[],'value_contract':None,'decision_dependencies':[], 'difference_state':'not_reaudited','asset_state':'not_reaudited',
|
||||
'updated_at':now}
|
||||
records[rid]=rec
|
||||
anchor={'path':source,'lines':line_nums,'sha256':pins[source],'authority':authority}
|
||||
if decision: anchor['decision_id']=decision
|
||||
if supplement: anchor['supplement_id']=supplement
|
||||
if anchor not in rec['source_anchors']:rec['source_anchors'].append(anchor)
|
||||
rec['source_group_ids']=sorted(set(rec['source_group_ids']+group_ids))
|
||||
rec['work_packages']=sorted(set(rec['work_packages']+[package]))
|
||||
if decision:rec['user_amendment_refs']=sorted(set(rec['user_amendment_refs']+[decision]))
|
||||
if supplement:rec['supplement_ids']=sorted(set(rec['supplement_ids']+[supplement]))
|
||||
if pending:
|
||||
ids=([pending] if isinstance(pending,str) else pending)
|
||||
rec['pending_decision_ids']=sorted(set(rec['pending_decision_ids']+ids))
|
||||
for did in ids:
|
||||
dependency={'decision_id':did,'kind':'informational_explicitly_unset_optional_extension' if did=='D03' else 'required_for_dependent_acceptance','must_not_block_independent_work':True}
|
||||
if dependency not in rec['decision_dependencies']:rec['decision_dependencies'].append(dependency)
|
||||
return rec
|
||||
|
||||
# A line can contain multiple independently judgeable clauses; every clause below is curated.
|
||||
# Repeated semantic keys attach another source anchor instead of another denominator unit.
|
||||
SPEC = '''
|
||||
3|identity.code|プロジェクトコードはProject Yggdrasil。|P00
|
||||
4|identity.title|仮タイトルはThe Tree。|P00
|
||||
5|identity.fantasy_management|ファンタジー世界樹経営シミュレーションのゲームとして成立する。|P20
|
||||
5|identity.tower_building|タワービルディングのゲームとして成立する。|P20
|
||||
5,32|identity.age_rating|原本の成人向け区分を台帳と配布設定で管理する。|P29
|
||||
6|platform.windows|同一Windows配布版でゲームを起動し通常操作できる。|P29|D10
|
||||
8|progression.main_duration|本編クリアの実プレイ時間は20〜30時間。|P28
|
||||
9|progression.full_duration|全要素クリアの実プレイ時間は50時間以上。|P28
|
||||
9|progression.all_endings|全エンドへ通常進行で到達し全エンド取得を判定できる。|P20|D08
|
||||
9|progression.encyclopedia_complete|図鑑の全対象を取得しコンプリートを判定できる。|P20|D08
|
||||
10,37,216|world.cutaway|ゲームマップは2Dサイドビューの断面図で表示する。|P23
|
||||
12|world.tree_setting|地上と地下を同一世界樹として管理する。|P20
|
||||
12|world.dual_management|地上領域と地下領域を同時に管理・拡張できる。|P07
|
||||
13|world.autonomous_observation|客と従業員の自律行動を通常画面で観察できる。|P24
|
||||
13|world.intervention|管理者の介入が人物の進行へ反映される。|P19|D08
|
||||
14|progression.tree_growth|フロアの積上げに伴い世界樹が成長する。|P20|D04
|
||||
14|progression.height_authority|高さの成長が表の権威増加へつながる。|P20|D04
|
||||
14|progression.depth_wealth|地下の深さの成長が裏の富増加へつながる。|P20|D04
|
||||
16|construction.normal_build|通常操作で施設を建設できる。|P08
|
||||
16|construction.normal_links|通常操作で通路・昇降設備を配置できる。|P14
|
||||
16,24|ui.layer_switch|通常UIで表層と裏層を即時に切り替える。|P07
|
||||
17|guest.diverse_arrival|多様な客が通常営業で来店する。|P15
|
||||
17,25|guest.autonomous_ai|客は欲求に従い自律的に行動する。|P15
|
||||
25|staff.autonomous_ai|従業員は全キャラクター自律AIの対象として業務・移動・休息を実行する。|P16
|
||||
19,26|economy.front_faith|表の健全サービスで信仰を獲得する。|P20|D04
|
||||
19|service.healing|治療サービスを通常業務として提供する。|P12|D04
|
||||
19|economy.front_gold|表のサービスで表資金を獲得する。|P05
|
||||
20,26|guest.underground_addiction|地下のサービス利用で客の依存度が上がる。|P15|D04
|
||||
20|economy.back_gold|裏のサービスで裏資金を獲得する。|P05
|
||||
21|staff.back_routes|裏層の動線を確保して職員業務へ接続する。|P16
|
||||
21,34|underground.blackmail|記録した根拠を使う脅迫取引の状態と収支を管理する。|P19|D08
|
||||
21,34|underground.trade_people|ユニット売却の状態と収支を管理する。|P16|D04
|
||||
22|progression.rank_unlock|世界樹ランク上昇で高度な施設を解禁する。|P20|D04
|
||||
22|defense.repulse|防衛力により外敵を撃退できる。|P21|D04
|
||||
24,41|world.two_slots|全グリッド座標に独立した表/裏の二スロットがある。|P07
|
||||
24,42|world.guest_front_visible|客は表層のみを認識する。|P07
|
||||
24,43|world.guest_back_hidden|客は裏層のユニットと機能を認識しない。|P07
|
||||
25|traffic.elevator_congestion|昇降機配置が悪いと渋滞が発生する。|P14
|
||||
25,67,109|staff.shift_day|各施設に昼番06:00〜18:00のスロットを設定する。|P16
|
||||
25,67,109|staff.shift_night|各施設に夜番18:00〜06:00のスロットを設定する。|P16
|
||||
26|progression.faith_story|信仰をストーリー進行へ反映する。|P20|D04
|
||||
26|guest.addiction_all_wealth|依存の進行により原本指定の全財産支出へつながる。|P15|D04
|
||||
26,195|guest.addiction_budget_priority|依存客は生存費より娯楽費を優先する。|P15|D04
|
||||
28|narrative.tree_origin|舞台は旧魔王城跡地に生えた世界樹。|P20|D08
|
||||
29|narrative.protagonist_past|主人公には勇者として使い捨てられた背景がある。|P20|D08
|
||||
29|narrative.protagonist_goal|宿屋の主として再起し真の王への即位を目指す進行がある。|P20|D08
|
||||
33|narrative.route_tones|原本3ルートの内容と表現が指定方向性を満たす。内容が未制作なら受入しない。|P19|D08
|
||||
34|underground.observation|原本の観察機能を状態と通常入口で管理する。|P19|D08
|
||||
37|world.unit_grid|マップを1ブロック単位のグリッドで管理する。|P07
|
||||
38|world.above_ground|Y>0を地上(幹・枝)として扱う。|P07
|
||||
38|world.below_ground|Y<0を地下(根)として扱う。|P07
|
||||
38|world.ground_boundary|Y=0を地面の境界として扱う。|P07
|
||||
39,164|construction.support|地面または他の完成済み施設・幹へ隣接して建設する。|P08
|
||||
39|construction.expand_area|進行に伴い建設可能領域を拡張する。|P20|D04
|
||||
42|world.front_guests|客ユニットは表層で行動する。|P07
|
||||
42|world.front_service_staff|接客中の従業員は表層で業務する。|P16
|
||||
43|world.back_waiting_staff|待機中の従業員は裏層にいる。|P16
|
||||
43|world.back_moving_staff|移動中の従業員は裏層を使う。|P16
|
||||
43|world.back_security|警備員は裏層を使う。|P16
|
||||
43|world.back_goods|運搬物資は裏層を通る。|P12
|
||||
43|world.back_observer|観察中の管理者状態を裏層へ対応付ける。|P19
|
||||
44,216|ui.front_back_transparency|表層モードでは裏層を半透明に表示する。|P07
|
||||
44,216|ui.back_front_wireframe|裏層モードでは表層をワイヤーフレーム表示する。|P07
|
||||
47|environment.beauty|景観度を計算し客満足度へ反映する。|P22|D04
|
||||
47|environment.cleanliness|清潔度を計算し客満足度へ反映する。|P22|D04
|
||||
47|environment.noise|騒音度を計算し客満足度へ反映する。|P22|D04
|
||||
49|infrastructure.linen_chute|リネンシュートを裏層で設置・運用できる。|P12|D04
|
||||
49|infrastructure.hidden_door|隠し扉を裏層で設置・運用できる。|P13|D04
|
||||
50,177|infrastructure.staff_door_crossing|施設奥の職員ドアを通じ表層と裏層を移動する。|P14
|
||||
50|infrastructure.staff_door_no_guest|客は従業員ドアを通過できない。|P14
|
||||
52,174|facility.peeping_room.target_placement|観察部屋は指定された表層対象の裏に限定して建設する。|P13
|
||||
52,174|facility.peeping_room.corridor_loss|観察部屋の占有グリッドは裏通路として利用できない。|P13
|
||||
53|observation.alternate_view|観察機能には通常マップと異なる専用視点がある。|P19
|
||||
53|observation.recorder|記録係の配置により根拠データを収集する。|P19|D04
|
||||
55|environment.ground_light|地上に採光ボーナスがある。|P22|D04
|
||||
55|environment.ground_mana|地上にマナ濃度ボーナスがある。|P06|D04
|
||||
55|guest.ground_classes|地上は一般市民・貴族・聖職者向けとなる。|P15|D04
|
||||
56|environment.underground_sound|地下に遮音ボーナスがある。|P22|D04
|
||||
56|environment.underground_concealment|地下に隠蔽ボーナスがある。|P22|D04
|
||||
56|guest.underground_classes|地下は犯罪者・魔物・依存客向けとなる。|P15|D04
|
||||
59|time.scale|実時間10秒がゲーム内1時間に対応する。|P04
|
||||
60|time.speeds|Pause/x1/x2/x3の再生速度を選択できる。|P04
|
||||
60|time.pause_build|Pause中も建設できる。|P08
|
||||
60|time.pause_commands|Pause中も指示できる。|P24
|
||||
62|time.morning_phase|朝フェーズは06:00〜10:00。|P04
|
||||
62|traffic.morning_down_rush|朝に下りラッシュが生じる。|P14
|
||||
62|guest.morning_checkout|朝にチェックアウトが発生する。|P15
|
||||
62|guest.morning_breakfast|朝に朝食需要が発生する。|P15
|
||||
62|staff.morning_arrival|朝に昼番が出勤する。|P16
|
||||
62|staff.morning_cleaning|朝に清掃業務を実行する。|P16
|
||||
63|time.day_phase|昼フェーズは10:00〜16:00。|P04
|
||||
63|guest.day_arrival|昼に新規客が来店する。|P15
|
||||
63|guest.day_circulation|昼に客が施設を回遊する。|P15
|
||||
63|time.day_building|昼に営業が安定し建設に適するゲームサイクルが成立する。|P28|D04
|
||||
64|time.evening_phase|夕フェーズは16:00〜20:00。|P04
|
||||
64|traffic.evening_up_rush|夕に上りラッシュが生じる。|P14
|
||||
64|guest.evening_bath_demand|帰還した冒険者が夕に大浴場へ向かう。|P15
|
||||
64|guest.evening_food_demand|帰還した冒険者が夕に食堂へ向かう。|P15
|
||||
64|staff.evening_shift_handover|18:00に夜番へ交代する。|P16
|
||||
64,69|staff.shift_route_congestion|交代時刻の裏層動線が悪いと職員の渋滞により業務が滞る。|P16
|
||||
65|time.night_phase|夜フェーズは20:00〜06:00。|P04
|
||||
65|guest.night_sleep|健全客は夜に睡眠行動を取る。|P15
|
||||
65|guest.night_underground|依存客は夜に地下へ向かう。|P15
|
||||
65|guest.need_sexual_night_peak|夜に原本の地下サービス欲求解消がピークとなる。|P15|D04
|
||||
65|events.ntr_night_peak|夜に原本のNTRイベント発生がピークとなる。|P19|D04
|
||||
68|staff.work_stamina|勤務で体力が減少する。|P16|D04
|
||||
68,109|staff.rest_stamina|休憩または非番の待機室等で体力が回復する。|P16|D04
|
||||
68,101,110|staff.zero_collapse|体力0で気絶状態となる。|P16
|
||||
68,110|staff.back_rescue|気絶者を救護班が裏ルートから回収する。|P16
|
||||
68|staff.seen_collapse_reputation|客が気絶者を目撃すると評判が低下する。|P16|D04
|
||||
71|economy.midnight_report|毎日00:00に決算レポートを表示する。|P05
|
||||
72|economy.settlement_calculation|日次決算で表裏の売上から全対象経費を引く。|P05
|
||||
72|progression.three_deficits_gameover|赤字決算が3回連続するとゲームオーバーとなる。|P20
|
||||
74|weather.rain_arrivals|雨で客足が減る。|P22|D04
|
||||
74|weather.rain_stay|雨で滞在が延長する。|P22|D04
|
||||
74|weather.storm_no_arrivals|嵐で新規客がゼロになる。|P22
|
||||
74|weather.fog_magic|マナの霧で魔法効率が上がる。|P22|D04
|
||||
74|weather.fog_excitation|マナの霧で興奮度が上がる。|P22|D04
|
||||
75|news.cadence|数日おきに流行ニュースが発生する。|P22|D04
|
||||
75|news.class_boost|ニュースで特定客層の売上が増える。|P22|D04
|
||||
75|news.facility_boost|ニュースで特定施設の売上が増える。|P22|D04
|
||||
78|guest.budget|客は所持金Budgetを持つ。|P15
|
||||
78|guest.loot|客は戦利品Lootを持つ。|P15
|
||||
79|guest.need_sleep_range|睡眠欲を0〜100で保持する。|P15
|
||||
17,79|guest.need_sleep_relief|睡眠欲を客室により解消する。|P15
|
||||
79|guest.need_sexual_range|原本の地下サービス欲求を0〜100で保持する。|P15
|
||||
17,79|guest.need_sexual_relief|原本の地下サービス欲求を対象施設で解消する。|P15
|
||||
79|guest.need_food_range|食欲を0〜100で保持する。|P15
|
||||
17,79|guest.need_food_relief|食欲を食堂により解消する。|P15
|
||||
79|guest.need_entertainment_range|娯楽欲を0〜100で保持する。|P15
|
||||
17,79|guest.need_entertainment_relief|娯楽欲をカジノ等により解消する。|P15
|
||||
80|guest.patience_loss|渋滞等により忍耐力が減る。|P15|D04
|
||||
80|guest.zero_patience_exit|忍耐力0で激怒退店する。|P15
|
||||
80|guest.dirt_contact|汚れを歩くと清潔度が減る。|P22|D04
|
||||
80|guest.dirt_spread|客が汚れを伝播する。|P22|D04
|
||||
82|guest.highest_need_destination|現在最も高い欲求を基に目的地を決定する。|P15
|
||||
83|guest.choice_distance|施設選択の重みに距離を使う。|P15
|
||||
83|guest.choice_queue|施設選択の重みに行列の短さを使う。|P15
|
||||
83|guest.choice_affordability|施設選択の重みに価格・クラスと所持金の適合を使う。|P15
|
||||
85|traffic.astar|経路探索にA*を用いる。|P14
|
||||
85|traffic.elevator_wait_cost|経路コストへ昇降機の待ち時間を含む。|P14
|
||||
86|traffic.guest_elevator_capacity|客用昇降機に定員がある。|P14|D02
|
||||
86|traffic.service_elevator_capacity|裏層の業務用EVは大容量である。|P14|D02
|
||||
176|traffic.service_elevator_fast|業務用EVは高速に移動する。|P14|D04
|
||||
86|traffic.stairs_slower|階段は他の垂直移動より速度が遅い。|P14|D04
|
||||
86|traffic.warp_instant|マナワープで瞬間移動する。|P14
|
||||
86|traffic.warp_cost|マナワープには大きいコストがかかる。|P14|D04
|
||||
87|traffic.overlap_half_speed|通路上のユニット重なりで移動速度が50%低下する。|P14
|
||||
89|guest.addiction_level_three_exit_refusal|依存Lv3で退店を拒否する。|P15
|
||||
89|guest.addiction_equipment_sale|依存Lv3で装備を売却して滞在を継続する。|P15
|
||||
90|guest.male_bankruptcy_riot|所持金0かつ高依存の男性客で暴動が起きる。|P15|D04
|
||||
90|guest.female_bankruptcy_contract|所持金0かつ高依存の女性客で原本の債務ユニット回収イベントが起きる。|P15|D04
|
||||
92|guest.noble_spending|貴族は金払いがよい。|P15|D04
|
||||
92|guest.noble_patience|貴族の忍耐力は極端に低い。|P15|D04
|
||||
93|guest.cleric_underground_exit|聖職者が地下要素を目撃すると即帰宅する。|P15
|
||||
93|guest.cleric_morality_loss|聖職者が地下要素を目撃すると風紀が下がる。|P15|D04
|
||||
94|guest.elf_nature|エルフは自然物を好む。|P15|D04
|
||||
95|guest.demon_underground_tolerance|魔族は地下で満足度が下がらない。|P15
|
||||
95|guest.demihuman_underground_tolerance|亜人は地下で満足度が下がらない。|P15
|
||||
98|staff.guild_hiring|一般従業員をギルドから雇用する。|P16
|
||||
98|staff.daily_wages|一般従業員に日給が発生する。|P05|D04
|
||||
98|staff.general_stress|一般従業員はストレスが溜まりやすい。|P16|D04
|
||||
98|staff.general_dismissal|一般従業員を通常操作で解雇できる。|P16
|
||||
99|staff.market_acquisition|原本の市場から対象ユニットを獲得できる。|P16|D04
|
||||
99,112|staff.debt_acquisition|破産客から対象ユニットを獲得できる。|P16|D04
|
||||
99|staff.bound_food_only|原本の拘束ユニットには食費のみが発生する。|P05|D04
|
||||
99|staff.bound_stress_resistance|原本の拘束ユニットは高いストレス耐性を持つ。|P16|D04
|
||||
101|staff.stamina|従業員にStaminaを保持する。|P16
|
||||
101|staff.stress|従業員にStressを保持する。|P16
|
||||
101|staff.loyalty|従業員にLoyaltyを保持する。|P16
|
||||
102|staff.skill_operations|料理・清掃に運営技術を用いる。|P16
|
||||
102|staff.skill_service|接客等に奉仕技術を用いる。|P16
|
||||
102|staff.skill_combat|警備に戦闘力を用いる。|P16
|
||||
103|staff.random_traits|生成時にランダム特性を付与する。原本の例示を必須全種と捏造しない。|P16
|
||||
112|staff.recruitment|通常操作で求人募集できる。|P16
|
||||
113|staff.dismissal_severance|解雇で退職金を支払う。|P05|D04
|
||||
113|staff.sale_price_stats|ユニット売却価格はステータスによって変わる。|P16|D04
|
||||
113|staff.sale_loyalty_risk|ユニット売却により他職員の忠誠度が下がるリスクがある。|P16|D04
|
||||
116|unique.irreplaceable|ユニーク人物は代替不可能である。|P17
|
||||
116|unique.hire_free|ユニーク人物の雇用費は無料。|P17
|
||||
116|unique.upkeep_free|ユニーク人物の維持費は無料。|P05
|
||||
116|unique.high_growth|ユニーク人物の成長上限は一般従業員より高い。|P17|D04
|
||||
116|unique.no_dismissal|ユニーク人物を解雇できない。|P17
|
||||
116|unique.no_sale|ユニーク人物を売却できない。|P17
|
||||
127|unique.stat_stamina|ユニーク人物に体力を保持する。|P17
|
||||
127|unique.stat_stress|ユニーク人物にストレスを保持する。|P17
|
||||
127|unique.stat_operations|ユニーク人物に運営能力を保持する。|P17
|
||||
127|unique.stat_service|ユニーク人物に奉仕能力を保持する。|P17
|
||||
127|unique.stat_combat|ユニーク人物に戦闘能力を保持する。|P17
|
||||
127|unique.stat_underground|ユニーク人物にアングラ能力を保持する。|P17
|
||||
128|unique.work_exp|業務によってEXPを蓄積する。|P17|D04
|
||||
128|unique.level_99|ユニーク人物はLv99まで成長する。|P17
|
||||
130|unique.passive_on_assignment|配置によってパッシブスキルが自動発動する。|P17
|
||||
131|unique.active_skill|防衛戦等でアクティブスキルを任意発動できる。|P17|D04
|
||||
132|unique.route_skills|ルート進行度によって習得スキルが変わる。|P17|D08
|
||||
134|unique.work_warning|長時間勤務の警告を表示する。|P17
|
||||
134|unique.work_24_hours|警告を無視して24時間勤務できる。|P17
|
||||
134|unique.work_stress_surge|24時間勤務でストレスが激増する。|P17|D04
|
||||
134|unique.outfit_aptitude|衣装変更で業務適正補正が変わる。|P17|D04
|
||||
135|unique.no_normal_loss|ユニーク人物は原則として通常ロストしない。|P17
|
||||
135|unique.defeat_temporary_absence|防衛敗北で原本の一時離脱状態を管理する。|P21|D08
|
||||
135|unique.bad_end_transformation|指定バッドエンド確定時だけ原本の人物状態変更を行う。|P19|D08
|
||||
138|relationship.three_emotions|感情は原本の3パラメータで管理する。|P18|D07
|
||||
140|relationship.affection_shield_max|好感度が愛の盾の最大耐久値になる。|P18|D07
|
||||
141|relationship.lust_reduces_defense|原本のLust値上昇で盾の防御力(ダメージ軽減率)が下がる。|P18|D04
|
||||
142|relationship.ntr_after_break|NTR度は盾破壊後に上昇する。|P18
|
||||
143|relationship.shield_blocks|愛の盾が有効な間はNTR攻撃を防ぐ。|P18
|
||||
143,146|relationship.zero_break|盾耐久値0でブレイク状態へ移る。|P18
|
||||
145|relationship.coassigned_attack|ライバルと同配置で盾耐久値が減る。|P18|D04
|
||||
147|relationship.contact_after_break|ブレイク後のライバル接触でNTR度が直接上がる。|P18|D04
|
||||
148|relationship.threshold_shield_loss|NTR度が指定閾値を越えると盾が消滅する。|P18|D04
|
||||
148|relationship.threshold_love_loss|指定閾値を越えた陥落後は純愛ルートが完全消失する。|P18|D04
|
||||
151|duel.combat|武力決闘を通常操作で選べる。|P18|D04
|
||||
151|duel.business|商談決闘を通常操作で選べる。|P18|D04
|
||||
151|duel.third_type|原本の第三種決闘を通常操作から実行できる。内容と演出が未制作なら受入しない。|P18|D08
|
||||
152|duel.win_exile|決闘勝利でライバルを追放できる。|P18
|
||||
152|duel.win_subordinate|決闘勝利でライバルを隷属状態へ移せる。|P18
|
||||
152|duel.win_full_shield|決闘勝利で愛の盾を全回復する。|P18
|
||||
153|duel.loss_route_lock|決闘敗北で原本の即時ルート確定状態へ移る。|P18
|
||||
155|route_action.date_heal|デートで盾を回復する。|P18|D04
|
||||
155|route_action.talk_heal|会話で盾を回復する。|P18|D04
|
||||
155|route_action.rival_isolation|ライバルを物理的に隔離して進行を抑止できる。|P18
|
||||
155|route_action.duel_removal|決闘でライバルを排除して進行を抑止できる。|P18
|
||||
156|route_action.neglect_weakens|放置により原本の盾弱体化が進む。|P18|D04
|
||||
156|route_action.underground_weakens|地下業務により原本の盾弱体化が進む。|P18|D04
|
||||
156|route_action.observation_progress|観察部屋から原本のルート進行状態を確認できる。|P19|D08
|
||||
157|route_action.item_lust|原本指定アイテムによってLust値が増す。|P18|D04
|
||||
157|route_action.work_lust|地下業務によってLust値が増す。|P18|D04
|
||||
157|route_action.shield_cap_loss|原本の堕落進行で盾耐久値上限が減る。|P18|D04
|
||||
157|route_action.corruption_terminal|原本の堕落終端状態を管理する。|P19|D08
|
||||
159|history.first_partner|原本の初回相手の履歴フィールドを保持する。|P19
|
||||
159|history.experience_count|原本の経験数の履歴フィールドを保持する。|P19
|
||||
159|history.current_partner|現在のパートナーを記録する。|P19
|
||||
160|history.cycle|原本の周期フィールドを保持する。|P19|D04
|
||||
160|history.parentage|原本の父親判定を保持する。|P19|D04
|
||||
160|history.pregnancy_appearance|原本の妊娠期に外見が変わる。|P25|D08
|
||||
160|history.pregnancy_stats|原本の妊娠期にステータスが変わる。|P19|D04
|
||||
161,235,242|asset.portrait_ring|純愛ルートの指輪差分を立ち絵へ反映する。|P25|D08
|
||||
161,235,242|asset.portrait_kiss_mark|NTRルートのキスマーク差分を立ち絵へ反映する。|P25|D08
|
||||
161,235,242|asset.portrait_sigil|原本の堕落ルート指定の紋差分を立ち絵へ反映する。|P25|D08
|
||||
161,235,242|asset.portrait_collar|原本の堕落ルート指定の首輪差分を立ち絵へ反映する。|P25|D08
|
||||
161,235,242|asset.portrait_brand|原本の堕落ルート指定の刻印差分を立ち絵へ反映する。|P25|D08
|
||||
164|construction.front_access|表層施設は客用通路へアクセス必須。|P08
|
||||
164|construction.back_access|裏層施設は裏通路へアクセス必須。|P08
|
||||
165|facility.upgrade_to_max|施設を指定LvMAXまで拡張できる。|P09|D04
|
||||
165,179|facility.micro_price|レベル上昇で価格設定(高額設定含む)が解禁される。|P09|D04
|
||||
179|facility.micro_food_rank|レベル上昇で食材ランク設定が解禁される。|P11|D04
|
||||
179|facility.micro_bath_policy|レベル上昇で原本の浴場ポリシー設定が解禁される。|P11|D04
|
||||
179|facility.micro_adult_policy|レベル上昇で原本の成人施設ポリシー設定が解禁される。|P13|D08
|
||||
179|facility.micro_casino_cheat|レベル上昇でカジノのイカサマ率設定が解禁される。|P11|D04
|
||||
182|economy.front_gold_account|表金を裏金と分けて保持する。|P05
|
||||
182|economy.back_gold_account|裏金を表金と分けて保持する。|P05
|
||||
183|mana.energy|マナを施設維持エネルギーとして使う。|P06|D04
|
||||
183|mana.zero_global_stop|マナ枯渇時に全館機能が停止する。|P06
|
||||
184|economy.material_construction|建設に資材を使う。|P08
|
||||
184|economy.material_repair|修理に資材を使う。|P21|D04
|
||||
186|economy.income_lodging|宿泊料を表の収益へ記帳する。|P05
|
||||
186|economy.income_food|飲食売上を表の収益へ記帳する。|P05
|
||||
187|economy.income_adult|原本の成人施設収入を裏の収益へ記帳する。|P05
|
||||
187|economy.income_casino|カジノ収入を裏の収益へ記帳する。|P05
|
||||
187|economy.income_blackmail|脅迫金を裏の収益へ記帳する。|P05
|
||||
187|economy.income_trade|ユニット売却収入を裏の収益へ記帳する。|P05
|
||||
188|economy.expense_wages|人件費を経費へ記帳する。|P05
|
||||
188|economy.expense_upkeep|維持費を経費へ記帳する。|P05
|
||||
188|economy.expense_purchase|仕入れ原価を経費へ記帳する。|P05
|
||||
188|economy.expense_loan|闇銀行への借金返済を記帳する。|P05
|
||||
188|economy.expense_rank_tax|世界樹ランク税を経費へ記帳する。|P05|D04
|
||||
190|economic_facility.reception|受付/精算所をロビーへ設置できる。|P05|D04
|
||||
190|economic_facility.reception_loss|受付処理が遅いと機会損失が生じる。|P05|D04
|
||||
191|economic_facility.vault_limit|金庫室で所持金上限を解放する。|P05|D04
|
||||
191|economic_facility.vault_target|金庫室が盗賊の標的になる。|P21|D04
|
||||
192|economic_facility.purchase_auto|購買部で消費品を自動発注する。|P05|D04
|
||||
192|economic_facility.purchase_loot|購買部で戦利品を買い取る。|P05|D04
|
||||
193|economic_facility.bank_instant|闇銀行から即時融資を受ける。|P05|D04
|
||||
193|economic_facility.bank_default|返済滞納で店舗破壊部隊が襲来する。|P21|D04
|
||||
196|guest.credit_exceeded|ツケ払いが信用度を超えると破産する。|P15|D04
|
||||
196|guest.debt_settlement_imprisonment|女性破産客の原本イベントで即座にユニット拘束と投獄へ移る。|P19|D08
|
||||
214|ui.hud_gold|上部に資金を表示する。|P24
|
||||
214|ui.hud_mana|上部にマナを表示する。|P24
|
||||
214|ui.hud_materials|上部に資材を表示する。|P24
|
||||
214|ui.hud_time|上部に時間を表示する。|P24
|
||||
214|ui.hud_weather|上部に天候を表示する。|P24
|
||||
214|ui.hud_rank|上部に世界樹ランクを表示する。|P24
|
||||
215|ui.bottom_layer_toggle|下部にFRONT/BACKの大きなレイヤー切替を表示する。|P24
|
||||
215|ui.bottom_build_menu|下部に建設メニューを表示する。|P24
|
||||
215|ui.bottom_management_menu|下部に管理メニューを表示する。|P24
|
||||
216|ui.mouse_scroll|断面マップをマウスでスクロールできる。|P24
|
||||
216|ui.zoom|断面マップをズームイン/アウトできる。|P24
|
||||
216|ui.layer_emphasis|断面マップで選択中の表/裏層を視覚的に強調する。|P23
|
||||
218|ui.facility_policy|施設詳細からポリシーを設定できる。|P24
|
||||
218|ui.shift_drag_drop|施設のシフトへドラッグ&ドロップで人物を配置する。|P24
|
||||
219|ui.guest_need_bars|客の欲求をバーで表示する。|P24
|
||||
219|ui.guest_ai_log|客のAI思考ログを表示する。|P24
|
||||
219|ui.staff_emotion_graph|従業員の原本3感情を3軸グラフで表示する。|P24|D07
|
||||
219|ui.shield_durability|従業員の愛の盾耐久値を表示する。|P24
|
||||
221|ui.observation_camera_entry|裏層のカメラアイコンから専用観察画面へ移る。|P24
|
||||
222|ui.observation_angle|専用観察画面でアングルを変更できる。|P24
|
||||
222|ui.observation_capture|専用観察画面でスクリーンショットによる根拠収集ができる。|P24
|
||||
222|ui.observation_intervention|専用観察画面で介入操作ができる。|P24
|
||||
224|ui.grid_snap|建設時にグリッドスナップする。|P24
|
||||
224|ui.connection_warning|建設時に接続ラインの警告を表示する。|P24
|
||||
225|ui.bubble_sleep|ユニット頭上にZzzのフキダシを表示する。|P23
|
||||
225|ui.bubble_anger|ユニット頭上に怒りのフキダシを表示する。|P23
|
||||
225|ui.bubble_heart|ユニット頭上にハートのフキダシを表示する。|P23
|
||||
225|ui.event_log|イベントログを表示する。|P24
|
||||
226|ui.warning_route_invasion|原本の侵食開始に重要警告演出を表示する。|P24
|
||||
226|ui.cut_duel|決闘発生時にカットイン演出を表示する。|P24
|
||||
226|ui.cut_defense|防衛開始時にカットイン演出を表示する。|P24
|
||||
229|art.front_theme|表の自然・秩序・光を表現する。|P25|D09
|
||||
229|art.back_theme|裏の人工・混沌・闇を表現する。|P25|D09
|
||||
230|art.pixel_map|マップを高精細ドット絵で表現する。|P25|D09
|
||||
230|art.pixel_sd|SDキャラを高精細ドット絵で表現する。|P25|D09
|
||||
230|art.anime_portrait|イベント・会話の立ち絵を高品質アニメ塗りで表現する。|P25|D09
|
||||
232|art.layer_ripple|レイヤー切替を魔法レンズ風の波紋としてシームレスに演出する。|P25|D09
|
||||
232|audio.no_mechanical_switch|レイヤー切替に原本で否定された機械的な音を用いない。|P26
|
||||
234|asset.sd_breathing|SDキャラの呼吸モーションを用意する。|P25|D09
|
||||
234|asset.sd_secondary_motion|原本指定のSD身体動作差分を完成素材として組み込む。|P25|D09
|
||||
234,245|asset.sd_job_walk|職種別の歩行モーションを用意する。|P25|D09
|
||||
234,245|asset.sd_adult_loop|原本指定の成人向けループ完成素材をゲームに組み込み、制作・受入状態を中立台帳で追跡する。|P25|D09
|
||||
235|asset.portrait_damage|立ち絵の破損差分を用意する。|P25|D09
|
||||
235|asset.portrait_dirt|立ち絵の汚れ差分を用意する。|P25|D09
|
||||
235|asset.portrait_love_tone|純愛ルートの立ち絵をソフトフォーカスとして区別する。|P25|D09
|
||||
235|asset.portrait_other_tone|他ルートの指定陰影と表情の完成差分をゲームへ組み込み、中立素材台帳で区別する。|P25|D09
|
||||
237,250|asset.ui_front_skin|表の羊皮紙・ファンタジーUIスキンを用意する。|P25|D09
|
||||
237,250|asset.ui_back_skin|裏の黒鉄・モニター風UIスキンを用意する。|P25|D09
|
||||
237|ui.dynamic_skin|レイヤーモードに合わせUIスキンを動的に変更する。|P24
|
||||
238|audio.same_theme_arrangements|表裏のBGMは同曲のアレンジ違いとする。|P26|D09
|
||||
238|audio.back_bass|裏BGMの重低音ベースを強調する。|P26|D09
|
||||
238|audio.crossfade|表裏のBGMをクロスフェード再生する。|P26
|
||||
239|audio.front_birds|表の環境音に小鳥を用意する。|P26|D09
|
||||
239|audio.back_steam|裏の環境音に蒸気管を用意する。|P26|D09
|
||||
239,252|asset.audio_adult|原本指定の成人向けSE完成素材をゲームに組み込み、制作・受入状態を中立台帳で追跡する。|P26|D09
|
||||
242|asset.main_portraits|メインキャラの立ち絵を用意する。|P25|D09
|
||||
242|asset.sub_portraits|サブキャラの立ち絵を用意する。|P25|D09
|
||||
242|asset.portrait_expression|立ち絵の表情差分を用意する。|P25|D09
|
||||
242|asset.portrait_outfit|立ち絵の衣装差分を用意する。|P25|D09
|
||||
243|asset.route_event_cg|原本のルート別イベントCG完成素材をゲームに組み込み、制作・受入状態を中立台帳で追跡する。|P25|D09
|
||||
243|asset.event_cg_variants|原本のイベントCG完成差分をゲームに組み込み、制作・受入状態を中立台帳で追跡する。|P25|D09
|
||||
243|asset.mob_stills|原本のモブ汎用スチル完成素材をゲームに組み込み、制作・受入状態を中立台帳で追跡する。|P25|D09
|
||||
245|asset.sd_idle|SDキャラの待機モーションを用意する。|P25|D09
|
||||
245|asset.sd_work|SDキャラの作業モーションを用意する。|P25|D09
|
||||
245|asset.sd_collapse|SDキャラの気絶モーションを用意する。|P25|D09
|
||||
246|asset.effect_shield|愛の盾エフェクトを用意する。|P25|D09
|
||||
246|asset.effect_smoke|煙エフェクトを用意する。|P25|D09
|
||||
246|asset.effect_mana|マナの光エフェクトを用意する。|P25|D09
|
||||
248|asset.distant_background|マップ背景遠景を用意する。|P25|D09
|
||||
250|asset.ui_emotion_graph|感情グラフUI素材を用意する。|P25|D09
|
||||
250|asset.ui_observation_overlay|専用観察画面に原本指定のカメラ風オーバーレイを用意する。|P25|D09
|
||||
252|asset.dynamic_bgm|動的変化対応のBGM素材を用意する。|P26|D09
|
||||
252|asset.environment_audio|環境音素材を用意する。|P26|D09
|
||||
252|asset.ui_audio|UI操作音素材を用意する。|P26|D09
|
||||
252|asset.character_voice|キャラクターボイス素材を用意する。|P26|D09
|
||||
'''
|
||||
for row in SPEC.strip().splitlines():
|
||||
parts=row.split('|'); nums,key,condition,package=parts[:4]
|
||||
group_ids=None
|
||||
if nums=='59' or key=='time.speeds':group_ids=['B-3.1a']
|
||||
if key in ['time.pause_build','time.pause_commands']:group_ids=['B-3.1b']
|
||||
if key in ['economy.midnight_report','economy.settlement_calculation']:group_ids=['B-3.4a']
|
||||
if key=='progression.three_deficits_gameover':group_ids=['B-3.4b']
|
||||
if key in ['traffic.astar','traffic.elevator_wait_cost']:group_ids=['B-4.3a']
|
||||
if key.startswith('traffic.') and nums=='86':group_ids=['B-4.3b']
|
||||
if key=='traffic.overlap_half_speed':group_ids=['B-4.3c']
|
||||
add(key,condition,[int(n) for n in nums.split(',')],package,group_ids,pending=parts[4] if len(parts)>4 else None)
|
||||
|
||||
# Each enumerated job is a separate obligation; role aliases do not add employees.
|
||||
for num, jobs,layer in [(105,[('waiter','ウェイター'),('receptionist','受付係'),('bunny','バニー')],'表層'),
|
||||
(106,[('chef','シェフ'),('cleaner','清掃員'),('carrier','運搬員'),('rescue','救護班')],'裏層'),
|
||||
(107,[('adult_service','原本指定の地下接客職'),('security','警備員'),('trainer','原本指定の調教職')],'原本指定層')]:
|
||||
for key,name in jobs:add('job.'+key,f'{name}の職種と{layer}での業務を通常配置から運用する。',[num],'P16',pending='D04')
|
||||
|
||||
# Original 7 characters are canonical identities, independent of current resource IDs.
|
||||
heroes=[('anri','アンリ','女将',118,[('floor_price','配置フロアの全施設売上を20%増やす。',None)]),
|
||||
('fine','フィーネ','メイド',119,[('cleaning_speed','清掃速度を3倍にする。',None),('assassination','暗殺確率を上げる。','D04')]),
|
||||
('tomari','トマリ','騎士',120,[('floor_trouble','配置フロアのトラブルを0%にする。',None),('defense_score','防衛スコアを加算する。','D04')]),
|
||||
('amelia','アメリア','シスター',121,[('donation','寄付金を3倍にする。',None),('recovery','回復速度を2倍にする。',None)]),
|
||||
('liz','リズ','狩人',122,[('rare_material','レア素材発見率を上げる。','D04'),('food_saving','高級食材消費を減らす。','D04')]),
|
||||
('mimi','ミミ','商人',123,[('purchase','仕入れ価格を20%減らす。',None),('sale','売却価格を20%増やす。',None)]),
|
||||
('satsuki','サツキ','雑用',124,[('vip_satisfaction','VIP客の満足度上昇を1.5倍にする。',None),('tip','チップを増やす。','D04')])]
|
||||
for key,name,role,num,effects in heroes:
|
||||
ent={'kind':'heroine','key':key,'original_name':name}
|
||||
add(f'heroine.{key}.identity',f'{name}を原本7人物の一人として通常進行で利用できる。',[30,num],'P17',entity=ent,pending='D08')
|
||||
add(f'heroine.{key}.role',f'{name}は{role}として業務する。',[num],'P17',entity=ent)
|
||||
for effect,condition,pending in effects:
|
||||
add(f'heroine.{key}.{effect}',name+'の固有効果: '+condition,[num],'P17',entity=ent,pending=pending)
|
||||
for route,label in [('love','純愛'),('ntr','NTR'),('corruption','堕落')]:
|
||||
add(f'heroine.{key}.route.{route}',f'{name}の{label}ルートを通常進行で到達・記録できる。',[9,30,33,138],'P19',entity=ent,pending='D08')
|
||||
for key,name in [('rai','ライ'),('riesenbart','リーゼンバルト'),('sou','ソウ')]:
|
||||
add(f'rival.{key}.hiring',f'ライバル{name}を雇用できる。',[125],'P17',entity={'kind':'rival','key':key,'original_name':name},pending='D08')
|
||||
add(f'rival.{key}.risk',f'ライバル{name}の雇用で原本の侵食リスクが生じる。',[125],'P18',pending='D04')
|
||||
|
||||
# Chapter 10 enumerates exactly 30 original facilities. Meaning keys are audit keys,
|
||||
# NOT a claim that those IDs/resources are present in production.
|
||||
FACS=[
|
||||
('standard_room','一般客室',199,'2x1','FRONT',None,'P10',[167,46]),
|
||||
('premium_room','上級客室',199,'4x1','FRONT',None,'P10',[167,46]),
|
||||
('vip_suite','VIPスイート',199,'6x2','FRONT','高層','P10',[167,46]),
|
||||
('dormitory','ドミトリー',199,'4x1','FRONT','地下','P10',[]),
|
||||
('cafeteria','大衆食堂',201,'4x2','FRONT',None,'P11',[168,46]),
|
||||
('restaurant','レストラン',201,'6x2','FRONT',None,'P11',[168,46]),
|
||||
('tavern','酒場',201,'4x2','FRONT',None,'P11',[168,46]),
|
||||
('food_stall','屋台',201,'2x1','FRONT',None,'P11',[]),
|
||||
('public_bath','大浴場',203,'6x2','FRONT',None,'P11',[169]),
|
||||
('casino','カジノ',203,'6x3','FRONT','地下','P11',[169]),
|
||||
('arena','闘技場',203,'8x4','FRONT','地下','P11',[169]),
|
||||
('chapel','礼拝堂',203,'4x3','FRONT',None,'P11',[169]),
|
||||
('adult_private_room','風俗個室',205,'2x1','FRONT','地下','P13',[170]),
|
||||
('luxury_soap','高級ソープ',205,'4x2','FRONT','地下','P13',[170]),
|
||||
('sm_room','SM拷問室',205,'4x2','FRONT','地下','P13',[170]),
|
||||
('black_market','闇市場',205,'4x2','FRONT','地下','P13',[170]),
|
||||
('kitchen','厨房',207,'4x2','BACK',None,'P12',[173]),
|
||||
('warehouse','倉庫',207,'4x2','BACK',None,'P12',[49,173]),
|
||||
('staff_waiting_room','待機室',207,'4x2','BACK',None,'P12',[49,172]),
|
||||
('staff_dormitory','従業員寮',207,'2x1','BACK',None,'P12',[49,172]),
|
||||
('security_station','警備員詰所',207,'2x2','BACK',None,'P12',[172]),
|
||||
('cleaning_room','清掃員室',207,'2x1','BACK',None,'P12',[173]),
|
||||
('infirmary','医務室',207,'4x2','BACK',None,'P12',[173]),
|
||||
('peeping_room','覗き部屋',209,'2x1','BACK',None,'P13',[49,174]),
|
||||
('prison','地下牢',209,'4x2','BACK','地下','P13',[174]),
|
||||
('mana_furnace','マナ炉',209,'6x3','BACK','地下','P13',[174]),
|
||||
('guest_elevator','蔓の昇降機',211,'2x2','FRONT',None,'P14',[46,86,176]),
|
||||
('service_elevator','業務用EV',211,'3x2','BACK',None,'P14',[49,86,176]),
|
||||
('mana_warp','ワープ',211,'2x2','FRONT',None,'P14',[86,176]),
|
||||
('magic_turret','魔法砲台',211,None,None,'外壁','P21',[])]
|
||||
SUPP_FIELDS=[('construction_gold',1,'建設金額',None),('upkeep_gold',2,'運用金額',None),
|
||||
('upkeep_mana',2,'運用マナ量','D04'),('service_price',3,'利用料金','D04'),
|
||||
('footprint',4,'占有サイズ','D06'),('role',5,'役割','D04'),('operation_state',6,'稼働状態',None),
|
||||
('customer_capacity',7,'同時利用顧客人数上限','D02'),('staff_min',8,'必要最低職員人数','D02'),('staff_max',8,'配置可能職員上限','D02'),
|
||||
('related_facility',9,'関連施設指定','D03'),('related_operating',9,'関連施設の実稼働フラグ',None),
|
||||
('service_attribute',10,'提供サービス属性','D01')]
|
||||
for key,name,num,size,layer,zone,package,extra in FACS:
|
||||
ent={'kind':'original_facility','key':key,'original_name':name}
|
||||
anchors=[num]+extra
|
||||
add(f'facility.{key}.exists',f'原本施設{name}を通常建設・運用できる。',anchors,package,entity=ent)
|
||||
if size:add(f'facility.{key}.footprint',f'{name}の占有寸法は{size}。',[num],package,entity=ent,pending='D06')
|
||||
if layer:add(f'facility.{key}.layer',f'{name}の層は{layer}。',[num],package,entity=ent)
|
||||
if zone:add(f'facility.{key}.zone',f'{name}の配置条件は{zone}。',[num],package,entity=ent,pending='D04' if zone=='高層' else None)
|
||||
for field,no,label,pending in SUPP_FIELDS:
|
||||
rid='AR.'+f'facility.{key}.{field}'
|
||||
cond=records[rid]['condition'] if rid in records else f'{name}の{label}を明示値または明示未設定として保持し、必要確定値は営業受入までに固定する。'
|
||||
rec=add(f'facility.{key}.{field}',cond,[983+no],'P09',source=SUPPLEMENT,
|
||||
authority='retained_record_of_explicit_user_facility_amendment',supplement=f'F{no:02}',entity=ent,pending=pending)
|
||||
if field=='operation_state':rec['acceptance']['condition_assertion']=f'{name}のINACTIVE/OPERATING/SUSPENDEDを区別し、画面・Runtime・保存で一致する。'
|
||||
if field=='customer_capacity':rec['acceptance']['condition_assertion']=f'{name}の利用中人数で専用上限を判定する。待機者とcapacityを流用しない。未設定を確定済み上限として受け入れない。'
|
||||
if field=='staff_max':rec['acceptance']['condition_assertion']=f'{name}の職員上限を最低人数と分離し、上限超過の配置を原状保持で拒否する。'
|
||||
if field=='related_operating':rec['acceptance']['condition_assertion']=f'{name}の関連先について配置有無ではなく実稼働を判定する。'
|
||||
# Unspecified extension values and required operating values have different contracts.
|
||||
for field,no,label,pending in SUPP_FIELDS:
|
||||
rec=records[f'AR.facility.{key}.{field}']
|
||||
rec['value_contract']={'field':field,'known_value':None,'certainty':'not_reaudited',
|
||||
'null_or_unset_allowed_as_final':field=='related_facility',
|
||||
'unset_policy':'単一関連先なしは表現できる。複数関連先/AND/ORは承認なしに追加しない。' if field=='related_facility' else '未設定は台帳で可視化するが必要営業値の確定・受入の代用にしない。',
|
||||
'source_has_numeric_value':False}
|
||||
if field=='footprint' and size:rec['value_contract'].update(known_value=size,certainty='original_explicit',source_has_numeric_value=True)
|
||||
if field=='operation_state':rec['value_contract'].update(known_value=['INACTIVE','OPERATING','SUSPENDED'],certainty='explicit_amendment_enum')
|
||||
# Finished visual slots are finite explicitly specified combinations (30 x 3 x 2 x 2).
|
||||
for level in [1,2,3]:
|
||||
for view in ['front','back']:
|
||||
for part,label in [('exterior','外観'),('interior','内装')]:
|
||||
add(f'asset.facility.{key}.lv{level}.{view}.{part}',f'{name} Lv{level} {view} の{label}完成素材を用意する。',[248],'P25',entity=ent,pending='D09')
|
||||
|
||||
# Other original infrastructure remains outside the chapter-10 count, never silently dropped.
|
||||
for key,name,nums in [('guest_corridor','客用通路',[46,177]),('service_corridor','裏通路',[49,177]),('stairs','階段',[86,176])]:
|
||||
add(f'infrastructure.{key}.exists',f'{name}を通常建設し移動へ接続する。',nums,'P14',pending='D04')
|
||||
for key in ['cafeteria','restaurant','tavern']:
|
||||
add(f'facility.{key}.kitchen_dependency',f'{dict((f[0],f[1]) for f in FACS)[key]}は裏層の厨房とセットで稼働する。',[168],'P11')
|
||||
add('facility.kitchen.behind_food','厨房は表層飲食店の裏に置く。',[173],'P12')
|
||||
add('facility.vip_suite.high_beauty','VIPスイートは高景観を必要とする。',[167],'P10',pending='D04')
|
||||
for key,nums in [('vip_suite',[167,199]),('public_bath',[169,203]),('adult_private_room',[170,205])]:
|
||||
add(f'facility.{key}.observation','対象施設は原本指定の観察機能に対応する。',nums,'P13')
|
||||
add('facility.tavern.noise','酒場が騒音を発生する。',[201],'P22',pending='D04')
|
||||
add('facility.staff_dormitory.night_event','従業員寮に原本指定の夜間イベント対応がある。',[172],'P19',pending='D08')
|
||||
add('facility.mana_furnace.unit_consumption','マナ炉で原本指定のユニット消費が発生する。',[209],'P13',pending='D04')
|
||||
|
||||
# Global semantic contracts for the ten fields. These are distinct from individual configured values.
|
||||
CONTRACTS=[
|
||||
('construction.payment',1,'通常建設で配置コストを支払う。','P08',None),
|
||||
('facility.state_three_values',6,'稼働状態はINACTIVE/OPERATING/SUSPENDEDの3状態として扱う。','P09',None),
|
||||
('facility.destroy_suspend',6,'破壊された施設を休止する。','P09',None),
|
||||
('facility.enforcement_suspend',6,'摘発された施設を休止する。','P09',None),
|
||||
('facility.no_auto_resume',6,'関連施設の条件回復だけで破壊・摘発後の休止を解除しない。','P09','D03'),
|
||||
('facility.customer_not_capacity',7,'customer_capacityを保管量・生産量等のcapacityから推定しない。','P09','D02'),
|
||||
('facility.customer_wait_separate',7,'待機者と利用中顧客数を分ける。','P09',None),
|
||||
('facility.staff_min_separate',8,'職員最低人数と職員上限を別に保持する。','P09','D02'),
|
||||
('facility.related_single',9,'関連先は単一指定の現契約を保持する。','P09','D03'),
|
||||
('facility.related_not_placement',9,'関連施設の配置有無を実稼働判定に置き換えない。','P09',None),
|
||||
|
||||
('pricing.attributes',10,'サービス属性の既存4型(appetite/relaxation/sexual/gambling)を区別する。','P09','D01'),
|
||||
('pricing.match_surcharge',3,'欲求一致時だけ指定した加算を料金へ適用する。','P09','D01'),
|
||||
('pricing.mismatch_allowed',10,'欲求不一致を施設利用不可の理由にしない。','P09',None),
|
||||
('pricing.no_unset_invention',10,'不足基準・加算額/率・複数属性・重複加算の未指定を勝手に補完しない。','P09','D01'),
|
||||
('facility.no_resume_invention',6,'指定されていない休止復帰条件を追加しない。','P09','D03'),
|
||||
('facility.no_multiple_related_invention',9,'指定されていない複数関連施設とAND/ORを追加しない。','P09','D03')]
|
||||
for key,no,cond,package,pending in CONTRACTS:
|
||||
add(key,cond,[983+no],package,source=SUPPLEMENT,authority='retained_record_of_explicit_user_facility_amendment',supplement=f'F{no:02}',pending=pending)
|
||||
|
||||
# Approved A01-A05 refine existing atoms; none create a second weight for the same value.
|
||||
DEC_LINE={'A01':6,'A02':13,'A03':20,'A04':27,'A05':33}
|
||||
def approved(key,cond,decision,package='P09',replace=False,pending=None):
|
||||
r=add(key,cond,[DEC_LINE[decision]],package,source=DECISIONS,
|
||||
authority='explicit_user_approved_amendment',decision=decision,replace=replace,pending=pending)
|
||||
if replace:
|
||||
r['value_contract']={'known_value_statement':cond,'certainty':'approved_initial_trial_value','decision_id':decision,'null_or_unset_allowed_as_final':False}
|
||||
r['pending_decision_ids']=[x for x in r['pending_decision_ids'] if x not in ['D01','D02','D04']]
|
||||
r['decision_dependencies']=[x for x in r['decision_dependencies'] if x['decision_id'] in r['pending_decision_ids']]
|
||||
return r
|
||||
for key,cond in [
|
||||
('upkeep.start_completion','維持費は施設完成時から計上する。'),
|
||||
('upkeep.end_removal','維持費は撤去まで計上する。'),
|
||||
('upkeep.time_proration','維持費日額を保有時間で按分する。'),
|
||||
('upkeep.closed_accrual','閉店中も維持費を計上する。'),
|
||||
('upkeep.suspended_accrual','休止中も維持費を計上する。'),
|
||||
('upkeep.midnight_payment','維持費を00:00に精算する。'),
|
||||
('upkeep.shortfall_carry','維持費不足額を翌日へ繰り越す。')]: approved(key,cond,'A01','P05')
|
||||
for key,cond in [
|
||||
('upkeep.trial_rate','一般施設の維持費初期試験値は建築費の0.5%/日。'),
|
||||
('upkeep.integer_arithmetic','維持費は既存の整数処理を保持する。'),
|
||||
('upkeep.special_override','礼拝堂・清掃員室の維持費に個別承認値を優先する。')]:approved(key,cond,'A02','P05')
|
||||
approved('heroine.anri.recruit_entry','従業員画面でアンリの無料加入を選ぶ。','A03','P17')
|
||||
approved('heroine.anri.floor_price','配置フロアのアンリ補正で全施設の利用料金を20%増加する。','A03','P17',replace=True)
|
||||
approved('heroine.anri.wallet_revenue','アンリ補正後の客支払額と施設収入を一致させる。','A03','P05')
|
||||
for decision,key,vals in [
|
||||
('A04','chapel',{'footprint':'占有寸法4x3','layer':'表層','construction_gold':'建築1200G','construction_materials':'資材12','construction_minutes':'工期180分','customer_capacity':'同時利用8人','staff_min':'聖職者最低1人','staff_max':'聖職者上限2人','service_minutes':'礼拝60分','service_price':'寄付20G','completion_faith':'礼拝完了時信仰+1','upkeep_gold':'維持費6G/日'}),
|
||||
('A05','cleaning_room',{'footprint':'占有寸法2x1','layer':'裏層','construction_gold':'建築350G','construction_materials':'資材4','construction_minutes':'工期60分','staff_min':'清掃員最低1人','staff_max':'清掃員上限2人','customer_capacity':'顧客0人','service_price':'利用料なし','upkeep_gold':'維持費2G/日'})]:
|
||||
for field,val in vals.items():approved(f'facility.{key}.{field}',f'{key}の承認初期試験値は{val}。',decision,replace=True)
|
||||
for key,cond in [
|
||||
('cleaning.cleanliness_range','清潔度の範囲は0〜100。'),
|
||||
('cleaning.initial_cleanliness','清潔度初期値は100。'),
|
||||
('cleaning.reservation_threshold','清潔度75以下で清掃予約する。'),
|
||||
('cleaning.on_site_minutes','現地清掃所要時間は8分。'),
|
||||
('cleaning.recovery_amount','現地清掃完了で清潔度を25回復する。'),
|
||||
('cleaning.defined_dirt_only','定義済み汚れ量を使用する。'),
|
||||
('cleaning.no_undefined_dirt','未定義の汚れ量を勝手に補完しない。')]:approved(key,cond,'A05','P16')
|
||||
|
||||
# Historical milestones: content requirements remain mapped in their feature domains;
|
||||
# these atoms test the dated delivery promise and cannot be retrospectively passed.
|
||||
SCHEDULE=[(255,'B-14.1','2026/03〜08',[('layers','表裏レイヤー切替'),('ai','AI移動ロジック'),('assets','基本素材'),('mvp','MVP')]),
|
||||
(256,'B-14.2','2026/09〜2027/02',[('parameters','パラメータ管理'),('portraits','立ち絵量産'),('tiles','施設タイル量産'),('scenario','シナリオ組込み'),('bgm','通常BGM組込み')]),
|
||||
(257,'B-14.3','2027/03〜08',[('adult','原本指定の成人向け内容'),('observation','専用観察モード'),('voice_record','ボイス収録'),('voice_integrate','ボイス組込み'),('balance','ゲームバランス徹底調整')]),
|
||||
(258,'B-14.4','2027/09〜12',[('debug','進行不能バグ等のデバッグ'),('ui','UIレスポンス向上'),('mastering','マスタリング'),('final','最終完成')])]
|
||||
for num,gid,window,deliverables in SCHEDULE:
|
||||
for key,label in deliverables:
|
||||
r=add(f'schedule.{gid}.{key}',f'{window}の工程内に{label}を達成する。',[num],'P00',[gid],scope='historical_schedule')
|
||||
r['acceptance']['evidence_policy']='日付付き当時の成果が必要。後日の機能実装で過去納期達成をPASSにしない。'
|
||||
r['historical_due_window']=window
|
||||
|
||||
ADDITIONAL_APPLICABILITY_EXPANSION = {'decision_id': 'D09', 'scope': 'additional_named_facilities_and_infrastructure_applicability', 'reason': '原本第10章30施設以外にも有限の名指し施設/設備がある。通常機能条件は存在するが、追加F01〜F10とLv別表裏/外内素材の全適用範囲は未確定。30施設の展開から外れていることを最終全施設網羅として受入しない。適用範囲確定時に原子ID追加/統合と分母改訂の履歴を記録する。一律12素材枠や全項目適用を推測で追加しない。', 'source_examples': [{'lines': [190], 'names': ['受付/精算所']}, {'lines': [191], 'names': ['金庫室']}, {'lines': [192], 'names': ['購買部']}, {'lines': [193], 'names': ['闇銀行']}, {'lines': [49, 177], 'names': ['裏通路']}, {'lines': [46, 177], 'names': ['客用通路']}, {'lines': [50, 177], 'names': ['従業員ドア']}, {'lines': [86, 176], 'names': ['階段']}, {'lines': [49], 'names': ['リネンシュート', '隠し扉']}], 'applicability_decisions_required': ['facility_supplement_F01_to_F10', 'levels_1_to_3', 'front_back_views', 'exterior_interior_visuals']}
|
||||
|
||||
# Align primary responsibility with the reviewed plan's actual package titles.
|
||||
for rec in records.values():
|
||||
rid=rec['id']; primary=rec['primary_package']
|
||||
if primary=='P22':primary='P07' # weather/environment implementation; P22 is the asset inventory
|
||||
if primary=='P07':primary='P23' if rid.startswith(('AR.ui.','AR.world.')) else 'P07'
|
||||
if rid.startswith(('AR.weather.','AR.environment.','AR.news.')):primary='P06' if rid=='AR.environment.ground_mana' else 'P07'
|
||||
if rid in ['AR.world.unit_grid','AR.world.above_ground','AR.world.below_ground','AR.world.ground_boundary']:primary='P08'
|
||||
if rid=='AR.world.dual_management':primary='P20'
|
||||
if rid.startswith(('AR.duel.','AR.route_action.')):primary='P19'
|
||||
if rid.startswith('AR.audio.'):primary='P25'
|
||||
if rid=='AR.art.layer_ripple':primary='P23'
|
||||
if rid.startswith('AR.asset.portrait_') or rid in ['AR.asset.main_portraits','AR.asset.sub_portraits','AR.asset.route_event_cg','AR.asset.event_cg_variants','AR.asset.mob_stills','AR.history.pregnancy_appearance']:primary='P26'
|
||||
if rid in ['AR.asset.dynamic_bgm','AR.asset.environment_audio','AR.asset.ui_audio','AR.asset.audio_adult']:primary='P25'
|
||||
if rid=='AR.asset.character_voice':primary='P26'
|
||||
if rid.startswith(('AR.facility.cafeteria.','AR.facility.restaurant.','AR.facility.tavern.','AR.facility.food_stall.')) and primary=='P11':primary='P10'
|
||||
if rid=='AR.facility.tavern.noise':primary='P07'
|
||||
rec['primary_package']=rec['package']=primary
|
||||
rec['normal_entry']=normal_entries.get(primary,'要件別通常入口の設計と検証')
|
||||
# Group-level routing is kept for cross-cutting work, separately from primary ownership.
|
||||
inherited={p for g in coverage_groups if g['requirement_id'] in rec['source_group_ids'] for p in g['work_packages']}
|
||||
rec['work_packages']=sorted(inherited|{primary})
|
||||
|
||||
# All group mappings use condition anchors. Headings are structural aliases, not extra counts.
|
||||
line_map={n:[] for n in range(1,len(lines)+1)}
|
||||
for r in records.values():
|
||||
for a in r['source_anchors']:
|
||||
if a['path']==BASE:
|
||||
for n in a['lines']:line_map[n].append(r['id'])
|
||||
coverage=[]
|
||||
for n,line in enumerate(lines,1):
|
||||
ids=sorted(set(line_map[n])); kind='condition'
|
||||
if line.startswith('#'):
|
||||
kind='structural_heading'
|
||||
level=len(line)-len(line.lstrip('#'))
|
||||
end=n
|
||||
for j in range(n+1,len(lines)+1):
|
||||
if lines[j-1].startswith('#') and len(lines[j-1])-len(lines[j-1].lstrip('#'))<=level:break
|
||||
end=j
|
||||
ids=sorted({rid for j in range(n+1,end+1) for rid in line_map[j]})
|
||||
elif n==7:
|
||||
kind='structural_label';ids=sorted(set(line_map[8]+line_map[9]))
|
||||
elif n==18:
|
||||
kind='structural_label';ids=sorted(set(line_map[19]+line_map[20]))
|
||||
elif n==150:
|
||||
kind='structural_description';ids=sorted(set(line_map[151]+line_map[152]+line_map[153]))
|
||||
elif n==254:
|
||||
kind='schedule_window';ids=sorted({rid for j in range(255,259) for rid in line_map[j]})
|
||||
coverage.append({'line':n,'kind':kind,'line_sha256':hashlib.sha256(line.encode()).hexdigest(),'atomic_ids':ids})
|
||||
|
||||
feature_ids=sorted(r['id'] for r in records.values() if r['scope']=='feature_or_content')
|
||||
schedule_ids=sorted(r['id'] for r in records.values() if r['scope']=='historical_schedule')
|
||||
register={'schema_version':1,'register_version':'atomic-v1-2026-10-02','generated_at_jst':now,
|
||||
'production_root':str(ROOT),'source_head':head,'source_pins':pins,
|
||||
'authority_policy':'指定原本保存版 > 明示承認/追加仕様(重複条件を精緻化) > 別冊参照 > 派生文書。今回新規Drive取得なし。',
|
||||
'completion_views':VIEWS,'state_policy':'未再監査はunverified/not_reaudited。PASSと適用外は証拠と独立判定者が必要。',
|
||||
'id_policy':'AR.<domain>.<canonical-source-entity>.<condition>; canonical-source-entity is an audit identity, not a production resource id',
|
||||
'denominator_version':'atomic-v1-2026-10-02','denominator_frozen':True,
|
||||
'denominator_scope':'Known-condition register v1 only. Final denominator is NOT established while D08/D09 target catalogs remain unresolved. Saved-original conditions plus retained explicit additions and A01-A05; no invented content targets or unapproved values.',
|
||||
'final_denominator_established':False, 'final_catalog_coverage_claimed':False,
|
||||
'denominator_ids':feature_ids,'historical_schedule_ids':schedule_ids,'duplicate_merges':merges,
|
||||
'open_expansion_register':[{'decision_id':'D08','reason':'全エンド/図鑑対象/章/制作内容の個別IDは未指定。集合契約と各人物3ルートを保持。確定時に原子ID追加・統合を分母改訂履歴へ記録し、集合契約の合格を個別内容の代用にしない。'},
|
||||
{'decision_id':'D09','reason':'サブ人物数・表情/衣装/CG/音の全差分量は未指定。有限明示される施設30×Lv1〜3×表裏×外内のみ個別化済み。確定数量の独立条件は分母改訂が必要。'}],
|
||||
'requirements':sorted(records.values(),key=lambda r:r['id'])}
|
||||
register['open_expansion_register'].append(ADDITIONAL_APPLICABILITY_EXPANSION)
|
||||
source_coverage={'schema_version':1,'source_path':BASE,'source_sha256':pins[BASE],
|
||||
'line_count':len(lines),'group_count':len(groups),'source_lines':coverage,
|
||||
'groups':[{'group_id':g['id'],'source_lines':g['source_lines'],
|
||||
'atomic_ids':sorted(r['id'] for r in records.values() if g['id'] in r['source_group_ids']),
|
||||
'scope':'historical_schedule' if g['id'].startswith('B-14.') else 'feature_or_content'} for g in groups],
|
||||
'facility_ids':[{'key':f[0],'original_name':f[1], 'original_line':f[2],
|
||||
'atomic_ids':sorted(r['id'] for r in records.values() if r['entity'] and r['entity'].get('kind')=='original_facility' and r['entity']['key']==f[0])} for f in FACS],
|
||||
'heroine_ids':[{'key':h[0],'original_name':h[1], 'atomic_ids':sorted(r['id'] for r in records.values() if r['entity'] and r['entity'].get('kind')=='heroine' and r['entity']['key']==h[0])} for h in heroes],
|
||||
'approved_decisions':[{'decision_id':d, 'atomic_ids':sorted(r['id'] for r in records.values() if d in r['user_amendment_refs'])} for d in DEC_LINE],
|
||||
'facility_supplements':[{'supplement_id':f'F{n:02}', 'atomic_ids':sorted(r['id'] for r in records.values() if f'F{n:02}' in r['supplement_ids'])} for n in range(1,11)]}
|
||||
freeze={'schema_version':1,'denominator_version':register['denominator_version'],'source_head':head,
|
||||
'source_pins':pins,'calculated_at_jst':now,'denominator_frozen':True,'counting_unit':'one unique independently judgeable source condition',
|
||||
'freeze_scope':'known-condition v1 only; final content-target denominator remains unresolved',
|
||||
'final_denominator_established':False,'denominator':len(feature_ids),'denominator_ids':feature_ids,'historical_schedule_count':len(schedule_ids),'historical_schedule_ids':schedule_ids,
|
||||
'implemented_ids':[],'accepted_ids':[],'blocked_ids':[], 'partial_ids':[], 'missing_evidence_ids':feature_ids,
|
||||
'difference_ids':[],'difference_audit_state':'not_reaudited',
|
||||
'not_reaudited_ids':feature_ids,'pending_decision_ids':sorted({d for r in records.values() for d in r['pending_decision_ids']}),
|
||||
'implementation_percent':0.0,'acceptance_percent':0.0,
|
||||
'percent_label':'currently evidenced whole-atom completion; 0 does not mean no legacy code exists',
|
||||
'applicability_review_pending_ids':feature_ids,'independent_reviewer':None,
|
||||
'change_control':{'requires':['previous_version','new_version','added_ids','removed_ids','merged_ids','reason','reviewer','source_reference'],
|
||||
'history':[{'previous_version':None,'new_version':register['denominator_version'],'added_ids':feature_ids,
|
||||
'removed_ids':[],'merged_ids':merges,'reason':'initial source split; no game acceptance inferred','reviewer':None,
|
||||
'review_state':'independent_review_required'}]}}
|
||||
for name,data in [('atomic_requirements.json',register),('source_atomic_coverage.json',source_coverage),('atomic_denominator_freeze.json',freeze)]:
|
||||
(OUT/name).write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n')
|
||||
print(json.dumps({'feature_atoms':len(feature_ids),'historical_schedule_atoms':len(schedule_ids),
|
||||
'source_lines':len(coverage),'unmapped_lines':[r['line'] for r in coverage if not r['atomic_ids']],
|
||||
'unmapped_groups':[r['group_id'] for r in source_coverage['groups'] if not r['atomic_ids']],
|
||||
'merges':len(merges)},ensure_ascii=False))
|
||||
@@ -0,0 +1,522 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Static, source-anchored P09 audit. Never loads Godot or edits production files."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
OUT = Path(__file__).resolve().parent
|
||||
BASE = "docs/traceability/original/BASELINE.txt"
|
||||
ANNEX = "artifacts/original-annex-A-drive-retrieval-2026-09-30-r01/retrieved-readable-annex-A.md"
|
||||
DECISIONS = "docs/plans/original-completion-2026-10-02/decisions.json"
|
||||
ADDITION = "docs/traceability/original/audit-2026-09-29/TRACEABILITY.md"
|
||||
PLAN = "docs/plans/original-completion-2026-10-02/execution/IMPLEMENTATION_PLAN.initial.md"
|
||||
DEF = "scripts/data/facility_definition.gd"
|
||||
CATALOG = "scripts/data/facility_catalog.gd"
|
||||
INSTANCE = "scripts/facility/facility_instance.gd"
|
||||
SERVICE = "scripts/facility/facility_service.gd"
|
||||
PRICING = "scripts/facility/facility_pricing.gd"
|
||||
RUNTIME = "autoload/simulation_runtime.gd"
|
||||
UPKEEP = "docs/plans/original-completion-2026-09-29/execution/UPKEEP_IMPLEMENTATION.md"
|
||||
PINS = {}
|
||||
|
||||
|
||||
def pin(path):
|
||||
if path not in PINS:
|
||||
PINS[path] = hashlib.sha256((ROOT / path).read_bytes()).hexdigest()
|
||||
return PINS[path]
|
||||
|
||||
|
||||
def anchor(path, lines, authority):
|
||||
return {"path": path, "lines": lines, "sha256": pin(path), "authority": authority}
|
||||
|
||||
|
||||
def findline(path, literal):
|
||||
matches = [i for i, line in enumerate((ROOT / path).read_text().splitlines(), 1) if literal in line]
|
||||
assert matches, (path, literal)
|
||||
return matches[0]
|
||||
|
||||
|
||||
def literal(raw):
|
||||
"""Decode only data literals used by the retained .tres files; no eval."""
|
||||
raw = raw.strip()
|
||||
enum = {"MapZone.ANY": 0, "ViewLayer.FRONT": 0, "Vector2i.ONE": [1, 1], "LEGACY_SCHEMA_VERSION": 1}
|
||||
if raw in enum:
|
||||
return enum[raw]
|
||||
wrapped = re.fullmatch(r"Array\[[^]]+\]\((.*)\)", raw)
|
||||
if wrapped:
|
||||
raw = wrapped[1]
|
||||
raw = raw.replace('&"', '"')
|
||||
raw = re.sub(r"Vector2i\(([-\d]+),\s*([-\d]+)\)", r"[\1, \2]", raw)
|
||||
raw = re.sub(r"\btrue\b", "True", raw)
|
||||
raw = re.sub(r"\bfalse\b", "False", raw)
|
||||
raw = re.sub(r"\bnull\b", "None", raw)
|
||||
return ast.literal_eval(raw)
|
||||
|
||||
|
||||
DEFAULTS = {}
|
||||
EXPORTED_KEYS = set()
|
||||
for n, line in enumerate((ROOT / DEF).read_text().splitlines(), 1):
|
||||
name = re.match(r"@export(?:_range\([^)]*\)|_multiline)?\s+var\s+(\w+)", line)
|
||||
if name:
|
||||
EXPORTED_KEYS.add(name[1])
|
||||
m = re.match(r"@export(?:_range\([^)]*\)|_multiline)?\s+var\s+(\w+)(?::[^=]+)?\s*:?=\s*(.+)$", line)
|
||||
if m:
|
||||
DEFAULTS[m[1]] = {"raw": m[2], "value": literal(m[2]), "line": n}
|
||||
|
||||
DEFINITIONS = {}
|
||||
for p in sorted((ROOT / "data/facilities").rglob("*.tres")):
|
||||
rel = str(p.relative_to(ROOT))
|
||||
raw_fields = {}
|
||||
duplicates = []
|
||||
for n, line in enumerate(p.read_text().splitlines(), 1):
|
||||
m = re.match(r"(\w+) = (.+)$", line)
|
||||
if not m or m[1] == "script":
|
||||
continue
|
||||
if m[1] in raw_fields:
|
||||
duplicates.append({"field": m[1], "first_line": raw_fields[m[1]]["line"], "duplicate_line": n,
|
||||
"same_raw_value": raw_fields[m[1]]["raw"] == m[2]})
|
||||
raw_fields[m[1]] = {"raw": m[2], "value": literal(m[2]), "line": n}
|
||||
DEFINITIONS[raw_fields["id"]["value"]] = {"path": rel, "fields": raw_fields, "duplicate_assignments": duplicates,
|
||||
"unknown_export_fields": [key for key in raw_fields if key not in EXPORTED_KEYS]}
|
||||
pin(rel)
|
||||
|
||||
catalog_text = (ROOT / CATALOG).read_text()
|
||||
NORMAL_PATHS = set()
|
||||
for key in ["FACILITY_PATHS", "INFRASTRUCTURE_FACILITY_PATHS", "RESCUE_FACILITY_PATHS", "ORIGINAL_FACILITY_PATHS"]:
|
||||
chunk = re.search(r"const " + key + r": Array\[String\] = \[(.*?)\]", catalog_text, re.S)[1]
|
||||
NORMAL_PATHS.update(re.findall(r'"res://([^"]+)"', chunk))
|
||||
|
||||
# Annex rows explicitly describe prior implementation, not original/individual approval.
|
||||
ANNEX_ROWS = {}
|
||||
ANNEX_CARDS = {}
|
||||
annex_id = None
|
||||
for n, line in enumerate((ROOT / ANNEX).read_text().splitlines(), 1):
|
||||
m = re.match(r"### .*(([a-z_]+))", line)
|
||||
if m:
|
||||
annex_id = m[1]
|
||||
ANNEX_CARDS[annex_id] = n
|
||||
ANNEX_ROWS[annex_id] = {}
|
||||
cells = [c.strip() for c in line.strip().strip("|").split("|")]
|
||||
if annex_id and line.startswith("|") and len(cells) == 4 and re.fullmatch(r"`\w+`", cells[1]):
|
||||
ANNEX_ROWS[annex_id][cells[1].strip("`")] = {"reported_value": cells[2], "reported_origin": cells[3],
|
||||
"source": anchor(ANNEX, [n], "retained_legacy_implementation_reference")}
|
||||
|
||||
|
||||
def current(fid, field):
|
||||
if fid is None or fid not in DEFINITIONS:
|
||||
return {"value": None, "raw": None, "origin": "no_definition", "source": None}
|
||||
d = DEFINITIONS[fid]
|
||||
if field in d["fields"]:
|
||||
f = d["fields"][field]
|
||||
return {"value": f["value"], "raw": f["raw"], "origin": "explicit_tres",
|
||||
"source": anchor(d["path"], [f["line"]], "current_implementation_only")}
|
||||
if field in DEFAULTS:
|
||||
f = DEFAULTS[field]
|
||||
return {"value": f["value"], "raw": f["raw"], "origin": "inherited_class_default",
|
||||
"source": anchor(DEF, [f["line"]], "current_implementation_only")}
|
||||
return {"value": None, "raw": None, "origin": "not_a_definition_field", "source": None}
|
||||
|
||||
|
||||
decision_data = json.loads((ROOT / DECISIONS).read_text())
|
||||
APPROVALS = {d["id"]: {**d, "source": anchor(DECISIONS, list(range(findline(DECISIONS, '"id": "' + d["id"] + '"'), findline(DECISIONS, '"id": "' + d["id"] + '"') + 3)),
|
||||
"confirmed_user_decision")} for d in decision_data["confirmed"]}
|
||||
APPROVED_VALUES = {
|
||||
"chapel": {"footprint": [4, 3], "layer": 0, "construction_cost": {"gold": 1200, "materials": 12},
|
||||
"build_minutes": 180, "customer_capacity": 8, "staff_min": 1, "staff_max": 2,
|
||||
"required_roles": ["cleric"], "base_service_minutes": 60, "base_price": 20,
|
||||
"faith_per_service": 1, "maintenance_gold": 6},
|
||||
"cleaning_room": {"footprint": [2, 1], "layer": 1, "construction_cost": {"gold": 350, "materials": 4},
|
||||
"build_minutes": 60, "customer_capacity": 0, "staff_min": 1, "staff_max": 2,
|
||||
"required_roles": ["cleaner"], "base_price": 0, "maintenance_gold": 2},
|
||||
}
|
||||
F_FIELDS = {
|
||||
"F01": ["construction_cost", "build_minutes"],
|
||||
"F02": ["maintenance_gold", "maintenance_mana"],
|
||||
"F03": ["base_price"],
|
||||
"F04": ["footprint", "layer", "zone"],
|
||||
"F05": ["required_roles", "primary_skill", "staff_min", "service_types", "default_service_type", "service_profiles", "base_service_minutes"],
|
||||
"F06": [], "F07": ["customer_capacity", "capacity", "queue_capacity"],
|
||||
"F08": ["staff_max"], "F09": [], "F10": ["service_attribute"],
|
||||
}
|
||||
FIELD_DECISIONS = {"customer_capacity": "D02", "staff_max": "D02", "service_attribute": "D01",
|
||||
"maintenance_mana": "D04"}
|
||||
GROUP_LINES = {"10.1": 199, "10.2": 201, "10.3": 203, "10.4": 205, "10.5": 207, "10.6": 209, "10.7": 211}
|
||||
|
||||
|
||||
def parameter(fid, field, original=None, original_source=None):
|
||||
c = current(fid, field)
|
||||
approved = APPROVED_VALUES.get(fid, {}).get(field)
|
||||
approval = None
|
||||
if field in APPROVED_VALUES.get(fid, {}):
|
||||
aid = "A04" if fid == "chapel" else "A05"
|
||||
approval = {"value": approved, "decision_id": aid, "certainty": "approved_initial_trial_value", "source": APPROVALS[aid]["source"]}
|
||||
state = "approved_initial_trial_value"
|
||||
comparison = "match" if c["value"] == approved else "mismatch_or_missing"
|
||||
elif original is not None:
|
||||
state = "original_explicit_value"
|
||||
comparison = "match" if c["value"] == original else "mismatch_or_missing"
|
||||
elif field == "maintenance_gold":
|
||||
gold = current(fid, "construction_cost")["value"]
|
||||
calculation = max((gold["gold"] + 199) // 200, 1) if isinstance(gold, dict) else None
|
||||
approval = {"formula": "0.5% of construction Gold per day; preserved integer trial rule: ceil, minimum 1G",
|
||||
"conditional_value_using_current_build_cost": calculation, "decision_id": "A02",
|
||||
"construction_cost_is_individually_approved": fid in APPROVED_VALUES,
|
||||
"source": APPROVALS["A02"]["source"],
|
||||
"integer_rule_source": anchor(UPKEEP, [58], "approved_trial_implementation_record"),
|
||||
"certainty": "approved_formula_current_basis_may_be_legacy"}
|
||||
state = "approved_formula_with_legacy_basis" if calculation is not None else "approved_formula_basis_missing"
|
||||
comparison = "conditional_formula_match" if calculation is not None and c["value"] == calculation else "conditional_formula_mismatch_or_missing"
|
||||
elif c["origin"] == "no_definition":
|
||||
state = "not_set_no_definition"
|
||||
comparison = "no_authoritative_value"
|
||||
elif field == "customer_capacity" and c["value"] == -1 or field == "service_attribute" and c["value"] == "":
|
||||
state = "explicitly_unset_required_contract"
|
||||
comparison = "no_authoritative_value"
|
||||
else:
|
||||
state = "current_value_without_individual_approval"
|
||||
comparison = "no_authoritative_value"
|
||||
return {"field": field, "value_status": state,
|
||||
"original": {"value": original, "source": original_source} if original is not None else None,
|
||||
"individual_or_formula_approval": approval, "current": c,
|
||||
"annex_A_legacy": ANNEX_ROWS.get(fid, {}).get(field),
|
||||
"comparison": comparison, "decision_id": FIELD_DECISIONS.get(field),
|
||||
"acceptance_state": "unverified_static_audit_only"}
|
||||
|
||||
|
||||
def source_function(name):
|
||||
facts = {
|
||||
"一般客室": ("睡眠欲解消・宿泊", [79, 167]),
|
||||
"上級客室": ("上級宿泊", [167]),
|
||||
"VIPスイート": ("VIP宿泊・覗き対応・要高景観", [167, 199]),
|
||||
"ドミトリー": ("地下の宿泊施設", [199]),
|
||||
"大衆食堂": ("食欲解消・厨房とセット運用", [79, 168]),
|
||||
"レストラン": ("飲食・厨房とセット運用", [168]),
|
||||
"酒場": ("飲食・厨房とセット運用・騒音発生", [168, 201]),
|
||||
"屋台": ("飲食施設", [200, 201]),
|
||||
"大浴場": ("衛生・覗き対応", [169, 203]),
|
||||
"カジノ": ("娯楽欲解消・イカサマ率設定をLvUPで解放", [79, 169, 179]),
|
||||
"闘技場": ("娯楽施設", [169, 203]),
|
||||
"礼拝堂": ("礼拝・寄付・完了時信仰加算", [169, 203]),
|
||||
"風俗個室": ("地下サービス・覗き対応・LvUPでハードプレイ設定", [170, 179, 205]),
|
||||
"高級ソープ": ("地下アングラ施設", [170, 205]),
|
||||
"SM拷問室": ("地下アングラ施設", [170, 205]),
|
||||
"闇市場": ("地下アングラ施設", [170, 205]),
|
||||
"厨房": ("表層飲食店の裏で供給", [168, 173]),
|
||||
"倉庫": ("物流施設", [173]),
|
||||
"待機室": ("非番従業員の体力回復・待機", [109, 172]),
|
||||
"従業員寮": ("従業員管理・夜間イベント対応", [172]),
|
||||
"警備員詰所": ("従業員管理・警備員の拠点", [107, 172]),
|
||||
"清掃員室": ("清掃員の裏層拠点", [106, 173]),
|
||||
"医務室": ("物流・救護に関わる裏層施設", [110, 173]),
|
||||
"覗き部屋": ("VIP/浴場/風俗の裏限定・裏通路寸断・記録係配置で脅迫ネタ収集", [52, 53, 174]),
|
||||
"地下牢": ("地下特殊施設", [174, 209]),
|
||||
"マナ炉": ("地下特殊施設・奴隷消費", [174, 209]),
|
||||
"蔓の昇降機": ("客用・定員あり・垂直移動", [86, 176]),
|
||||
"業務用EV": ("裏層・大容量・高速の垂直移動", [86, 176]),
|
||||
"ワープ": ("瞬間移動・マナコスト大", [86, 176]),
|
||||
"魔法砲台": ("外壁の防衛設備", [210, 211]),
|
||||
}
|
||||
text, lines = facts[name]
|
||||
return {"condition": text, "source": anchor(BASE, lines, "specified_original")}
|
||||
|
||||
|
||||
def relation(name):
|
||||
if name in ["大衆食堂", "レストラン", "酒場"]:
|
||||
return {"required": True, "condition": "裏層の厨房とセット運用。関連先は配置の存在だけでなく実稼働を評価する。",
|
||||
"target_kind": "kitchen", "source": anchor(BASE, [168, 173], "specified_original")}
|
||||
if name == "厨房":
|
||||
return {"required": True, "condition": "表層飲食店の裏に配置。供給先との対応を保持。", "target_kind": "front_food_facility",
|
||||
"source": anchor(BASE, [173], "specified_original")}
|
||||
if name == "覗き部屋":
|
||||
return {"required": True, "condition": "VIP/浴場/風俗の裏限定。複数関連条件のAND/ORではなく対象種別の許可集合。",
|
||||
"target_kind": ["VIP", "bath", "adult_room"], "source": anchor(BASE, [52, 174], "specified_original")}
|
||||
return {"required": None, "condition": "個別の関連先は原本/A01〜A05に指定なし。空文字を正式な不要判定としない。",
|
||||
"target_kind": None, "source": None}
|
||||
|
||||
|
||||
mapping = json.loads((ROOT / "docs/plans/original-completion-2026-09-29/execution/original_facility_mapping.json").read_text())
|
||||
records = []
|
||||
for ix, mapped in enumerate(mapping, 1):
|
||||
fid, name = mapped["definition_id"], mapped["name"]
|
||||
source = anchor(BASE, [GROUP_LINES[mapped["section"]]], "specified_original")
|
||||
size = [int(v) for v in mapped["original_size"].split("x")] if "x" in mapped["original_size"] else None
|
||||
layer = 1 if mapped["original_layer_zone"].startswith("BACK") else 0 if mapped["original_layer_zone"].startswith("FRONT") else None
|
||||
zone = 3 if "地下" in mapped["original_layer_zone"] else None
|
||||
original_values = {"footprint": size, "layer": layer, "zone": zone}
|
||||
parms = {field: parameter(fid, field, original_values.get(field), source if field in original_values else None)
|
||||
for fields in F_FIELDS.values() for field in fields}
|
||||
parms.update({field: parameter(fid, field) for field in ["max_level", "upgrade_costs", "unlock_rank", "research_requirements", "policy_schema", "faith_per_service"]})
|
||||
# Lv1 data is recorded as current base data, never assumed approved for every level.
|
||||
levels = []
|
||||
for lv in [1, 2, 3]:
|
||||
levels.append({"level": lv,
|
||||
"authoritative_level_specific_price": None,
|
||||
"authoritative_level_specific_customer_capacity": None,
|
||||
"authoritative_level_specific_staff_min": None,
|
||||
"authoritative_level_specific_staff_max": None,
|
||||
"approved_base_trial_values_initial_level_candidate": APPROVED_VALUES.get(fid) if lv == 1 else None,
|
||||
"base_approval_has_explicit_level_scope": False,
|
||||
"base_approval_use_note": "A04/A05初期試験値をLv1初期値候補に結び付ける。承認本文にLv別変更はない。Lv2/3への複写や不変性の承認は主張しない。" if fid in APPROVED_VALUES else None,
|
||||
"current_level_specific_definition": None,
|
||||
"current_base_parameters_reference": list(parms) if lv == 1 and fid else [],
|
||||
"upgrade_cost_from_previous_current_legacy": current(fid, "upgrade_costs")["value"][lv-2] if lv > 1 and fid and len(current(fid, "upgrade_costs")["value"]) >= lv-1 else None,
|
||||
"unlock_condition": None, "policy_unlock_assignment": None,
|
||||
"status": "no_approved_level_ladder", "decision_ids": ["D02", "D04"],
|
||||
"source": anchor(PLAN, [207], "user_requested_implementation_plan")})
|
||||
r = {"id": f"FVC.original.{ix:02d}", "name": name, "source_section": mapped["section"],
|
||||
"definition_id": fid, "definition_path": DEFINITIONS[fid]["path"] if fid else None,
|
||||
"definition_exists": bool(fid), "normal_registered_static": bool(fid and DEFINITIONS[fid]["path"] in NORMAL_PATHS),
|
||||
"registration_state": "normal_registered" if fid and DEFINITIONS[fid]["path"] in NORMAL_PATHS else "definition_only" if fid else "no_definition",
|
||||
"original_value_source": source,
|
||||
"source_function": source_function(name),
|
||||
"major_items": {group: {"source": anchor(ADDITION, [984 + int(group[1:]) - 1], "retained_user_addition_record"),
|
||||
"parameter_fields": fields, "acceptance_state": "unverified"} for group, fields in F_FIELDS.items()},
|
||||
"parameters": parms,
|
||||
"placement_conditions": {"height": "高層" if "高層" in mapped["original_layer_zone"] else None,
|
||||
"height_threshold": None, "high_beauty_required": name == "VIPスイート", "beauty_threshold": None,
|
||||
"outer_wall_required": name == "魔法砲台", "source": source,
|
||||
"access_and_support": anchor(BASE, [164], "specified_original")},
|
||||
"related_facility": {"source_required_relation": relation(name), "current_definition_field": "not_present",
|
||||
"current_instance_default": {"id": "", "operating": True,
|
||||
"source": anchor(INSTANCE, [49, 50, 84, 85], "current_implementation_only")},
|
||||
"runtime_creation_sets_related_id": False,
|
||||
"runtime_creation_source": anchor(RUNTIME, [findline(RUNTIME, 'func _register_operational_facility('), findline(RUNTIME, '&"customer_capacity": definition.customer_capacity')], "current_implementation_only"),
|
||||
"single_dependency_contract_reference": "common_contracts.related_facility"},
|
||||
"operation_state_contract_reference": "common_contracts.operation_state",
|
||||
"surcharge_contract_reference": "common_contracts.need_match_surcharge",
|
||||
"need_match_inputs": {
|
||||
"attribute": parms["service_attribute"],
|
||||
"deficit_threshold": {"value": None, "status": "required_before_dependent_charging", "decision_id": "D01"},
|
||||
"surcharge_amount_or_rate": {"value": None, "status": "required_before_dependent_charging", "decision_id": "D01"},
|
||||
"source": anchor(DECISIONS, [findline(DECISIONS, '"id": "D01"')], "pending_required_user_value"),
|
||||
"deficit_threshold_not_inferred_from_need_effects": True},
|
||||
"generic_capacity_contract": {
|
||||
"is_customer_capacity": False,
|
||||
"current": parms["capacity"]["current"],
|
||||
"profile_capacity_units": {key: value.get("capacity_units") for key, value in (current(fid, "service_profiles")["value"] or {}).items()},
|
||||
"queue_capacity_is_separate": True,
|
||||
"current_runtime_use": "service slots (definition.capacity clamped to at least 1), not unique active guest count",
|
||||
"source": anchor(INSTANCE, [247, 248, 253, 261, 270], "current_implementation_only")},
|
||||
"level_contracts": levels,
|
||||
"unresolved_value_fields": [field for field, p in parms.items() if p["value_status"] in ["explicitly_unset_required_contract", "not_set_no_definition", "approved_formula_basis_missing"]],
|
||||
"unapproved_current_fields": [field for field, p in parms.items() if p["value_status"] == "current_value_without_individual_approval"],
|
||||
"definition_anomalies": {key: DEFINITIONS[fid][key] for key in ["duplicate_assignments", "unknown_export_fields"]} if fid else None,
|
||||
"runtime_acceptance_state": "unverified_no_engine_run_in_this_audit"}
|
||||
records.append(r)
|
||||
|
||||
supplemental = []
|
||||
matched = {r["definition_id"] for r in records if r["definition_id"]}
|
||||
for fid, definition in DEFINITIONS.items():
|
||||
if fid in matched:
|
||||
continue
|
||||
supplemental.append({"id": f"FVC.supplemental.{fid}", "definition_id": fid,
|
||||
"display_name": definition["fields"]["display_name"]["value"], "definition_path": definition["path"],
|
||||
"normal_registered_static": definition["path"] in NORMAL_PATHS,
|
||||
"chapter_10_mapping": None,
|
||||
"major_item_applicability": "not_finalized_for_facilities_outside_chapter_10",
|
||||
"parameters": {field: parameter(fid, field) for fields in F_FIELDS.values() for field in fields},
|
||||
"annex_A_card": anchor(ANNEX, [ANNEX_CARDS[fid]], "retained_legacy_implementation_reference") if fid in ANNEX_CARDS else None})
|
||||
|
||||
missing_blueprints = []
|
||||
for r in records:
|
||||
if r["definition_exists"]:
|
||||
continue
|
||||
missing_blueprints.append({"contract_id": r["id"], "name": r["name"], "production_id": None,
|
||||
"safe_original_values": {key: r["parameters"][key]["original"] for key in ["footprint", "layer", "zone"] if r["parameters"][key]["original"]},
|
||||
"safe_original_conditions": [r["source_function"], r["placement_conditions"], r["related_facility"]["source_required_relation"]],
|
||||
"unknowns": ["production_id", "construction_cost", "build_minutes", "maintenance_mana", "base_price", "customer_capacity",
|
||||
"staff_min", "staff_max", "service_attribute", "Lv1_to_Lv3_values", "unlock_conditions"],
|
||||
"register_and_allow_build_now": False,
|
||||
"reason": "原本で確定する寸法・層・地域・機能だけを設計用blueprintへ抽出可能。FacilityDefinition既定値で空欄を埋めて営業可能とする承認はない。"})
|
||||
|
||||
common = {
|
||||
"operation_state": {"authoritative_states": ["INACTIVE", "OPERATING", "SUSPENDED"],
|
||||
"persistent_suspension": "破壊/摘発後の休止は関連施設復旧だけでは解除しない。",
|
||||
"source": anchor(ADDITION, [989], "retained_user_addition_record"),
|
||||
"current_code": anchor(INSTANCE, [8, 186, 198, 219, 225, 229], "current_implementation_only"),
|
||||
"mana_zero": {"value": 0, "behavior": "全館機能停止", "source": anchor(BASE, [183], "specified_original")},
|
||||
"resumption_conditions": {"value": None, "status": "explicitly_unset_optional_extension", "decision_id": "D03",
|
||||
"is_mandatory_missing": False}, "acceptance_state": "unverified"},
|
||||
"related_facility": {"cardinality": "single", "flag": "actual_operation_state", "cycles": "reject",
|
||||
"source": anchor(ADDITION, [992], "retained_user_addition_record"),
|
||||
"current_code": anchor(SERVICE, [141, 146, 151, 155, 359, 365], "current_implementation_only"),
|
||||
"multi_relation_AND_OR": {"value": None, "status": "explicitly_unset_optional_extension", "decision_id": "D03", "is_mandatory_missing": False},
|
||||
"individual_assignment": "本文明示の厨房連携や覗き対象を汎用単一関連APIが自動割当済みとは扱わない。", "acceptance_state": "unverified"},
|
||||
"need_match_surcharge": {"attribute_vocabulary": ["appetite", "relaxation", "sexual", "gambling"],
|
||||
"source": anchor(ADDITION, [986, 993, 995], "retained_user_addition_record"),
|
||||
"deficit_threshold": {"value": None, "status": "required_before_dependent_charging", "decision_id": "D01"},
|
||||
"surcharge_amount_or_rate": {"value": None, "status": "required_before_dependent_charging", "decision_id": "D01"},
|
||||
"per_facility_attribute": {"value": None, "status": "required_before_dependent_charging", "decision_id": "D01"},
|
||||
"multiple_attributes_and_duplicate_surcharge": {"value": None, "status": "explicitly_unset_optional_extension", "is_mandatory_missing": False},
|
||||
"nonmatching_utilization": "allowed", "quote_api_supplied_amount_sentinel": -1,
|
||||
"current_quote_code": anchor(PRICING, [findline(PRICING, 'static func quote_service(')], "current_implementation_only"),
|
||||
"current_normal_pricing": "pending_service_readmodel returns unresolved inputs; real surcharge not connected",
|
||||
"current_runtime": anchor(RUNTIME, [findline(RUNTIME, 'func get_facility_service_pricing_readmodel(')], "current_implementation_only"),
|
||||
"guest_need_effects_are_attribute_assignment": False,
|
||||
"guest_need_0_100_is_deficit_threshold": False,
|
||||
"Anri_multiplier": {"percent": 20, "scope": "assigned floor fees; guest payment equals facility revenue", "source": APPROVALS["A03"]["source"]},
|
||||
"composition_order_with_Anri": {"value": None, "status": "requires_contract_before_surcharge_connection", "source": anchor(PLAN, [206], "user_requested_implementation_plan")},
|
||||
"acceptance_state": "unverified"},
|
||||
"generic_capacity": {"is_customer_capacity": False, "waiting_is_customer_count": False,
|
||||
"definition_default": 0, "runtime_minimum_slots": 1,
|
||||
"source": anchor(DEF, [61, 63, 64], "current_implementation_only"),
|
||||
"runtime_source": anchor(RUNTIME, [findline(RUNTIME, '&"capacity": maxi(definition.capacity, 1)')], "current_implementation_only"),
|
||||
"customer_enforcement": anchor(INSTANCE, [247, 248, 253, 261, 270], "current_implementation_only"),
|
||||
"examples": {"kitchen": "generic20 / profile batch4", "warehouse": "generic100; inventory/handling meaning", "staff_dormitory": "generic12 staff support / current customer0", "cleaning_room": "generic0 / approved customer0 / runtime slots clamped1"}},
|
||||
"levels": {"planned_levels": [1, 2, 3], "original_max_level_number": None,
|
||||
"original": anchor(BASE, [165, 179], "specified_original"), "plan": anchor(PLAN, [207], "user_requested_implementation_plan"),
|
||||
"current_max_level_default": 3, "default_is_approved_ladder": False,
|
||||
"current_level_specific_price_staff_customer_data": "none in FacilityDefinition",
|
||||
"unapproved_upgrade_costs": "retained per-facility raw data, never treated as approved prices/capacities",
|
||||
"unlock_rank_default_1_is_approved": False,
|
||||
"normal_build_code_checks_unlock_rank_or_research": False,
|
||||
"normal_build_source": anchor(RUNTIME, [findline(RUNTIME, 'func _execute_build_facility('), findline(RUNTIME, 'definition.build_minutes,')], "current_implementation_only"),
|
||||
"acceptance_state": "unverified"},
|
||||
"upkeep": {"A01": APPROVALS["A01"], "A02": APPROVALS["A02"],
|
||||
"individual_overrides": ["chapel:6G/day", "cleaning_room:2G/day"],
|
||||
"formula_is_approval_of_build_cost": False,
|
||||
"annex_zero_maintenance_values_are_current": False,
|
||||
"acceptance_state": "unverified_in_this_value_audit"},
|
||||
"cleaning_A05": {"cleanliness_range": [0, 100], "initial": 100, "reserve_at_or_below": 75,
|
||||
"onsite_minutes": 8, "restore": 25, "use_only_defined_dirt": True, "source": APPROVALS["A05"]["source"],
|
||||
"not_service_attribute_or_matching_deficit_threshold": True},
|
||||
}
|
||||
|
||||
count_status = Counter(p["value_status"] for r in records for p in r["parameters"].values())
|
||||
summary = {"original_facilities": len(records), "defined": sum(r["definition_exists"] for r in records),
|
||||
"normal_registered_static": sum(r["normal_registered_static"] for r in records),
|
||||
"definition_only": [r["definition_id"] for r in records if r["registration_state"] == "definition_only"],
|
||||
"undefined": len(missing_blueprints), "all_current_definition_files": len(DEFINITIONS), "supplemental_defined_outside_chapter10": len(supplemental),
|
||||
"original_facility_parameter_records": sum(len(r["parameters"]) for r in records),
|
||||
"parameter_value_status_counts": dict(sorted(count_status.items())),
|
||||
"customer_capacity": dict(Counter(r["parameters"]["customer_capacity"]["value_status"] for r in records)),
|
||||
"staff_max": dict(Counter(r["parameters"]["staff_max"]["value_status"] for r in records)),
|
||||
"service_attribute": dict(Counter(r["parameters"]["service_attribute"]["value_status"] for r in records)),
|
||||
"all_current_definition_service_attributes_empty": all(current(fid, "service_attribute")["value"] == "" for fid in DEFINITIONS),
|
||||
"all_original_defined_dimensions_match": all(r["parameters"]["footprint"]["comparison"] == "match" for r in records if r["definition_exists"]),
|
||||
"all_original_defined_layers_match": all(r["parameters"]["layer"]["comparison"] == "match" for r in records if r["definition_exists"]),
|
||||
"approved_runtime_acceptances_counted": 0,
|
||||
"is_final_all_facility_catalog": False,
|
||||
"optional_unset_D03_counted_as_mandatory_missing": 0,
|
||||
"counts_are_field_observations_not_atomic_completion_denominator": True}
|
||||
|
||||
recommendations = [
|
||||
{"id": "P09-safe-01", "action": "A04/A05の承認済み人数・最少/最大職員・料金の契約を通常UIと保存へつなぐ境界検証を先行。current値を変更する前にsource/value確定と表示を区別。",
|
||||
"new_numeric_decision_needed": False, "dependencies": ["existing approved facilities"], "acceptance": "customer_count excludes queued guests; over-limit refusal preserves state; staff_min and staff_max remain distinct; read-only pricing inspection"},
|
||||
{"id": "P09-safe-02", "action": "customer_capacity=-1とservice_attribute空文字をUIで未設定と表示し、現在明示2/0も承認由来の別表示にする。generic容量・人数・待機人数を分ける。",
|
||||
"new_numeric_decision_needed": False, "dependencies": ["existing readmodels"], "acceptance": "unset is never presented as 0/unlimited/approved; fields and save values unchanged"},
|
||||
{"id": "P09-safe-03", "action": "13未定義施設のsource-only blueprint/設計カタログを追加。確定寸法/層/地下/高層/外壁/厨房/覗き制約だけを保持し、未確定の値はnullとする。通常建設へ登録しない。",
|
||||
"new_numeric_decision_needed": False, "dependencies": [], "acceptance": "no fabricated costs/capacities/IDs; cannon footprint remains null; no FacilityDefinition default leakage into playable construction"},
|
||||
{"id": "P09-safe-04", "action": "食堂・酒場の厨房セット運用を現単一関連APIと実稼働判定へ接続する契約/候補検証を用意。厨房停止・破壊・未配置・保存復元時の動作を検証。",
|
||||
"new_numeric_decision_needed": False, "dependencies": ["normal source-confirmed kitchen relationship", "placement selection contract"],
|
||||
"acceptance": "cannot operate solely because a kitchen exists; do not invent multi dependency or automatic suspension recovery"},
|
||||
{"id": "P09-safe-05", "action": "Lv1–3の明示tableと未設定guardを作り、Lv2/3料金や人数をbase値の複写で確定しない。既存upgrade_costsとrank1は旧実装欄に保全。",
|
||||
"new_numeric_decision_needed": False, "dependencies": ["save schema/version policy before persisted model changes"],
|
||||
"acceptance": "unknown level action rejected before charging or mutation; existing saves preserved; known A04/A05 base values reused without claiming level scope"},
|
||||
{"id": "P09-safe-06", "action": "欲求加算はD01確定まで請求を接続せず、nonmatch利用可・未解決見積拒否・財布/収入一致の計算契約と比較例を準備する。",
|
||||
"new_numeric_decision_needed": False, "dependencies": ["D01 only before actual surcharge"], "acceptance": "no threshold copied from cleaning/guest needs; no attribute inferred from need_effects; A03 composition explicitly specified before charge"},
|
||||
]
|
||||
|
||||
for p in [BASE, ANNEX, DECISIONS, ADDITION, PLAN, DEF, CATALOG, INSTANCE, SERVICE, PRICING, RUNTIME, UPKEEP]:
|
||||
pin(p)
|
||||
document = {"schema_version": 1, "audit_id": "P09-facility-value-static-2026-10-02-v1",
|
||||
"source_head": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip(),
|
||||
"source_is_working_tree": True, "method": "read-only text parsing and semantic source audit; no engine, no production mutation",
|
||||
"authority_order": ["specified original", "confirmed user A01-A05 and facility addition", "annex A legacy implementation reference", "current implementation"],
|
||||
"status_definitions": {"original_explicit_value": "原本の明示値。現値一致は静的比較であり受入ではない。",
|
||||
"approved_initial_trial_value": "個別承認の初期試験値。最終バランスや全Lv値の承認を意味しない。",
|
||||
"approved_formula_with_legacy_basis": "率・整数則は承認済み。建築費の現値は個別承認値とは限らない。",
|
||||
"current_value_without_individual_approval": "旧/現在コードに値あり。原本/個別承認の根拠は未確認。勝手に採用値へ格上げしない。",
|
||||
"explicitly_unset_required_contract": "当該機能の本番接続前に必要な未設定値。",
|
||||
"not_set_no_definition": "独立した定義なし。原本の確定項目は別欄へ抽出。",
|
||||
"explicitly_unset_optional_extension": "未採用/未設定維持が許される拡張。必須欠損に数えない。"},
|
||||
"summary": summary, "source_pins": PINS, "approvals": APPROVALS,
|
||||
"common_contracts": common, "original_facilities": records, "missing_definition_blueprints": missing_blueprints,
|
||||
"supplemental_current_definitions": supplemental,
|
||||
"scope_pending": {"named_original_outside_chapter10": ["受付/精算所", "金庫室", "購買部", "闇銀行", "通路", "階段", "従業員ドア", "リネンシュート", "隠し扉"],
|
||||
"source": anchor(BASE, [49, 50, 86, 176, 177, 190, 191, 192, 193], "specified_original"),
|
||||
"contract": "F各施設とLv素材の適用範囲はatomic open expansion registerと同じ未確定状態。30施設/37定義の件数を最終全施設数にしない。"},
|
||||
"safe_followup_candidates": recommendations,
|
||||
"acceptance": {"completed": False, "runtime_runs_in_this_audit": 0, "source_comparison_is_runtime_acceptance": False}}
|
||||
|
||||
(OUT / "facility_value_contracts.json").write_text(json.dumps(document, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
def brief(v):
|
||||
return "未定義" if v is None else "未設定" if v == -1 else str(v)
|
||||
|
||||
table = ["| 原本施設 | 定義/通常登録 | 原本サイズ・層 | min/max現値 | 人数専用/汎用capacity現値 | 料金現値 | 根拠 |",
|
||||
"|---|---|---|---|---|---|---|"]
|
||||
for r in records:
|
||||
p = r["parameters"]
|
||||
root_value = mapped_value = "×".join(map(str, p["footprint"]["original"]["value"])) if p["footprint"]["original"] else "未指定"
|
||||
layer = p["layer"]["original"]["value"] if p["layer"]["original"] else None
|
||||
place = root_value + (" 裏" if layer == 1 else " 表" if layer == 0 else " 外壁")
|
||||
approved_label = "A04初期値" if r["definition_id"] == "chapel" else "A05初期値" if r["definition_id"] == "cleaning_room" else "数値は旧/現実装・個別根拠未確認" if r["definition_exists"] else "原本の寸法等のみ確定"
|
||||
table.append(f"| {r['name']} | {r['registration_state']} | {place} | {brief(p['staff_min']['current']['value'])}/{brief(p['staff_max']['current']['value'])} | {brief(p['customer_capacity']['current']['value'])}/{brief(p['capacity']['current']['value'])} | {brief(p['base_price']['current']['value'])} | {approved_label} |")
|
||||
|
||||
blueprint_table = ["| 未定義施設 | 確定できる項目 | 原本行 |",
|
||||
"|---|---|---|"]
|
||||
for bp in missing_blueprints:
|
||||
r = next(r for r in records if r["id"] == bp["contract_id"])
|
||||
dimension = bp["safe_original_values"].get("footprint", {}).get("value")
|
||||
dims = "×".join(map(str, dimension)) if dimension else "寸法未指定"
|
||||
blueprint_table.append(f"| {bp['name']} | {dims}; {r['source_function']['condition']}; {mapping[int(r['id'].split('.')[-1])-1]['original_layer_zone']} | {GROUP_LINES[r['source_section']]} |")
|
||||
|
||||
readme = f"""# P09 施設必須値の再監査(2026-10-02)
|
||||
|
||||
原本30施設について、定義あり{summary['defined']}、通常登録{summary['normal_registered_static']}、定義のみ1(black_market)、未定義{summary['undefined']}を静的に確認した。現在の全`.tres`は{summary['all_current_definition_files']}件で、そのうち原本第10章の対応外は{len(supplemental)}件。これは最終の全施設数でも、実装・受入完了率でもない。今回のエンジン実行は0件、本体変更は0件。
|
||||
|
||||
機械可読台帳は `facility_value_contracts.json`。各値は原本文、A01〜A05、別冊A作成時の値と由来、現`.tres`明示値またはクラス既定値を別欄に保存し、出典行とSHAを保持する。生成は `python3 docs/plans/original-completion-2026-10-02/build_facility_value_contracts.py`。検証は `validate_facility_value_contracts.py`。承認差異の静的一致からRuntime/UI/保存のPASSを作らない。
|
||||
|
||||
原本を最上位とし、A01〜A05と会話追加10項目を重ねる。別冊Aは旧実装の完全一覧であり、数値承認書ではない。冒頭3・10・14〜19・76・106行が既定値/旧状態/仮値/異なる処理系を区別している。参照先 `docs/FACILITY_UNIT_PARAMETERS.md` は現ローカルに存在しないため、その内容を推定補完しなかった。既存の保存版TRACEABILITY 978〜995行を追加指定の記録に用いた。
|
||||
|
||||
## 30施設の現値と根拠
|
||||
|
||||
表のmin/max、人数専用、capacity、料金は現実装の観測値である。礼拝堂・清掃員室以外の数値を承認済みに扱わない。未設定は人数専用-1、未定義は独立定義なしを表す。
|
||||
|
||||
{chr(10).join(table)}
|
||||
|
||||
原本で寸法のある現定義17施設は寸法と層が静的一致する。一般的な「表」は地上専用を意味しない。地下や高層の指定は個別に保存し、魔法砲台の幅・高さ・層は捏造しない。premium_room、staff_dormitory、cleaning_room、chapelの通常登録は今回変更ではなく現基準の既存成果である。
|
||||
|
||||
## 10項目と必須値の区別
|
||||
|
||||
1. 配置コスト: 原本は資金/資材と建設ルールを指定するが、施設別コストと工期の数値はA04/A05以外に個別承認を確認できない。現値は保存しても正式値へ格上げしない。
|
||||
2. 運用コスト: A01の保有按分、休業/休止中計上、00:00精算、不足繰越とA02の0.5%初期試験率は確定。既存ceil/最低1Gを出典付きで保持する。礼拝堂6G/日、清掃員室2G/日は個別上書き。率の承認は建築費自体の個別承認を意味しない。闇市場の現maintenance_gold=既定0は、現1600Gに率を当てた条件付き8Gと異なる。未登録定義を登録する前にこの差異を解消する契約が必要。施設別マナ量は原本では未指定。原本183行のマナ0で全停止を別に保持する。
|
||||
3. 利用料金: 礼拝堂20G寄付、清掃員室利用料なしを承認初期値として保持。他施設の単価や5段階60/80/100/150/250%は旧実装値。D01の不足基準、加算額/率、属性割当は未設定。元の四欲求0〜100や清掃予約75を不足基準に流用しない。A03の20%補正は承認済みだが、新しい一致加算との順序/丸めはP09-02接続前に固定する。
|
||||
4. サイズ: 29施設の原本寸法と30対象の層/地下/高層/外壁指定を追跡。魔法砲台寸法は未指定のまま。
|
||||
5. 役割: 職種とサービス機能の原文、required_rolesとstaff_minを分離。A04聖職者min1/max2、A05清掃員min1/max2は確定。原本の記録係配置などの役割と人数の数値は同一視しない。
|
||||
6. 稼働状態: INACTIVE/OPERATING/SUSPENDEDと破壊/摘発後の持続休止は会話追加の確定契約。復帰条件D03はユーザー指定の未設定拡張であり、必須値欠損に数えない。
|
||||
7. 顧客同時利用最大人数: 承認値は礼拝堂8、清掃員室0のみ。上級客室2・従業員寮0は現明示値だが根拠未確認。残る定義13件は-1継承、未定義13件には値なし。待機者は利用人数とは別。厨房capacity20/profile生産4、倉庫capacity100、従業員寮capacity12などをcustomer_capacityへ転記しない。
|
||||
8. 配置可能従業員最大数: staff_maxはstaff_minと別。2施設の個別承認、15施設の旧/現値、13施設の未定義を保持する。Lv別変更もD02の未確定に残す。
|
||||
9. 関連施設/稼働フラグ: 原本168・173行の食堂/レストラン/酒場と厨房セット、52・174行の覗き対象などを個別追跡。現汎用単一依存APIは循環拒否と実稼働評価を持つが、建設からのcreate_instanceにはrelated_facility_idを渡していない。原本の対応を自動設定済みと認定しない。D03の複数依存AND/ORは未設定維持が許され、必須欠損へ数えない。
|
||||
10. 属性: 許可型appetite/relaxation/sexual/gamblingは確定、施設への割当は全37定義で空文字。Runtimeのhunger/sleep/hygiene/medical/leisure/intimacy効果と提供属性は別の名前空間・用途。効果タグから勝手に属性を選ばない。不一致の利用は許可する。
|
||||
|
||||
## Lv1〜3と解放
|
||||
|
||||
全30施設にLv1/2/3を別レコードとして置き、価格・人数・staff_min/max・解放条件・Policy解放割当を未確定として追跡する。A04/A05の初期値をLv1候補へ参照するが、承認本文にLv指定はなく、Lv2/3で不変とも自動増加とも承認されていない。原本165行のLvMAXに数値はない。計画P09-03のLv1〜3と現既定max_level3を混同しない。
|
||||
|
||||
現FacilityDefinitionにレベル別料金/人数の表はない。厨房などのupgrade_costsは旧実装の費用列であり、レベル別営業値ではない。unlock_rank既定1は正式な個別解放承認ではなく、現通常建設コマンドにunlock_rank/research_requirementsの評価もない。別冊Aの世界樹ランク表や初回Day案内は派生計画/実装欄に保全し、原本施設別解放条件へ昇格しない。原本179行の価格、食材、混浴、ハード設定、イカサマ率のLv解放は機能要件として残り、どのLvでどの値にするかを捏造しない。
|
||||
|
||||
## 未定義13施設から抽出できる範囲
|
||||
|
||||
{chr(10).join(blueprint_table)}
|
||||
|
||||
これらはsource-only blueprintとして先行定義できる。費用・人数・職員上限・マナ量・属性・個別解放・Lv2/3の値が未確定のまま、FacilityDefinitionの既定100G/1資材/60分などを適用して通常建設へ公開しない。既存地下客室をドミトリー、地下VIPサロンをVIPスイート、魔力制御室をマナ炉の完成代替と扱わない。
|
||||
|
||||
## 次に進める低依存作業
|
||||
|
||||
{chr(10).join('- ' + r['action'] for r in recommendations)}
|
||||
|
||||
この監査は値の出典と欠損を整理したもので、P09や各施設の受入完了ではない。通常操作、グループ利用中変更、日跨ぎ、途中保存、修正後同一ソース回帰の証拠は引き続き必要である。第9章経済施設、通路/階段/ドア/リネンシュート/隠し扉へのF各施設/Lv素材適用範囲はatomicのopen expansion registerと同じ未確定状態を維持する。
|
||||
|
||||
静的データの追加観測として、医務室定義には同値のstaff_max/capacity二重代入と、FacilityDefinitionに宣言がないqueue_staff_minがある。台帳のdefinition_anomaliesへ保存し、今回Runtimeへの影響を推測したり本体を修正したりしない。validatorは49出典ファイルのSHA、30施設/780値/90Lvの構造、全出典行、D03除外、架空値拒否を確認する。PASSは台帳の整合性だけを意味する。
|
||||
"""
|
||||
(OUT / "P09_VALUE_CONTRACTS.md").write_text(readme)
|
||||
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"confirmed": [
|
||||
{
|
||||
"id": "A01",
|
||||
"title": "維持費の計上",
|
||||
"decision": "完成から撤去までの保有時間で日額按分。閉店・休止中も計上し00:00に精算。不足を翌日へ繰越。",
|
||||
"origin": "user_reply",
|
||||
"reference": "../original-completion-2026-09-29/execution/UPKEEP_IMPLEMENTATION.md"
|
||||
},
|
||||
{
|
||||
"id": "A02",
|
||||
"title": "維持費の初期試験値",
|
||||
"decision": "建築費の0.5%/日を初期試験値として試験・調整する。既存の整数処理を保ち、礼拝堂・清掃員室は個別承認値を使う。",
|
||||
"origin": "user_reply",
|
||||
"reference": "../original-completion-2026-09-29/execution/UPKEEP_IMPLEMENTATION.md"
|
||||
},
|
||||
{
|
||||
"id": "A03",
|
||||
"title": "アンリの加入と売上補正",
|
||||
"decision": "従業員画面で無料加入を選ぶ。配置フロアの利用料金を20%増やし客の支払額と施設収入を一致させる。",
|
||||
"origin": "user_reply",
|
||||
"reference": "../original-completion-2026-09-29/execution/ANRI_APPROVED_INTEGRATION.md"
|
||||
},
|
||||
{
|
||||
"id": "A04",
|
||||
"title": "礼拝堂の初期試験値",
|
||||
"decision": "4×3・表層。建築1200G、資材12、工期180分、同時8人、聖職者最低1人/上限2人、礼拝60分、寄付20G、完了時信仰+1、維持費6G/日。",
|
||||
"origin": "user_reply_in_conversation"
|
||||
},
|
||||
{
|
||||
"id": "A05",
|
||||
"title": "清掃員室と清掃の初期試験値",
|
||||
"decision": "2×1・裏層。建築350G、資材4、工期60分、清掃員最低1人/上限2人、顧客0人・利用料なし、維持費2G/日。清潔0〜100・初期100、75以下で予約、現地8分で25回復。定義済み汚れ量を使い未定義値を補完しない。",
|
||||
"origin": "user_reply",
|
||||
"reference": "../original-completion-2026-09-29/execution/CLEANING_APPROVED_FOUNDATION.md"
|
||||
}
|
||||
],
|
||||
"pending": [
|
||||
{
|
||||
"id": "D01",
|
||||
"title": "欲求一致加算の必須値",
|
||||
"details": "不足基準、加算額/率、施設の属性割当。複数属性・重複加算は採用しない限り明示未設定を維持。",
|
||||
"needed_at": "P09の本番課金",
|
||||
"independent_work": "計算契約、未設定表示、非一致利用、財布・台帳の試験枠を進める。",
|
||||
"kind": "required_before_dependent_charging"
|
||||
},
|
||||
{
|
||||
"id": "D02",
|
||||
"title": "施設別の人数と従業員上限",
|
||||
"details": "原本や個別承認にない人数専用上限、staff_max、Lv別変更を整理。capacityから推測転記しない。",
|
||||
"needed_at": "各施設の通常営業",
|
||||
"independent_work": "明示値がある施設から進め、未設定の一覧と超過拒否を作る。",
|
||||
"kind": "required_per_facility"
|
||||
},
|
||||
{
|
||||
"id": "D03",
|
||||
"title": "休止復帰と複数関連施設",
|
||||
"details": "休止復帰条件と複数関連施設AND/ORはユーザー指定により未設定。解除なし・単一依存の現仕様を維持。",
|
||||
"needed_at": "新たな復帰/複数条件を追加する場合",
|
||||
"independent_work": "現仕様を接続して試験する。回答待ちで現実装を止めない。",
|
||||
"kind": "explicitly_unset_optional_extension"
|
||||
},
|
||||
{
|
||||
"id": "D04",
|
||||
"title": "未指定の経済と行動係数",
|
||||
"details": "税・利息・マナ・天候・環境・役割・成長等の必須値。A01〜A05は決定済み。",
|
||||
"needed_at": "各機能の本番値固定",
|
||||
"independent_work": "原本明示値、契約、候補比較を先行する。",
|
||||
"kind": "required_per_feature"
|
||||
},
|
||||
{
|
||||
"id": "D05",
|
||||
"title": "同時発生と安全処理",
|
||||
"details": "EV/搬送停止、費用時点、日跨ぎ順序の未確定部分。承認済み維持費時点を再確認しない。",
|
||||
"needed_at": "更新規約の凍結",
|
||||
"independent_work": "原状保持と重複防止、具体的な境界事例を作る。",
|
||||
"kind": "implementation_contract"
|
||||
},
|
||||
{
|
||||
"id": "D06",
|
||||
"title": "解消不能な旧配置の移行",
|
||||
"details": "占有衝突や移動中の状態の扱い。旧セーブの拒否時原状保持は必須。",
|
||||
"needed_at": "旧配置に不可逆な変更を適用する前",
|
||||
"independent_work": "読み取り移行プレビュー、バックアップ、拒否時の復帰を進める。",
|
||||
"kind": "required_only_for_ambiguous_conversion"
|
||||
},
|
||||
{
|
||||
"id": "D07",
|
||||
"title": "旧関係値と人物進行の意味",
|
||||
"details": "旧5値と原本3値の意味対応、既存ルートの扱い。",
|
||||
"needed_at": "旧進行の変換",
|
||||
"independent_work": "旧値を保全し、新規ゲームの原本モデルを実装する。",
|
||||
"kind": "required_only_for_ambiguous_conversion"
|
||||
},
|
||||
{
|
||||
"id": "D08",
|
||||
"title": "章とルートと必要内容",
|
||||
"details": "章、人物ごとの分岐、全エンド、図鑑対象、未制作内容の範囲。",
|
||||
"needed_at": "内容の凍結と最終受入",
|
||||
"independent_work": "非露骨な状態・分岐・中立テストで構造を検証する。未制作は原本差異として残す。",
|
||||
"kind": "content_scope"
|
||||
},
|
||||
{
|
||||
"id": "D09",
|
||||
"title": "素材品質と制作量",
|
||||
"details": "完成素材の品質、必要量、担当、予算、権利。The Towerの提供ファイルは体験の参考とする。",
|
||||
"needed_at": "完成素材受入や発注",
|
||||
"independent_work": "Kenney優先の調査、ライセンス台帳、差替契約、既存資産の確認を進める。",
|
||||
"kind": "asset_delivery"
|
||||
},
|
||||
{
|
||||
"id": "D10",
|
||||
"title": "Windowsの性能と評価環境",
|
||||
"details": "対象機スペック、表示倍率、解像度、描画目標、評価担当。",
|
||||
"needed_at": "Windowsでの性能合否",
|
||||
"independent_work": "利用できる対象機を記録し、機能検証と手元の実測を進める。",
|
||||
"kind": "platform_acceptance"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
# 原本全実装に向けた作業計画 2026年10月改訂
|
||||
|
||||
作成日: 2026-10-02。基準ソース: `285528b7a3b1b46a2a67af5aa90d9085539b42f4`。作業対象: ローカル制作コピー `/Users/korizatou114/Projects/とりにく工房ゲーム制作部`。
|
||||
|
||||
この計画は、指定された基本仕様書の全実装と会話で追加された施設仕様を、通常操作、保存復元、画面、内容資産、Windows配布版の受入まで進めるための改訂版である。既存成果を再利用し、未接続・未検証・未制作を残作業として追跡する。最初に原子要件と現在の証拠を再監査し、その後は通常操作から保存まで通る小さな変更として実装を進める。
|
||||
|
||||
2026-09-29版を作業順と進捗管理の面で更新する。旧計画と旧証拠は履歴として保持する。この文書の作成は、残件の実装やゲームの合格を示すものではない。
|
||||
|
||||
## 原本と目標
|
||||
|
||||
原本は指定Driveファイルの[保存済み読取版](../../traceability/original/BASELINE.txt)と[取得情報](../../traceability/original/baseline_metadata.json)を用いる。会話での明示追加・承認を適用し、別冊Aは既存全パラメータの参照資料として扱う。派生仕様書、古い実装、都合のよい仮値を原本に優先させない。今回の計画作成ではDriveを新規再取得していないため、保存版を「最新取得済み」とは表記しない。
|
||||
|
||||
全75要件群を対応表に保持する。機能・内容71群と過去工程4群を分け、原本30施設、施設主要10項目、7ヒロイン、ライバル、全エンド、図鑑、素材と音、Windows、20〜30時間の本編と50時間以上の全要素クリアを含める。原本の完全クリアには図鑑コンプが含まれ、物語の終了だけで代用しない。
|
||||
|
||||
The Towerの参考資料は、動きのあるドット断面、行列、EV、職員の仕事、混雑と改善を観察できる操作体験の参考とする。専用素材の制作、既存素材の出典、原本との対応を個別に管理する。
|
||||
|
||||
## 現在地
|
||||
|
||||
| 確認項目 | 現在の確認値 | 完了として認定していないこと |
|
||||
|---|---|---|
|
||||
| 最新本体 | 採用後マップ1396ファイルのSHAを再照合し差分0 | 候補コード、旧版の配布物を現本体として扱わない |
|
||||
| 全自動回帰 | 155スイート、12,310チェック、失敗0 | 原本全実装、全資産、操作感、Windowsの合格 |
|
||||
| 原本30施設 | 通常登録16、定義のみ1、未定義13 | 16件の全機能・全レベルの受入 |
|
||||
| 施設主要10項目 | 人数や状態の基盤と料金未設定表示は部分採用 | 欲求一致加算の実徴収、全施設の必須値 |
|
||||
| 人物 | アンリの通常加入と一部固有効果等を接続 | 7ヒロイン全員の通常加入、技能、成長、全ルート |
|
||||
| Windows | 旧1394版の開発配布物あり | 現1396版の書出しと同一EXEでの実機受入 |
|
||||
| 性能と長期プレイ | 限定的な軽量化と試験あり | Runtime全体の予算達成、本編・全要素の実プレイ受入 |
|
||||
|
||||
施設監査時の37定義、カタログ、定義スキーマ、施設対応表の40ファイルは現本体でもSHAが一致したため、16/1/13は現在の静的登録状態として使える。通常登録率16/30=53.33%は施設機能の完成率ではない。最新回帰の100%も全実装率とは別である。[基準と照合結果](baseline.json)
|
||||
|
||||
旧台帳は完了1、着手中17、予定12であった。部分実装の記録があるP09、P14、P15、P16、P20、P23、P25、P28を着手中へ修正し、新台帳は完了1、着手中25、予定4とする。新たに完了へ昇格した項目はない。唯一の完了P00は基準固定であり、1/30=3.33%をゲーム全体の実装率として表示しない。旧台帳の各証拠は過去記録として保持する。
|
||||
|
||||
## 完了条件と進捗率
|
||||
|
||||
各原子要件には、原本条件、定義、実行処理、通常UI入口、保存・移行、境界試験、完成資産と配布環境、証拠の8観点を割り当てる。適用外は理由と確認者を記録し、未実装や未制作を適用外に置き換えない。候補作成、モデル試験、定義登録、通常プレイ受入を別状態で扱う。
|
||||
|
||||
P01で分母を固定するまでは総合実装率と総合受入率を未算出にする。75複合群、30作業パッケージ、テスト件数、ファイル数を代用の分母にしない。以下は原子要件の件数割合であり、労力の割合ではない。
|
||||
|
||||
- 実装率 = 必須の定義・実行処理・通常UI・保存条件をすべて満たした原子要件数 ÷ 対象原子要件総数 × 100。
|
||||
- 受入率 = すべての適用完了条件と現版の証拠がそろった原子要件数 ÷ 同じ対象原子要件総数 × 100。
|
||||
- 部分実装は達成済み観点を別表示し、総合割合へ一律50%として加えない。未設定・未検証・未制作も分母に残す。
|
||||
- 工程4群は機能の分母から除き、工程達成として別記する。過去の納期達成を後日の実装でPASSに変更しない。
|
||||
- 原本と追加仕様の重複条件は一度だけ数える。分母の変更時は追加・削除・統合したIDと理由を履歴に残す。
|
||||
- 報告には基準HEAD、分母、分子、保留数、差異数、集計時刻を付け、未丸め値から小数点2桁へ表示する。工数割合が必要なら別途残作業量を見積もり、件数割合と混ぜない。
|
||||
|
||||
本計画の残作業単位は実行順を管理するための区切りであり、まだ原子要件の分母ではない。[75群の対応表](requirement_coverage.json)は網羅性を維持するもので、各群の現在合否を保証するものではない。
|
||||
|
||||
## 承認済み仕様と未確定項目
|
||||
|
||||
承認済みの維持費方式、0.5%初期値、アンリ、礼拝堂、清掃員室と清掃の試験値は再確認せず使用する。これらを原本由来の数値と混同せず、個別承認値として管理する。
|
||||
|
||||
| ID | 承認済み事項 |
|
||||
|---|---|
|
||||
| A01 | 完成から撤去までの保有時間で日額按分。閉店・休止中も計上し00:00に精算。不足を翌日へ繰越。 |
|
||||
| A02 | 建築費の0.5%/日を初期試験値として試験・調整する。既存の整数処理を保ち、礼拝堂・清掃員室は個別承認値を使う。 |
|
||||
| A03 | 従業員画面で無料加入を選ぶ。配置フロアの利用料金を20%増やし客の支払額と施設収入を一致させる。 |
|
||||
| A04 | 4×3・表層。建築1200G、資材12、工期180分、同時8人、聖職者最低1人/上限2人、礼拝60分、寄付20G、完了時信仰+1、維持費6G/日。 |
|
||||
| A05 | 2×1・裏層。建築350G、資材4、工期60分、清掃員最低1人/上限2人、顧客0人・利用料なし、維持費2G/日。清潔0〜100・初期100、75以下で予約、現地8分で25回復。定義済み汚れ量を使い未定義値を補完しない。 |
|
||||
|
||||
|
||||
施設追加仕様では、capacityは保管量・生産量等にも使うため顧客人数へ転記しない。staff_maxは必要最低人数と別にする。関連施設は配置の有無ではなく稼働を判定し、条件回復だけで破壊・摘発後の休止を解除しない。欲求一致は料金加算条件であり、不一致利用を禁止しない。
|
||||
|
||||
欲求一致の額・率、不足基準、施設別属性と必須人数などは局所的な未確定として扱う。複数属性、重複加算、複数関連施設のAND/OR、休止復帰条件は現指定では未設定を維持し、追加仕様を発明しない。待ちがある項目と独立した作業を続ける。
|
||||
|
||||
| ID | 未確定事項 | 固定が必要な地点 | 先に進める作業 |
|
||||
|---|---|---|---|
|
||||
| D01 | 不足基準、加算額/率、施設の属性割当。複数属性・重複加算は採用しない限り明示未設定を維持。 | P09の本番課金 | 計算契約、未設定表示、非一致利用、財布・台帳の試験枠を進める。 |
|
||||
| D02 | 原本や個別承認にない人数専用上限、staff_max、Lv別変更を整理。capacityから推測転記しない。 | 各施設の通常営業 | 明示値がある施設から進め、未設定の一覧と超過拒否を作る。 |
|
||||
| D03 | 休止復帰条件と複数関連施設AND/ORはユーザー指定により未設定。解除なし・単一依存の現仕様を維持。 | 新たな復帰/複数条件を追加する場合 | 現仕様を接続して試験する。回答待ちで現実装を止めない。 |
|
||||
| D04 | 税・利息・マナ・天候・環境・役割・成長等の必須値。A01〜A05は決定済み。 | 各機能の本番値固定 | 原本明示値、契約、候補比較を先行する。 |
|
||||
| D05 | EV/搬送停止、費用時点、日跨ぎ順序の未確定部分。承認済み維持費時点を再確認しない。 | 更新規約の凍結 | 原状保持と重複防止、具体的な境界事例を作る。 |
|
||||
| D06 | 占有衝突や移動中の状態の扱い。旧セーブの拒否時原状保持は必須。 | 旧配置に不可逆な変更を適用する前 | 読み取り移行プレビュー、バックアップ、拒否時の復帰を進める。 |
|
||||
| D07 | 旧5値と原本3値の意味対応、既存ルートの扱い。 | 旧進行の変換 | 旧値を保全し、新規ゲームの原本モデルを実装する。 |
|
||||
| D08 | 章、人物ごとの分岐、全エンド、図鑑対象、未制作内容の範囲。 | 内容の凍結と最終受入 | 非露骨な状態・分岐・中立テストで構造を検証する。未制作は原本差異として残す。 |
|
||||
| D09 | 完成素材の品質、必要量、担当、予算、権利。The Towerの提供ファイルは体験の参考とする。 | 完成素材受入や発注 | Kenney優先の調査、ライセンス台帳、差替契約、既存資産の確認を進める。 |
|
||||
| D10 | 対象機スペック、表示倍率、解像度、描画目標、評価担当。 | Windowsでの性能合否 | 利用できる対象機を記録し、機能検証と手元の実測を進める。 |
|
||||
|
||||
|
||||
## 実装順と到達点
|
||||
|
||||
大きなパッケージ全体の完了を待ってすべての着手を止めない。各変更に必要な保存・更新・会計・交通の契約が受入済みなら、その施設や人物の縦断実装を始める。全体統合と最終認定では依存パッケージ全体を受け入れる。
|
||||
|
||||
| 段階 | 作業と主な担当パッケージ | 到達点と次の条件 |
|
||||
|---|---|---|
|
||||
| 0 | P00〜P02。基準固定、原子台帳、証拠と通常操作の再監査 | 分母と残件が確定し、現版と過去版・候補が区別される |
|
||||
| 1 | P03〜P09、P22、P28の先行測定。保存、更新順、仕入れ、マナ、施設共通契約 | 通常営業の財布・台帳・停止・人数が一貫し、新規機能の保存条件が明確 |
|
||||
| 2 | P08〜P14、P23。原本30施設、特殊設備、交通、断面表示 | 全施設を通常建設・運用でき、依存喪失・移動・保存の境界を通る |
|
||||
| 3 | P15〜P19、P24。客、職員、7ヒロイン、関係、イベント、通常操作 | 各人物と分岐へ通常到達でき、業務・料金・効果が保存再開で一致する |
|
||||
| 4 | P20〜P26。ランク、章、エンド、図鑑、防衛、完成素材と音 | 新規から本編と全要素へ到達でき、必要内容と完成資産がそろう |
|
||||
| 5 | P27〜P29。要件別受入、通しデバッグ、性能、長期プレイ、Windows最終監査 | 同じ候補版で全必須要件と配布版を受け入れる |
|
||||
|
||||
素材の必要量整理と制作、Windowsの部分確認、負荷測定は早期から続ける。P28の測定を最後まで先送りしない。P27〜P29の最終合格には機能と内容の全体完了が必要である。
|
||||
|
||||
## 直近の着手順
|
||||
|
||||
| 順番 | 次に行う変更 | 必要な前提 | 終了条件 |
|
||||
|---|---|---|---|
|
||||
| 1 | P01の台帳分割と現在証拠の再監査 | 保存版原本と現HEADの固定 | 原子分母、現合否、未設定と未制作が再計算可能 |
|
||||
| 2 | 未採用Pathの不正transit部分成功ガードを受入 | 現HEADへ限定リベース、基準経路の契約 | 異常時全拒否、正常互換、保存、1200Tickの全状態・全イベント比較、関連回帰 |
|
||||
| 3 | P05有料自動仕入れの補正候補を受入 | 現HEADへ限定リベース、手動購入と領収書契約 | 二重課金・数量単価・資金不足・返送・停止・再起動の試験、通常UI、全回帰、限定採用 |
|
||||
| 4 | P09の10項目対応と施設別必須値の整理 | 人数・状態・料金の契約と既承認値 | 未確定の局所一覧、価格合成の比較案。確定した範囲から通常課金へ接続 |
|
||||
| 5 | P10〜P14の施設を一件ずつ通常営業まで完成 | 当該施設が使う共通契約と値 | 定義だけで止めず建設・職員・客・会計・保存・画面を通す |
|
||||
| 6 | 初回の最新Windows確認と性能測定 | 採用済み最新版、記録した対象機 | 旧1394配布物から更新し、同一EXEの操作と現Runtimeの性能を記録 |
|
||||
|
||||
候補の受入に失敗したら本体へ採用せず、再現条件を固定して修正する。上記2と3は安全性・会計の独立した変更として検証する。性能探索と新機能の意味を同時に変更せず、比較できる基準を残す。
|
||||
|
||||
## 作業パッケージの残件
|
||||
|
||||
P00〜P29のIDを維持して過去記録との対応を保つ。各項目には現在地、統合前提、実装単位、追加受入を記載する。下記の作業単位はいずれも本改訂で完了を宣言していない。P00の継続照合は既存基準の維持作業である。
|
||||
|
||||
### P00 原本 作業対象の固定
|
||||
|
||||
現在地: 基準固定済み。指定原本の保存版を使用。
|
||||
|
||||
統合前提: なし。原本対応: B-1.1, B-14.1, B-14.2, B-14.3, B-14.4。
|
||||
|
||||
- P00-01 原本と基準ソースの継続照合。指定原本の読取保存版と別冊AのSHA、HEAD、採用済みマップを固定し、更新時だけ影響要件を再監査する。 受入: 原本の優先順位と差分履歴が再現できる。最新Drive再取得を今回実施したとは表示しない。
|
||||
|
||||
### P01 原子要件と受入台帳
|
||||
|
||||
現在地: 75群の対応は存在するが、原子要件と現在の受入状態は未確定。
|
||||
|
||||
統合前提: P00。原本対応: B-1.1, B-1.2, B-1.3, B-1.4, B-1.5, B-1.6, B-14.1, B-14.2, B-14.3, B-14.4。
|
||||
|
||||
- P01-01 原子要件への分割。原本文の複数条件を分割し、施設30件、追加10項目、7人物、図鑑、全エンド、工程4件を漏れなく対応付ける。 受入: 各原本文に対応先があり、同じ条件を重複加点しない。
|
||||
- P01-02 現在の実装と証拠の再監査。実装、未接続、未検証、未設定、未制作を分け、現在HEADに適用できる証拠と過去証拠を識別する。 受入: 通常入口・Runtime・保存・試験・資産の対応が存在し、候補コードを採用済みとして数えない。
|
||||
- P01-03 集計基準の固定。原子要件の分母、適用する完了条件、理由付き適用外、判定者を版管理して初回進捗を集計する。 受入: 分母と分子のID一覧から他者が再計算できる。テスト件数や部分実装への一律半点を使用しない。
|
||||
|
||||
### P02 既存機能の回帰基準
|
||||
|
||||
現在地: 最新本体の155スイートと12,310チェックは合格。全原本受入とは別。
|
||||
|
||||
統合前提: P00。原本対応: B-3.1a, B-3.1b, B-3.3, B-3.4a, B-3.4b, B-4.2, B-4.3c, B-5.4。
|
||||
|
||||
- P02-01 回帰と代表セーブの固定。最新全回帰のソース一致を確認し、初期・営業中・移動中・日付境界・防衛中の代表セーブを選ぶ。 受入: ログ、入力、シード、保存版、チェック数が対応し、失敗を含む履歴が追える。
|
||||
- P02-02 通常操作の基準シナリオ。開業、雇用、配置、仕入れ、建設、保存終了再開を通常画面で通す。 受入: デバッグ値投入の試験と自然営業の試験を区別し、画面と実状態が一致する。
|
||||
- P02-03 Windows基準環境の記録。利用できるWindows機のOS、CPU、GPU、RAM、表示倍率、解像度、検証するビルドを記録する。 受入: 過去の実機確認を現ビルドへ転用せず、同一EXEの起動と保存再開を確認する。
|
||||
|
||||
### P03 保存形式と移行契約
|
||||
|
||||
現在地: 保存・移行の基盤と複数の復元試験はある。全原本の追加状態は未完。
|
||||
|
||||
統合前提: P01, P02。原本対応: B-2.1, B-4.1, B-5.2, B-6.3, B-7.2, B-8.1, B-9.1。
|
||||
|
||||
- P03-01 機能別の保存契約。会計、施設状態、人数、欲求属性、マナ、人物、図鑑、物語に新規項目を追加する前に保存条件を固定する。 受入: 新旧版・欠損・未知版・破損を検証し、失敗時に原データが変わらない。
|
||||
- P03-02 配置と関係値の旧版移行。寸法変更、動線喪失、旧関係値の意味を個別に確認し、移行プレビューと拒否時の復帰を用意する。 受入: 移動中、EV乗車中、搬送中、同時建設中の参照が孤立しない。推測による関係値変換をしない。
|
||||
- P03-03 プロセスを跨ぐ再開。営業・配送・清掃・防衛・人物進行の途中で保存、終了、再起動して比較する。 受入: 同じ操作列の施設・財布・台帳・人物・イベントが一致し、二重効果がない。
|
||||
|
||||
### P04 更新順序 責務の整理
|
||||
|
||||
現在地: 更新順と分離サービスの改善は部分採用済み。全機能の順序契約は継続。
|
||||
|
||||
統合前提: P03。原本対応: B-1.3, B-1.4, B-3.2, B-9.2。
|
||||
|
||||
- P04-01 同時発生の更新規約。閉店、停電、破壊、退出、課金、シフト交代、00:00決算の順序を明記する。 受入: 順序境界で停止中課金、二重決算、予約の孤立がない。
|
||||
- P04-02 トランザクションと再送。課金・補給・依存判定・イベント効果の失敗時原状保持と再送時の一回反映を接続する。 受入: 途中失敗後の状態・帳簿・再試行結果が一致する。
|
||||
- P04-03 Runtimeの負荷監視。新サービス導入ごとにTick・snapshot・経路・描画更新を同一条件で測る。 受入: 状態や乱数を変える軽量化を受け入れず、性能結果と意味的同等性を別に判定する。
|
||||
|
||||
### P05 全収支 維持費 税 借入
|
||||
|
||||
現在地: 承認済み維持費は接続済み。有料自動仕入れの修正候補は未採用。
|
||||
|
||||
統合前提: P04。原本対応: B-3.4a, B-3.4b, B-9.1, B-9.2, B-9.3。
|
||||
|
||||
- P05-01 自動仕入れ候補の受入。1394基準の補正候補を現1396基準へ限定リベースし、見積・数量・単価・領収書・配送と通常UIへ接続する。 受入: 手動購入互換、資金不足、二重課金、返送、停電、隔離、途中再起動を検証してから採用する。
|
||||
- P05-02 全費目と資金区分。表金・裏金、維持費未払い、仕入れ、人件費、元本、利息、税を帳簿へ接続する。 受入: 3営業日で資金増減と台帳が一致し、借入元本を売上に含めない。
|
||||
- P05-03 金庫と銀行の通常営業。金庫容量、購買窓口、借入、返済、滞納を通常画面から操作する。 受入: 上限・不足・期限・保存・赤字終了と、滞納から脅威への接続を確認する。
|
||||
|
||||
### P06 マナ枯渇と全館停止
|
||||
|
||||
現在地: 停電、厨房、救護の停止復帰は部分実装。有限マナ供給の契約が残る。
|
||||
|
||||
統合前提: P04。原本対応: B-2.6, B-9.1, B-10.6, B-10.7。
|
||||
|
||||
- P06-01 供給と蓄積の契約。世界樹、マナ炉、消費施設の生成・蓄積・消費と旧ゼロ値保存の意味を整理する。 受入: 初期値や係数の根拠を区別し、旧版移行と残量境界で矛盾がない。
|
||||
- P06-02 全館停止の統合。利用中、EV、ワープ、物流、救護、清掃、入館へ同じ停止判定を接続する。 受入: 停電中の無条件売上・生産がなく、乗客と搬送対象が消えない。
|
||||
- P06-03 復帰と表示。供給復帰時の予約・在庫・運行を再開し、HUDへ供給、消費、蓄積を表示する。 受入: 破壊・摘発の休止を停電復旧で解除しない。途中保存後も復帰結果が一致する。
|
||||
|
||||
### P07 天候 流行 環境
|
||||
|
||||
現在地: 嵐の境界と天候保存、清潔度基盤は部分接続済み。
|
||||
|
||||
統合前提: P04。原本対応: B-2.3, B-2.6, B-3.2, B-3.5, B-4.5, B-11.1。
|
||||
|
||||
- P07-01 雨と霧と自然天候。雨の来客減・滞在延長、マナ霧、自然変化を実営業へ接続する。 受入: 天候切替直前後、途中保存、自然来客の差を同条件で説明できる。
|
||||
- P07-02 環境と客反応。景観・清潔・騒音、採光・遮音・隠蔽を満足度、客特性、売上へ接続する。 受入: 異なる環境で実際の選択と満足度が変化し、清潔伝播も保存される。
|
||||
- P07-03 流行ニュースと可視化。流行の効果、期限、通知、将来需要の説明を通常画面へ出す。 受入: 通知文と需要変化が一致し、再読込でニュースや効果が重複しない。
|
||||
|
||||
### P08 施設寸法 既存配置の整合
|
||||
|
||||
現在地: 原本に対応する17定義の寸法・層は整合。全30の配置運用は未完。
|
||||
|
||||
統合前提: P03, P01。原本対応: B-2.1, B-8.1, B-10.1, B-10.2, B-10.3, B-10.4, B-10.5, B-10.6, B-10.7。
|
||||
|
||||
- P08-01 全30施設の配置条件。未定義13件と闇市場を含め、正規ID・寸法・層・地上地下・高層・背面条件を固定する。 受入: 30件の原本対応と個別建設条件が一致し、類似施設で代用しない。
|
||||
- P08-02 旧配置の安全な変更。混合建設、占有変更、役者の実位置、救護・物流・EVをまとめて移行する。 受入: 重複、孤立、消失がない。解消不能時は元配置とセーブを保つ。
|
||||
- P08-03 配置の通常操作受入。通常パレットから支持・接続・不正位置・撤去・再建築を操作する。 受入: 拒否理由と実判定が一致し、パン・ズーム・保存再開後も同じ位置を扱う。
|
||||
|
||||
### P09 施設共通契約と追加10項目
|
||||
|
||||
現在地: 人数・稼働状態等の基盤と料金未設定表示は採用済み。欲求加算の実徴収は未接続。
|
||||
|
||||
統合前提: P05, P06, P08。原本対応: B-8.1, B-8.2, B-8.3, B-8.5, B-9.2。
|
||||
|
||||
- P09-01 施設10項目の統合。人数専用上限とcapacity、職員上限と最低人数、単一関連施設と稼働フラグ、3状態を区別して全定義へ適用する。 受入: 待機者を利用人数に数えず、関連施設が配置だけで稼働扱いにならず、休止は自動復帰しない。
|
||||
- P09-02 欲求一致加算の接続。不足基準、料金加算、施設属性の必須未確定値を整理し、確定後に見積から実徴収・台帳までつなぐ。 受入: 一致時のみ指定加算、不一致でも利用可。財布減額と収入が一致し、アンリ補正との順序と丸めを確認する。
|
||||
- P09-03 施設レベルと営業設定。Lv1からLv3の価格、人数、解放条件、営業Policyと説明を接続する。 受入: 通常操作、利用中変更、人数超過、停止、日跨ぎ、途中保存で契約が一致する。
|
||||
|
||||
### P10 宿泊 飲食施設の完成
|
||||
|
||||
現在地: 一般・上級客室等は部分営業済み。VIP・ドミトリー・レストラン・屋台が未定義。
|
||||
|
||||
統合前提: P09。原本対応: B-8.2, B-10.1, B-10.2。
|
||||
|
||||
- P10-01 宿泊4種類の完了。VIPスイートとドミトリーを追加し、一般・上級を含む宿泊条件、精算、景観、人数をそろえる。 受入: 4種類を通常建設し自然客が利用、前払い・退店・停止・保存を検証する。
|
||||
- P10-02 飲食4種類の完了。レストラン、屋台を追加し、食堂、酒場と厨房、食材、職種、物流を統合する。 受入: 厨房の稼働喪失・在庫不足・届く前の利用拒否・営業収支を確認する。
|
||||
- P10-03 営業と中期収支。通常初期資金で新設施設を運用し、複数シードと序盤・中盤の需要で試す。 受入: 施設別売上と費用、依存、人数、移動、保存が一致する。
|
||||
|
||||
### P11 衛生 娯楽 礼拝施設
|
||||
|
||||
現在地: 大浴場と礼拝堂は部分接続済み。カジノと闘技場が未定義。
|
||||
|
||||
統合前提: P09, P07。原本対応: B-8.2, B-10.3。
|
||||
|
||||
- P11-01 娯楽2施設の営業。カジノと闘技場の定義、従業員、利用、会計、解放、設定を接続する。 受入: 通常建設から利用、収益、退出、停止、保存再開まで成立する。
|
||||
- P11-02 浴場と礼拝の統合。浴場と承認済み礼拝堂を人数・清潔・職種・信仰・ランクへ接続する。 受入: 寄付、信仰、人物補正、営業停止が一回だけ反映される。
|
||||
- P11-03 4施設の境界受入。同時利用限界、職員離席、停電、破壊、関連条件喪失を試す。 受入: 表示された稼働理由・利用可否と実状態が一致する。
|
||||
|
||||
### P12 バックヤード施設完成
|
||||
|
||||
現在地: 宿舎・清掃員室・実清掃・救護等は部分接続済み。全業務と特殊設備は残る。
|
||||
|
||||
統合前提: P09。原本対応: B-2.4, B-5.3, B-5.4, B-8.3, B-10.5, B-3.3。
|
||||
|
||||
- P12-01 裏方7施設の完成。厨房・倉庫・待機・宿舎・清掃・警備・医務の配置、職種、仕事、停止をそろえる。 受入: 客侵入を防ぎ、実出勤から実作業・休息・帰宅まで成立する。
|
||||
- P12-02 専用動線と設備。リネンシュート、隠し扉、従業員ドアを物流、清掃、救護経路へ接続する。 受入: 孤立入口、経路喪失、交代、撤去で人員・物資・救護対象が消えない。
|
||||
- P12-03 全シフトの運用。昼夜の交代、休息、清掃、配送、救護が重なる通常一日を操作する。 受入: 人数と予約が上限内で、停止・保存再開を跨いでも重複業務がない。
|
||||
|
||||
### P13 特殊 地下施設の非描写システム接続
|
||||
|
||||
現在地: 一部関連基盤はあるが原本の特殊施設接続は未着手。
|
||||
|
||||
統合前提: P09, P12。原本対応: B-1.6, B-2.5, B-8.2, B-8.3, B-10.4, B-10.6。
|
||||
|
||||
- P13-01 特殊施設とアクセス。原本の特殊・地下施設を配置、職員アクセス、役割、会計の非描写状態として接続する。 受入: 特殊条件の建設時・運用時判定が一致し、無効配置が通らない。
|
||||
- P13-02 背面条件と対象参照。覗き部屋の対象背面、通路寸断、対象喪失と証拠IDを状態として実装する。 受入: 対象撤去、重複収集、停止、途中保存で状態と参照が整合する。
|
||||
- P13-03 内容差異の記録。地下牢、マナ炉等のイベント接続と未制作の表現資産を別に管理する。 受入: 状態処理の完成を内容制作の完成として加点しない。
|
||||
|
||||
### P14 垂直交通 ワープ完成
|
||||
|
||||
現在地: EV・階段と救護同伴等の基盤あり。ワープと全経路受入が残る。
|
||||
|
||||
統合前提: P06, P09。原本対応: B-2.4, B-4.3a, B-4.3b, B-4.3c, B-8.4, B-10.7。
|
||||
|
||||
- P14-01 交通の失敗時契約。不正transitキーでsnapshot全体を拒否する未採用候補を現基準で検証する。 受入: 部分成功を返さず、正常順序・型・所有権・保存・経路が基準と一致する。
|
||||
- P14-02 ワープの通常接続。マナワープの建設、対象、待ち時間、消費、停止、実移動を接続する。 受入: 途中保存、マナ不足、到着不能時に移動や費用が二重反映されない。
|
||||
- P14-03 混雑と経路選択。客用・業務用EV、階段、ワープの待ち時間をA*費用へ反映する。 受入: ラッシュ、相乗り、50%減速、救護同伴、経路喪失を実位置と人数で検証する。
|
||||
|
||||
### P15 客の全パラメータ 時間帯AI
|
||||
|
||||
現在地: 欲求選択と候補拒否ログ等は部分実装。全客特性は未完。
|
||||
|
||||
統合前提: P07, P10, P11, P14, P13。原本対応: B-3.2, B-4.1, B-4.2, B-4.4, B-4.5, B-9.4。
|
||||
|
||||
- P15-01 時間帯と欲求。朝の下り、夕の上り、夜の目的地を四欲求、距離、行列、価格へ接続する。 受入: 最大欲求を維持し、日付・天候・施設変化で目的地分布が説明できる。
|
||||
- P15-02 種族と環境。クラス・種族、忍耐、清潔、汚染伝播、戦利品を実来客モデルへ接続する。 受入: 原本の客種ごとに反応が異なり、再読込で属性が再抽選されない。
|
||||
- P15-03 依存と信用の状態。依存、装備処分、信用超過、退店判断、破産を非描写状態として処理する。 受入: 金額と所持品を保全し、途中中断・再送・保存でも一回だけ遷移する。
|
||||
|
||||
### P16 雇用 職種 労務完成
|
||||
|
||||
現在地: 一般求人・配置・清掃等の基盤あり。全雇用区分と特性・費用は残る。
|
||||
|
||||
統合前提: P12, P15, P13。原本対応: B-5.1, B-5.2, B-5.3, B-5.4, B-5.5, B-3.3。
|
||||
|
||||
- P16-01 獲得と費用の区分。求人、市場、原本の雇用区分ごとの費用と獲得・退職を非描写状態として整理する。 受入: 求人から配置・精算・退職まで通常操作でき、ユニーク禁止操作を維持する。
|
||||
- P16-02 特性と全職種。生成時特性、体力、ストレス、忠誠、技能と全職種の仕事を接続する。 受入: 同一保存で特性が固定され、配置しても仕事内容がない職種を残さない。
|
||||
- P16-03 労務と交代。勤務、休息、給与、気絶、救護、退職時参照除去を横断検証する。 受入: 人員上限、未払い、交代中の予約・移動が整合し、消失や二重精算がない。
|
||||
|
||||
### P17 ユニーク7名 固有効果 成長
|
||||
|
||||
現在地: アンリの通常無料加入と料金補正等を部分接続。7ヒロイン全体は未完。ライはライバルとして別集計。
|
||||
|
||||
統合前提: P16, P05, P11。原本対応: B-1.5, B-6.1, B-6.2, B-6.3, B-6.4, B-6.5。
|
||||
|
||||
- P17-01 7名の加入と固有効果。アンリ、フィーネ、トマリ、アメリア、リズ、ミミ、サツキとライバルを区別し、加入と固有効果を通常営業へ接続する。 受入: 7名それぞれ配置前後・同時配置・配置変更の効果を確認し、無料・削除禁止を維持する。
|
||||
- P17-02 業務成長と技能。実業務EXP、Lv99、基礎技能、パッシブ、能動スキル、ルート別解放を接続する。 受入: 上限と取得条件、財布や台帳への補正、保存再開で効果が一致する。
|
||||
- P17-03 衣装と勤務と離脱。24時間警告、継続・中止、衣装補正、限定的離脱と復帰を通常UIへ接続する。 受入: 警告を操作でき、原本外の恒久ロストを起こさず、未設定加入条件は捏造しない。
|
||||
|
||||
### P18 関係パラメータ 盾 確定遷移
|
||||
|
||||
現在地: 原本の関係3値と盾の全面接続は未着手。
|
||||
|
||||
統合前提: P03, P17。原本対応: B-7.1, B-7.2, B-7.3。
|
||||
|
||||
- P18-01 関係モデルと保存。原本の3値と盾を専用モデルへ接続し、旧関係値を保全する。 受入: 表示と状態が一致し、意味不明な値の一対一変換を行わない。
|
||||
- P18-02 接触と境界。同配置、別配置、軽減、盾上限、盾破壊、閾値を非描写状態遷移として接続する。 受入: 閾値前後と反復更新で効果が一回だけ適用される。
|
||||
- P18-03 不可逆な分岐。原本のルート確定と旧進行の扱いを保存・履歴へつなぐ。 受入: 確定後のロードでルートが逆転せず、未決状態を勝手に固定しない。
|
||||
|
||||
### P19 イベント 決闘 行動 履歴基盤
|
||||
|
||||
現在地: 一般イベント基盤はあるが原本の決闘・人物行動の全接続は未着手。
|
||||
|
||||
統合前提: P18, P05。原本対応: B-7.1, B-7.4, B-7.5, B-7.6, B-11.3。
|
||||
|
||||
- P19-01 人物行動と決闘。原本の行動入口、3種決闘、勝敗、費用、結果を非描写イベントへ接続する。 受入: 通常入口から到達し、勝敗の盾・状態・台帳が一回だけ変化する。
|
||||
- P19-02 観察と履歴。専用観察画面、証拠ID、操作介入、対象状態、履歴を中立テスト内容でつなぐ。 受入: 対象不在、停止、取消、離脱、保存再開で参照と履歴が一致する。
|
||||
- P19-03 分岐の到達性。人物ごとの3ルートに必要なイベント・条件・中断処理を列挙する。 受入: 7名×3ルートの到達表があり、未制作内容は欠損として残る。
|
||||
|
||||
### P20 ランク 信仰 物語進行
|
||||
|
||||
現在地: ランク表示等の部分実装あり。章・エンド・図鑑の通常到達は未完。
|
||||
|
||||
統合前提: P05, P11, P15, P19。原本対応: B-1.1, B-1.2, B-1.3, B-1.4, B-1.5, B-6.4, B-6.5, B-8.1, B-8.5。
|
||||
|
||||
- P20-01 ランクと解放。信仰・評判・裏影響、ランク条件、領域、施設レベル、Policy、スキル解放を接続する。 受入: 通常営業から条件を達成でき、未達理由を表示する。
|
||||
- P20-02 章と全エンド。物語の開始、章、人物進行、最終ランク、終点を通常操作へ接続する。 受入: 条件値を直接注入せず新規から本編終点へ到達し、各エンドを保存再開できる。
|
||||
- P20-03 図鑑の完成。対象一覧、発見・登録条件、閲覧、達成状況、保存、コンプリート判定を実装する。 受入: 図鑑コンプを通常到達で検証し、本編クリアと全要素クリアを区別する。
|
||||
|
||||
### P21 防衛 監査 銀行 砲台
|
||||
|
||||
現在地: 防衛の準備・表示・結果・イベント橋渡しは部分接続。自然脅威等が残る。
|
||||
|
||||
統合前提: P05, P13, P14, P17。原本対応: B-1.3, B-6.4, B-6.5, B-9.3, B-10.7, B-11.4。
|
||||
|
||||
- P21-01 自然脅威と監査。自然営業のHeat、滞納、原本の脅威から警告と防衛準備へつなぐ。 受入: サービス直呼びだけでなく通常営業から発生し、通知と状態が一致する。
|
||||
- P21-02 砲台と能動技能。魔法砲台の建設、マナ、職員、人物の任意技能、戦闘結果を接続する。 受入: 発動可否、費用、クールダウン、勝敗、中断を保存再開まで検証する。
|
||||
- P21-03 破壊と摘発と休止。施設損傷、摘発、人物の限定離脱、台帳と営業停止を統合する。 受入: 意図しない恒久ロストや自動休止解除がなく、利用中の状態を保全する。
|
||||
|
||||
### P22 素材台帳 量産仕様の先行確定
|
||||
|
||||
現在地: 施設・人物・素材枠の初期台帳あり。必要数と完成資産の受入は未完。
|
||||
|
||||
統合前提: P01。原本対応: B-12.1, B-12.3, B-12.4, B-13.1, B-13.2, B-13.3, B-13.4, B-13.5。
|
||||
|
||||
- P22-01 必要素材の確定。全30施設のLv1〜3と表裏、7人物、ライバル、状態、背景、UI、音の必要枠を列挙する。 受入: 仮素材、コード描画、完成素材の区分と利用条件が分かり、枠数を素材枚数と混同しない。
|
||||
- P22-02 素材契約と権利。Kenneyを優先して適合素材を選び、解像度、命名、差替、出典とライセンスを記録する。 受入: 完成枠には使用可能な資産と権利記録があり、欠損を代替表示で隠さない。
|
||||
- P22-03 制作量と担当。未制作の絵、モーション、音、声、脚本を必要量と担当・予算候補へ分ける。 受入: 未割当、未承認、制作待ちが明示され、技術工数と内容制作工数を分ける。
|
||||
|
||||
### P23 タワー画面 レイヤー演出
|
||||
|
||||
現在地: ドット断面、レイヤー表示、複数UIスキンは部分実装。原本全演出の受入は残る。
|
||||
|
||||
統合前提: P08, P22。原本対応: B-2.2, B-11.1, B-11.4, B-12.1, B-12.2, B-12.4。
|
||||
|
||||
- P23-01 レイヤーと波紋。表時の裏半透明、裏時の表ワイヤー、魔法レンズ風の波紋を原本どおり接続する。 受入: 連続切替でも座標・選択・対象が一致し、描画層と操作対象を取り違えない。
|
||||
- P23-02 断面の操作性。パン、上下スクロール、ズーム、建設グリッド、接続警告、HUD配置を整える。 受入: 小画面とWindows表示倍率で読めて押せる。x3時にも操作が追随する。
|
||||
- P23-03 生活の観察。部屋、行列、EV、職員、物資、営業状態が断面上で理解できる表示へ調整する。 受入: 人物や設備の表示が実状態に対応し、同需要の動線改善を観察できる。
|
||||
|
||||
### P24 インスペクター 通知 操作完成
|
||||
|
||||
現在地: ドラッグ配置、選択復元、営業人数、料金未設定表示等は部分採用。
|
||||
|
||||
統合前提: P09, P15, P16, P18, P23, P19。原本対応: B-11.1, B-11.2, B-11.3, B-11.4, B-13.4。
|
||||
|
||||
- P24-01 全インスペクター。欲求、思考、感情、盾、料金内訳、稼働理由、上限、人物効果を通常画面へつなぐ。 受入: 表示値が実計算・台帳と一致し、未設定をゼロ確定に見せない。
|
||||
- P24-02 操作と通知。配置、無効ドロップ、モーダル、停止、再開、フキダシ、ログ、カットインを整える。 受入: 無効操作を説明し、クリック代替と日本語入力・表示倍率を確認する。
|
||||
- P24-03 選択と保存の受入。施設・客・職員・レイヤーの選択を撤去、離脱、保存再起動に跨がせる。 受入: 不在対象を参照せず、有効な選択と画面値が復元される。
|
||||
|
||||
### P25 ドット人物 施設差分 音響接続
|
||||
|
||||
現在地: 歩行・清掃等の一部モーションとコード描画あり。全差分・音響は未受入。
|
||||
|
||||
統合前提: P22, P23, P12, P17。原本対応: B-12.1, B-12.3, B-12.4, B-13.2, B-13.3, B-13.4, B-13.5。
|
||||
|
||||
- P25-01 実状態に沿うドット動作。歩行、接客、食事、搬送、救護、清掃、休息、気絶と設備動作をそろえる。 受入: 停止中に仕事が進まず、人物位置と搬送・作業状態が一致する。
|
||||
- P25-02 施設と人物の差分。施設Lv、表裏、人物状態、破損・汚れ等の必要差分を完成素材へ置換する。 受入: 全枠の読込、見切れ、欠損、状態変化とライセンスを確認する。
|
||||
- P25-03 音の接続。表裏BGMクロスフェード、環境音、操作音、必要な声を音量設定へ接続する。 受入: 切替、停止、ロードで二重再生せず、Windows出力と設定保存を確認する。
|
||||
|
||||
### P26 シナリオ 立ち絵 内容資産の制作受入
|
||||
|
||||
現在地: 全章・人物別内容と完成資産の制作受入は未着手。
|
||||
|
||||
統合前提: P19, P20, P22。原本対応: B-1.1, B-1.5, B-1.6, B-7.5, B-7.6, B-12.1, B-12.3, B-13.1, B-13.5。
|
||||
|
||||
- P26-01 全必要内容の台帳。全章、7名の3ルート、全エンド、台詞、立ち絵、音声の必要枠と条件を確定する。 受入: 原本で要求された未制作内容を欠損として記録し、架空の完成数を置かない。
|
||||
- P26-02 内容と状態の接続。制作可能な非露骨内容をイベント・章・閲覧・人物状態へ接続し、仮素材を区別する。 受入: 全必要分岐に内容IDがあり、欠損と未制作をスキップPASSしない。
|
||||
- P26-03 制作受入と差異。完成内容の品質、権利、表示、保存と原本との差異を受入する。 受入: 原本必須内容の未制作が残る場合は原本全実装未完として報告する。
|
||||
|
||||
### P27 統合 境界 障害復旧試験
|
||||
|
||||
現在地: 最新全回帰合格。原本75群全体の受入と通しデバッグは未完。
|
||||
|
||||
統合前提: P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21, P24, P25, P26。原本対応: B-1.1, B-1.3, B-14.4。
|
||||
|
||||
- P27-01 要件別の合否。原子台帳へ部品、通常Runtime、画面、保存、資産の証拠を結び付ける。 受入: 全必須要件の判定があり、未設定や欠損を試験省略でPASSにしない。
|
||||
- P27-02 横断シナリオ。開業、決算、天候、停電、ラッシュ、救護、破産、防衛、人物、ランクを横断する。 受入: 中断と保存再起動を挟んでも財布・台帳・人員・参照が一致する。
|
||||
- P27-03 通しデバッグの完了。要件別全件合格後に新規から本編・全要素の通し確認を行い、欠陥を再現ケースで修正する。 受入: 重大不具合0、未解決差異0。修正後の関連回帰と影響した通し経路を再確認する。
|
||||
|
||||
### P28 性能 操作感 長期バランス
|
||||
|
||||
現在地: 複数の軽量化は採用済み。Runtime全体の性能予算は未達。
|
||||
|
||||
統合前提: P27。原本対応: B-1.1, B-3.1a, B-4.3a, B-4.3c, B-11.1, B-12.1, B-14.4。
|
||||
|
||||
- P28-01 現HEADの性能基準。実Runtimeの20,000マス・200客・50職員・150施設を基準にTick、経路、snapshot、描画、保存を測る。 受入: 機種、ビルド、シード、速度、測定区間、平均・P95・最大を残す。
|
||||
- P28-02 意味を保つ軽量化。失敗時拒否を含む同等性を先に固定し、snapshot変換・検証・コピー・経路接続の実測上位から改善する。 受入: 同条件の反復比較と全状態・全イベント一致を確認し、キャッシュの古さ・所有権・不正入力を検証する。
|
||||
- P28-03 操作感と長期バランス。Windows描画、初見操作、ラッシュ、長期収支、本編20〜30時間・全要素50時間以上を実プレイで確認する。 受入: 性能予算と対象Windows描画目標を満たし、時間の早送りだけで操作感やプレイ時間を合格にしない。
|
||||
|
||||
### P29 Windows実機 配布版 最終監査
|
||||
|
||||
現在地: 旧1394版Windows配布物のZIP・PCK検査とmacOSでのPCK実行は合格。現1396版のWindows実機受入は未完。
|
||||
|
||||
統合前提: P28。原本対応: B-1.1, B-14.1, B-14.2, B-14.3, B-14.4。
|
||||
|
||||
- P29-01 最新候補の書き出し。採用済みソースだけからWindows EXE/PCKを再生成し、同梱データとZIPを確認する。 受入: ソース・PCK・EXE・ZIPのハッシュ、同梱マニフェスト、CRCが対応する。
|
||||
- P29-02 Windows同一版の操作。日本語パス、新規、建設、営業、表示倍率、音、入力、保存終了再起動を実EXEで確認する。 受入: macOS PCK実行と区別し、同一Windows版の操作記録・ログ・画面・性能を残す。
|
||||
- P29-03 最終監査と引渡し。原本75群、追加10項目、全施設、人物、資産、図鑑、全エンドを再監査して配布証拠をまとめる。 受入: 同じリリース候補で必須未実装0・未承認差異0・未制作0・重大不具合0。工程4件の過去納期達成は別判定。
|
||||
|
||||
## 原本施設の残件
|
||||
|
||||
未定義13施設はVIPスイート、ドミトリー、レストラン、屋台、カジノ、闘技場、高級ソープ、SM拷問室、覗き部屋、地下牢、マナ炉、ワープ、魔法砲台。定義のみの闇市場は通常登録と運用を追加する。原本外の通路等7件を不足施設の代替として数えない。
|
||||
|
||||
各施設を「原本の位置と寸法確認 → 定義と必要値 → 通常パレット → 稼働と依存 → 職員と客の実行 → 会計 → 途中保存 → 境界 → 実描画 → Windows」の順で閉じる。機能未完成のまま施設登録数だけを増やさない。既存16施設も同じ受入表で不足を確認する。
|
||||
|
||||
## 人物と進行の受入
|
||||
|
||||
ヒロインはアンリ、フィーネ、トマリ、アメリア、リズ、ミミ、サツキの7名。ライ、リーゼンバルト、ソウ等のライバルは別集合とし、アンリとライの加入を2/7ヒロイン実装と数えない。
|
||||
|
||||
7名×3ルートの到達表をP19とP26で持つ。これは21組の到達性を調べる義務であり、21本の完成脚本が存在するという意味ではない。固有効果、EXP、Lv99、衣装、24時間警告、技能、限定離脱を人物別に検証する。
|
||||
|
||||
物語と図鑑は通常プレイの解放・取得・閲覧・保存を検証する。値を直接書き換えた条件API試験は部品試験として残し、自然な本編到達の証拠として使わない。未制作の必須内容や制作できない表現は原本との差異として残し、全実装の完了に数えない。
|
||||
|
||||
## 検証と通しデバッグ
|
||||
|
||||
| 検証段階 | 確認する内容 | 保存する証拠 |
|
||||
|---|---|---|
|
||||
| 仕様とデータ | 原本条件、全施設、値、属性、素材枠、未確定 | 原子台帳と出典、データ検査、差異一覧 |
|
||||
| 部品と境界 | 金額、人数、状態、更新順、経路、効果、異常時原状保持 | 入力、期待、実値、失敗を含むログ |
|
||||
| 通常Runtime | 自然来客、勤務、物流、清掃、課金、決算、防衛 | シード、実Tick、状態、イベント、台帳 |
|
||||
| 保存と移行 | 旧版、未知版、破損、途中処理、別プロセス再開 | 元データの保全、前後比較、再実行手順 |
|
||||
| 実描画と入力 | パン、ズーム、層、配置、選択、説明、画面サイズ | 操作列、画面、対象IDと実状態 |
|
||||
| 配布版 | 同梱データ、PCKとEXE、ZIP、Windows操作 | ハッシュ、マニフェスト、実機ログ |
|
||||
| 長期と全要素 | 本編、全エンド、全人物、図鑑、経済、性能 | 到達表、実プレイ記録、不具合と再確認 |
|
||||
|
||||
横断シナリオは、A新規から3営業日決算、B雨・嵐・霧と復帰、C停電とEV・救護・物流、D昼夜交代とラッシュ・休息、E関連施設停止・破壊・摘発、F欲求・価格・行列・人数、G返済滞納と襲撃結果、Hランク解放と全施設、I7人物と全分岐、J旧版移行と配布、K図鑑と全要素完了とする。各重要地点で保存、終了、再起動を挟む。
|
||||
|
||||
各変更は関連試験と必要な境界から始め、統合点で標準全体回帰を行う。全要件別受入がそろった段階で通しデバッグを開始し、重大不具合と未解決差異を解消して完了する。修正後は該当回帰と影響を受けた通し経路を再確認し、ソースが変わっていない検証を理由なく全件反復しない。
|
||||
|
||||
## 性能の基準
|
||||
|
||||
| 対象 | 継続する予算 |
|
||||
|---|---:|
|
||||
| simulation x1 平均 | 4ms以下 |
|
||||
| simulation x3 P95 | 12ms以下 |
|
||||
| path query P95 | 2ms以下 |
|
||||
| 主スレッド停滞 最大 | 50ms以下 |
|
||||
| 保存 最大 | 5秒以下 |
|
||||
| 読込 最大 | 8秒以下 |
|
||||
|
||||
代表負荷は20,000マス、顧客200、従業員50、施設150とし、行列・物流・イベントも付与する。原本の最終到達規模がこれを超えるなら負荷条件を追加する。単独モデルの小規模成功を実RuntimeやGPUの成功としない。
|
||||
|
||||
9月30日に計測したPath候補の小規模4反復値はx1平均9.575ms、x3 P95 45.541ms、最大停滞67.434msであり、予算未達である。これは現1396HEADを新測定した値ではない。P28で最新版を同条件と混合負荷の両方で測り直す。Windows描画目標はD10で別に定め、シミュレーション予算をFPS保証としない。
|
||||
|
||||
snapshotの検証・変換・コピー、経路接続、更新回数を実測上位から改善する。同じ入力、乱数、全状態、全イベントの比較、正常・異常入力、所有権、キャッシュ失効を先に確認する。メモリは反復ロードと長時間運転で持続増加を調べる。
|
||||
|
||||
## 工数と日程と担当
|
||||
|
||||
旧計画の180〜390人日は9月29日時点の全体粗見積であり、現在の残工数へ転用しない。既存成果、未検証部分、脚本・絵・音・声の必要量をP01とP22で確定した後、残作業ごとに実績から見積もる。技術作業、内容制作、外部待ち、実プレイ20〜30時間と50時間以上を分離する。
|
||||
|
||||
固定発売日は本改訂では置かない。原本の2027年12月マスターアップは比較する目標として保持し、達成可能と断定しない。段階0終了時に初回残工数、段階2終了時に人物・素材・進行の再見積、段階4終了時に通しデバッグとWindowsの見通しを更新する。実装単位の完了ごとに状態と証拠を更新し、段階終了時に計画と割合を再集計する。
|
||||
|
||||
実装担当はコード・データ・通常操作・保存の証拠を作成する。受入担当は原本条件と証拠を確認する。素材とシナリオの制作担当は必要量とともに割り当てる。本計画作成時の別担当による読み取り監査は実施済みだが、今後すべての制作や実機試験に独立担当が確保されたとは扱わない。
|
||||
|
||||
## 最終完了の判定
|
||||
|
||||
同じリリース候補について、必須未実装0、未承認の原本差異0、現仕様の必須値未設定0、未制作必須資産0、重大不具合0、原子要件の受入証拠欠損0を満たす。明示的に未設定の追加拡張仕様は、その理由を残したまま現仕様の適用範囲として扱う。
|
||||
|
||||
通常新規ゲームから本編終点、全エンド、全ヒロイン、図鑑コンプへ到達し、指定プレイ時間、性能、保存移行、音、画面、Windows同一版を受け入れる。内容を変更して差異を解消する場合は承認履歴を残し、原本準拠と改訂仕様準拠を区別する。未達が残る限り目標全体を完了にしない。
|
||||
|
||||
## 計画の資料と更新方法
|
||||
|
||||
- [基準と静的照合](baseline.json): 原本、採用ソース、証拠、施設数、割合の未算出状態。
|
||||
- [作業台帳](work_packages.json): 全30パッケージ、現在地、残作業単位、受入、依存、旧進捗。
|
||||
- [原本対応表](requirement_coverage.json): 全75群と残作業単位の対応。現在の原子合否は再監査前。
|
||||
- [割合の集計契約](progress_contract.json): 分母固定前は未算出とし、将来の原子台帳に必要な項目を定義。
|
||||
- [承認と未確定事項](decisions.json): 再確認不要の回答と局所的な未確定。
|
||||
- [計画検証結果](plan_validation.json): 対応漏れ、ID、依存循環、資料ハッシュを検査した結果。
|
||||
- [改訂とレビュー](REVIEW.md): 前計画からの修正と残る不確実性。
|
||||
|
||||
変更時は `python3 docs/plans/original-completion-2026-10-02/validate_plan.py` を実行して計画を再検証する。通常検査は着手・保留・証拠付き完了への更新を認め、初期状態の履歴を保持する。初期版を再確認するときだけ `--initial-snapshot --verify-baseline` を付ける。後者は履歴時点のソースを比較するため、将来の本体変更で不一致が出た場合は旧合格の適用範囲を再評価する。この検査はゲームを実行せず、計画の整合性だけを確認する。
|
||||
|
||||
## 全75群の作業対応
|
||||
|
||||
以下は原本の網羅性を確認するための対応表である。9月29日の群別状態を現在の全受入として再利用していない。機能と内容の現在判定はP01で原子要件へ分解して更新する。
|
||||
|
||||
| 原本ID | 要件 | 担当パッケージ |
|
||||
|---|---|---|
|
||||
| B-1.1 | 基本情報・Windows・2D・20〜30時間/50時間以上 | P00, P01, P20, P26, P27, P28, P29 |
|
||||
| B-1.2 | 地上/地下の二面性経営・観察介入・権威/富 | P01, P20 |
|
||||
| B-1.3 | 建設→集客→収益→管理→拡張防衛 | P01, P04, P20, P21, P27 |
|
||||
| B-1.4 | 二層・自律AI・渋滞・信仰と依存 | P01, P04, P20 |
|
||||
| B-1.5 | 世界観・主人公・7ヒロイン3運命 | P01, P17, P20, P26 |
|
||||
| B-1.6 | 成人向けルートと経営的搾取 | P01, P13, P26 |
|
||||
| B-2.1 | グリッド・Y正負・支持・領域拡張 | P03, P08 |
|
||||
| B-2.2 | 同座標Front/Back・半透明/ワイヤー表示 | P23 |
|
||||
| B-2.3 | 客エリアと景観/清潔/騒音の満足度効果 | P07 |
|
||||
| B-2.4 | バックヤード・職員ドア・客通行禁止 | P12, P14 |
|
||||
| B-2.5 | 覗き部屋の配置制約・通路寸断・証拠収集 | P13 |
|
||||
| B-2.6 | 地上/地下の採光・マナ・遮音・隠蔽ボーナス | P06, P07 |
|
||||
| B-3.1a | 10秒=1時間、Pause/x1/x2/x3 | P02, P28 |
|
||||
| B-3.1b | アクティブポーズ中も建設・指示 | P02 |
|
||||
| B-3.2 | 4時間帯と朝下り/夕上り/夜行動 | P04, P07, P15 |
|
||||
| B-3.3 | 昼夜シフト・体力0気絶・救護・通勤混雑 | P02, P12, P16 |
|
||||
| B-3.4a | 毎日00:00に決算レポート | P02, P05 |
|
||||
| B-3.4b | 3回連続赤字でゲームオーバー | P02, P05 |
|
||||
| B-3.5 | 雨・嵐・マナの霧・流行ニュース | P07 |
|
||||
| B-4.1 | 所持金/戦利品・四欲求・忍耐/清潔/依存 | P03, P15 |
|
||||
| B-4.2 | 最大欲求優先と距離/行列/価格選択 | P02, P15 |
|
||||
| B-4.3a | A*とエレベーター待ち時間コスト | P14, P28 |
|
||||
| B-4.3b | 4種垂直移動のゲーム内利用 | P14 |
|
||||
| B-4.3c | 重なり時速度50%低下 | P02, P14, P28 |
|
||||
| B-4.4 | 依存Lv3退店拒否・装備売却・性別別破産 | P15 |
|
||||
| B-4.5 | 貴族/聖職者/エルフ/魔族のAI特性 | P07, P15 |
|
||||
| B-5.1 | 一般雇用と奴隷の費用/耐性/売却区別 | P16 |
|
||||
| B-5.2 | 体力/ストレス/忠誠/技能/ランダム特性 | P03, P16 |
|
||||
| B-5.3 | 接客/裏方/救護/防衛等の職種 | P12, P16 |
|
||||
| B-5.4 | シフト休息と気絶回収 | P02, P12, P16 |
|
||||
| B-5.5 | 求人/市場/破産捕獲・退職金/売買価格 | P16 |
|
||||
| B-6.1 | ユニーク無料・高成長・解雇売却不可 | P17 |
|
||||
| B-6.2 | 7ヒロインの指定固有効果とライバル | P17 |
|
||||
| B-6.3 | 基礎技能と業務EXP・Lv99 | P03, P17 |
|
||||
| B-6.4 | 配置パッシブ・任意発動・ルート別スキル | P17, P20, P21 |
|
||||
| B-6.5 | 24時間勤務警告・衣装補正・限定離脱 | P17, P20, P21 |
|
||||
| B-7.1 | 三感情・盾・決闘の3ルート運命 | P18, P19 |
|
||||
| B-7.2 | 好感/淫乱/NTR・好感度=盾最大・淫乱軽減 | P03, P18 |
|
||||
| B-7.3 | 同配置接触→盾破壊→NTR上昇→不可逆陥落 | P18 |
|
||||
| B-7.4 | 3種決闘・勝利追放/隷属/盾回復・敗北確定 | P19 |
|
||||
| B-7.5 | 会話/デート/隔離・覗き・薬による各ルート | P19, P26 |
|
||||
| B-7.6 | 履歴・妊娠/父親・刻印立ち絵 | P19, P26 |
|
||||
| B-8.1 | 隣接/通路必須とLv拡張 | P03, P08, P09, P20 |
|
||||
| B-8.2 | 表層全施設・飲食と厨房セット | P09, P10, P11, P13 |
|
||||
| B-8.3 | 裏層全施設と特殊設備 | P09, P12, P13 |
|
||||
| B-8.4 | EV/階段/ワープ/通路/職員ドア | P14 |
|
||||
| B-8.5 | Lv解放の価格/食材/混浴/ハード/イカサマ設定 | P09, P20 |
|
||||
| B-9.1 | 表金/裏金・マナ全館停止・資材 | P03, P05, P06 |
|
||||
| B-9.2 | 全収益/人件費/維持/仕入/借金/ランク税 | P04, P05, P09 |
|
||||
| B-9.3 | 受付/金庫/購買/闇銀行 | P05, P21 |
|
||||
| B-9.4 | 依存客の娯楽優先・信用超過の破産拘束 | P15 |
|
||||
| B-10.1 | 宿泊施設の寸法/層/ゾーン | P08, P10 |
|
||||
| B-10.2 | 飲食施設の寸法/層/ゾーン | P08, P10 |
|
||||
| B-10.3 | 衛生/娯楽施設の寸法/層/ゾーン | P08, P11 |
|
||||
| B-10.4 | アングラ施設の寸法/層/ゾーン | P08, P13 |
|
||||
| B-10.5 | バックヤード施設の寸法/層/ゾーン | P08, P12 |
|
||||
| B-10.6 | 特殊施設の寸法/層/ゾーン | P06, P08, P13 |
|
||||
| B-10.7 | インフラ/防衛施設の寸法/層/ゾーン | P06, P08, P14, P21 |
|
||||
| B-11.1 | 資源/時間/天候/Rank・下部切替・スクロールズーム | P07, P23, P24, P28 |
|
||||
| B-11.2 | Policy/ドラッグ配置・欲求/思考/感情グラフ/盾 | P24 |
|
||||
| B-11.3 | カメラ入口・覗きアングル/撮影/介入 | P19, P24 |
|
||||
| B-11.4 | グリッド/接続警告・フキダシ/ログ・カットイン | P21, P23, P24 |
|
||||
| B-12.1 | 二面性・高精細ドットとアニメ立ち絵 | P22, P23, P25, P26, P28 |
|
||||
| B-12.2 | 魔法レンズ風の波紋切替 | P23 |
|
||||
| B-12.3 | SD各モーション・立ち絵破損/汚れ/刻印差分 | P22, P25, P26 |
|
||||
| B-12.4 | 表裏UIスキン・BGMクロスフェード・環境音/SE | P22, P23, P25 |
|
||||
| B-13.1 | 立ち絵/CG/差分 | P22, P26 |
|
||||
| B-13.2 | SDモーション/エフェクト | P22, P25 |
|
||||
| B-13.3 | Lv1〜3表裏タイル/背景 | P22, P25 |
|
||||
| B-13.4 | 表裏スキン/感情/覗き素材 | P22, P24, P25 |
|
||||
| B-13.5 | BGM/環境/操作/成人向けSE/声 | P22, P25, P26 |
|
||||
| B-14.1 | 2026/03〜08 プロトタイプ | P00, P01, P29 |
|
||||
| B-14.2 | 2026/09〜2027/02 アルファ | P00, P01, P29 |
|
||||
| B-14.3 | 2027/03〜08 ベータ/成人向け | P00, P01, P29 |
|
||||
| B-14.4 | 2027/09〜12 ポリッシュ | P00, P01, P27, P28, P29 |
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"copied_before_first_progress_update_at_jst": "2026-10-02T01:41:59.589881+09:00",
|
||||
"scope": "Initial planning data retained before mutable progress integration; no new game or source evidence.",
|
||||
"initial_package_counts": {
|
||||
"completed": 1,
|
||||
"in_progress": 25,
|
||||
"planned": 4
|
||||
},
|
||||
"initial_delivery_count": 88,
|
||||
"sha256": {
|
||||
"baseline.json": "750bb3d313cc5e2ce0d4c79e563a995abd7a78f991dee1dd75c627877b02cd7b",
|
||||
"execution/IMPLEMENTATION_PLAN.initial.md": "f9cb3f61e4cb020e37f792c615dfbd4794763884a8ec528de14c3469cf4e5248",
|
||||
"execution/work_packages.initial.json": "c4bd86eb4b0b4b8ff93cfd9e1c06de376dd0039a68db26a9f104b3fc0632a511",
|
||||
"execution/progress_contract.initial.json": "5e67310e29c7fe91b984a5b510619f3505c2b31711253850787f164a73758151"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
# P03 保存プリフライト修正の独立レビュー
|
||||
|
||||
2026-10-02。本体の変更・Godotの追加実行は行わず、save_service / simulation_snapshot / Runtime previewの差分、test_save_preflight、焦点7本485チェックのログを監査した。
|
||||
|
||||
新しい`inspect_snapshot`と寸法previewの接続、manifest/clock envelopeの厳格型検証、既知サイズの読取前制限について、差分が原因の重大不具合は見つからなかった。ただし**既存の復旧経路にP01/P03受入を妨げる原状保持の不足2件**がある。485チェックの合格だけで未知版や全APIの読取専用保証を受け入れられない。
|
||||
|
||||
| 判定 | 対象 | 結果 |
|
||||
|---|---|---|
|
||||
| 確認済み | inspect_snapshot / 寸法preview | 通常loadのtransaction reconcileを通らず正式packageを読む。formalなしでもtmp/bakへ昇格しない。previewは隔離Runtimeで検証し、本Runtimeと全ディスクSHAを保つ試験がある。計測カウンタの更新は発生するため全メモリが不変という意味ではない。 |
|
||||
| 確認済み | manifest型 | versionはunsigned範囲の有限整数数値だけを許可。文字列・bool・分数・nullを拒否。JSON由来の厳密な整数floatは許可する。checksumは64桁hex文字列、重複sizeは存在する場合に正の整数かつheaderと一致が必須。 |
|
||||
| 確認済み | clock envelope型 | schema/minute/speed/versionのcoerceを避け、present pausedはbool必須。省略paused/versionのlegacy defaultとx4→x3互換を維持。generic envelopeの整数float互換であり、現Runtimeのbinary schemaは従来通りstrict TYPE_INTを要求する。 |
|
||||
| 確認済み | サイズ | worldの圧縮payload/展開予定値は各256 MiB以下・正数、manifestは4 MiB以下としてbulk read/decompress前に検査する。writerも出力blob/manifestを拒否して正式保存を保持する。 |
|
||||
| 不足 | 未知版+transaction artifact | 下記SFR-01。正式保存の版を識別する前にreconcileして未知版を古いbakへ置換し得る。 |
|
||||
| 不足 | 明示backup選択のread_only | 下記SFR-02。候補選択前のformal reconcileでtmp/marker等が変化し得る。 |
|
||||
|
||||
## SFR-01: 未知の正式保存を旧backupで置換する [P1]
|
||||
|
||||
`save_service.gd:932`の`_reconcile_interrupted_formal_save`は、`target_status.current_loadable == false`のとき、未知形式と壊れた現形式を区別していない。formalが存在し有効transaction markerがあると、現版tmp/bakを選び、formalを削除して置換する(同関数のinvalid formal replacement分岐)。`load_snapshot`、`migrate_save`、`inspect_recovery_candidates`、`load_with_recovery`、明示`load_recovery_candidate`がこの経路を先に通る。
|
||||
|
||||
再現条件は、正式保存のmanifestとTTWB headerをformat2へ変え、圧縮payload/checksumを保ったまま、現format1のbak/tmpと有効`.save_transaction`を残すこと。通常loadが正式保存を古い状態へ置換して成功する静的経路を確認した。checksumを破損したfuture版、manifestだけfuture、headerだけfutureでも、識別できる未知版を破損現版として消してよい根拠にはならない。
|
||||
|
||||
現128チェックの未知版試験は`inspect_snapshot`での拒否と、transaction markerなしでの未登録migrate拒否を確認する。上の複合条件を覆っていない。レビューではGodotを実行していないので、実再現の結果は未取得と明示する。
|
||||
|
||||
## SFR-02: 明示候補読取のread_onlyと実処理が不一致 [P2]
|
||||
|
||||
`save_service.gd:313〜336`は明示候補読取をread-onlyと説明し、結果にも`read_only=true`を付けるが、候補pathの選択前に`_reconcile_interrupted_formal_save`を呼ぶ。
|
||||
|
||||
formalが正常、有効markerとtmpがある場合、backupを選ぶだけでもtmp/markerを削除する。formalが欠損しtmpがある場合、backupを読むだけでもtmpが正式名へ昇格する。明示候補読取は指定packageだけを検証し、通常load/通常recoveryが復旧を所有する契約へ分ける必要がある。
|
||||
|
||||
## 候補と受入の範囲
|
||||
|
||||
`artifacts/original-completion-2026-10-02/p03-recovery-readonly-candidate/combined.diff`、完全候補、原因別diff、fixture driver、receiptを作成した。本体変更0、Godot実行0で、候補は未採用・未検証である。rootが既存全回帰の終了後に別stageで、元本体の実再現→候補修正後試験→保存/復旧/移行の関連回帰を行う。
|
||||
|
||||
候補はbounded manifestと固定TTWB prefixのいずれでも未知版を先に識別し、checksum成功を前提にしない。通常load/recoveryの未知版は全artifactを変えず拒否する。migrateは明示登録された過去版→現版の全隣接stepだけを認め、その場合もsourceの検証前に既存artifactを整理しない。既知現版の壊れたtransaction復旧は維持する。明示候補読取からreconcileを除く。
|
||||
|
||||
`inspect_recovery_candidates`は既存の説明通り通常復旧を含む一覧APIであり、全体をread-onlyとは呼ばない。formalが未知と識別できた場合の不変拒否を先行する。
|
||||
|
||||
485ログは7本すべてexit0・実チェック数とcompletion markerが一致し、4修正/試験ファイルSHAは現在と一致した。source-startの318パスは317一致で、差分は今回焦点外の`tests/performance/test_digest_performance.gd`だけだった。初回失敗ログも保存されており、後続r02と混同しない。全157回帰の終了結果はこのレビューに含めていない。
|
||||
|
||||
サイズ上限はserialized world blobの制限であり、総ピークメモリ256 MiB以下の証明ではない。256 MiB一致の試験はmanifest/header不一致でdecompress前に拒否し、巨大な正常full Runtimeを実際に展開した受入ではない。20,000セルはworld-onlyのサイズ測定で、full Runtime受入ではない。これらはP28の性能/規模受入として引き続き未検証にする。
|
||||
|
||||
## 既存移行・復旧契約との照合
|
||||
|
||||
`test_save_operations.gd`の349〜450行と`test_runtime_save_lifecycle.gd`の170〜200/230〜380行を候補と照合した。以下は静的整合の確認で、候補の再実行合格ではない。
|
||||
|
||||
| 既存契約 | 候補との整合 |
|
||||
|---|---|
|
||||
| registryの1→2→3隣接step・入力コピー・欠損step監査 | registry自体を変更しないため維持。 |
|
||||
| current formatだが不正snapshotの通常load/no-op migrate拒否 | guardは現formatを通し、既存schema検証へ到達するため維持。 |
|
||||
| 明示0→1 package移行、step ID、manifest注記、完全pre_migration.bak | 過去版→現版の全step確認後に既存移行本体へ入る。callbackとbackup/promotion処理は変更しない。 |
|
||||
| 登録済みstepが途中失敗したときformalのworld/manifest不変、backup未作成 | sourceのdecode/registry成功前にreconcileを実行しないため、stale普通saveのtmp/bak/markerも不変になる。追加driverでこの複合条件を要求する。 |
|
||||
| 移行後のtorn pre_migration.bakをdurable .oldから修復 | formalが現formatになった状態では従来のinspect/reconcile枝を維持。 |
|
||||
| `.migration.original`のhold、完成candidateのpromotion、candidateなしrollback | formal欠損では未知版guardに識別値がなく、従来のmigration reconcileへ進む。現format持ちの既存fixtureを維持。 |
|
||||
| 壊れたcurrent formalからbackup/autosaveへ通常復旧 | current版識別は通すため従来の復旧を維持。識別可能な未知formalだけをfallbackの対象から外す。 |
|
||||
| 明示backup/autosave選択がpackageを昇格しない | 候補選択にreconcileが不要。formalなしのbak選択は成功でき、tmp/markerは通常loadに残す。 |
|
||||
|
||||
登録0→1の成功時は、formalと専用migration artifactだけを既存手順で更新する。stale普通saveのtmp/bak/markerは前処理で片付けず保持する。次の通常loadは新しい現formalを優先し、marker/tmpの既存cleanupを実行できる。これを未知formalの拒否や明示backupの純読取と混同しない。
|
||||
|
||||
追加driverは8入口×5未知形、formal有無のbackup選択、既知現版破損復旧、登録legacy成功の普通artifact保持、登録legacy失敗の全SHA保持を含む。rootによる別stageでの実再現・候補検証待ちである。
|
||||
+3700
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,100 @@
|
||||
"""Bind atomic conditions to current static leads without promoting them to PASS.
|
||||
|
||||
Old group evidence supplies search leads only. Presence and unchanged bytes are
|
||||
not proof of normal operation, complete content, or accepted release behavior.
|
||||
"""
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
PLAN = ROOT / "docs/plans/original-completion-2026-10-02"
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None
|
||||
|
||||
|
||||
def main():
|
||||
atomic = json.loads((PLAN / "atomic_requirements.json").read_text())
|
||||
groups = {r["id"]: r for r in json.loads((ROOT / "docs/traceability/original/requirements.json").read_text())}
|
||||
baseline = json.loads((ROOT / "artifacts/facility-need-pricing-production-scale-adoption-2026-10-01-r01/after.json").read_text())
|
||||
definitions = {}
|
||||
for path in sorted((ROOT / "data/facilities").rglob("*.tres")):
|
||||
match = re.search(r'^id = &"([^"]+)"$', path.read_text(), re.MULTILINE)
|
||||
if match:
|
||||
definitions[match.group(1)] = path
|
||||
catalog = (ROOT / "scripts/data/facility_catalog.gd").read_text()
|
||||
characters = {r["id"] for r in json.loads((ROOT / "data/characters/mvp_unique_characters.json").read_text())["entries"]}
|
||||
locators = {}
|
||||
rows = []
|
||||
|
||||
def locate(name, symbol=None):
|
||||
key = (name, symbol)
|
||||
if key not in locators:
|
||||
path = ROOT / name
|
||||
entry = {"path": name, "exists": path.is_file(), "sha256": sha(path),
|
||||
"matches_retained_baseline": sha(path) == baseline.get(name) if name in baseline else None,
|
||||
"semantic_acceptance": "not_verified"}
|
||||
if symbol and path.is_file():
|
||||
lines = [i for i, line in enumerate(path.read_text().splitlines(), 1) if symbol in line]
|
||||
entry.update(symbol=symbol, current_matching_lines=lines, symbol_present=bool(lines))
|
||||
locators[key] = entry
|
||||
return locators[key]
|
||||
|
||||
for requirement in atomic["requirements"]:
|
||||
source_refs, test_refs = {}, set()
|
||||
for group_id in requirement["source_group_ids"]:
|
||||
group = groups[group_id]
|
||||
for ref in group.get("source_evidence", []):
|
||||
source_refs[(ref["path"], ref.get("symbol"))] = locate(ref["path"], ref.get("symbol"))
|
||||
test_refs.update(group.get("related_component_tests", []))
|
||||
item = {"requirement_id": requirement["id"], "source_group_ids": requirement["source_group_ids"],
|
||||
"primary_package": requirement["primary_package"], "planned_normal_entry": requirement["normal_entry"],
|
||||
"normal_entry_verified": False, "static_source_leads": list(source_refs.values()),
|
||||
"component_test_leads": [locate(t) for t in sorted(test_refs)],
|
||||
"save_contract_leads": [locate("scripts/core/save_service.gd"), locate("scripts/core/simulation_snapshot.gd"), locate("autoload/simulation_runtime.gd")],
|
||||
"boundary_evidence": "group tests require condition-specific review",
|
||||
"implementation_state": "not_reaudited_at_all_required_views", "acceptance_state": "unverified",
|
||||
"reviewer": "root static provenance audit"}
|
||||
if requirement["id"].startswith("AR.facility."):
|
||||
facility_id = requirement["id"].split(".")[2]
|
||||
path = definitions.get(facility_id)
|
||||
if path:
|
||||
name = str(path.relative_to(ROOT))
|
||||
item["definition"] = dict(locate(name), catalog_literal_present=("res://" + name) in catalog,
|
||||
source_value_approval="requires condition-specific review")
|
||||
field = requirement["id"].split(".")[3]
|
||||
aliases = {"footprint": "footprint", "layer": "layer", "staff_min": "staff_min", "staff_max": "staff_max",
|
||||
"customer_capacity": "customer_capacity", "service_attribute": "service_attribute", "service_price": "base_price",
|
||||
"upkeep_gold": "maintenance_gold", "upkeep_mana": "maintenance_mana"}
|
||||
if field in aliases:
|
||||
prefix = aliases[field] + " = "
|
||||
values = [(i, line[len(prefix):]) for i, line in enumerate(path.read_text().splitlines(), 1) if line.startswith(prefix)]
|
||||
item["implementation_field"] = {"field": aliases[field], "explicit_lines": values,
|
||||
"state": "explicit_implementation_not_yet_source_approved" if values else "inherited_default_not_explicit_approval"}
|
||||
else:
|
||||
item["definition"] = {"state": "missing_definition", "canonical_audit_id": facility_id}
|
||||
if requirement["id"].startswith("AR.heroine."):
|
||||
person_id = "heroine_" + requirement["id"].split(".")[2]
|
||||
item["character_catalog"] = dict(locate("data/characters/mvp_unique_characters.json"),
|
||||
canonical_audit_id=person_id, entry_present=person_id in characters,
|
||||
effect_or_route_verified=False)
|
||||
if requirement["id"].startswith(("AR.asset.", "AR.art.")):
|
||||
item["finished_asset_acceptance"] = {"state": "missing_acceptance", "audit": "assets_platform_audit.json",
|
||||
"fallback_counts_as_complete": False}
|
||||
rows.append(item)
|
||||
report = {"scope": "current static bindings and missing definitions; no group-to-atom PASS promotion",
|
||||
"audited_at_jst": datetime.now(ZoneInfo("Asia/Tokyo")).isoformat(), "atomic_register_sha256": sha(PLAN / "atomic_requirements.json"),
|
||||
"known_condition_denominator": len(atomic["denominator_ids"]), "rows": rows,
|
||||
"unique_source_locators": len(locators), "whole_atom_acceptance_claimed": False,
|
||||
"complete_runtime_ui_save_review": False}
|
||||
(PLAN / "current_evidence_links.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")
|
||||
print(json.dumps({k:v for k,v in report.items() if k != "rows"}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Build source-only P05 stages from the current, preservation-fixed production."""
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import uuid
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "artifacts/original-completion-2026-10-02/p05-current-stage-input-r01"
|
||||
PACKET = ROOT / "artifacts/original-completion-2026-10-02/p05-auto-purchase-rebase"
|
||||
SAVE_SHA = "79a8c3654868c81d3e623a429d8fdfe51f15d098cef8d5b60be091d5200b285d"
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def write(path, value):
|
||||
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
OUT.mkdir(parents=True, exist_ok=False)
|
||||
spec = importlib.util.spec_from_file_location("regression_inputs", Path(__file__).with_name("run_current_regression.py"))
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
baseline_map = module.source_map()
|
||||
assert baseline_map["scripts/core/save_service.gd"] == SAVE_SHA
|
||||
targets = json.loads((PACKET / "overlay-targets.json").read_text())["targets"]
|
||||
assert len(targets) == 20
|
||||
stages = Path("/private/tmp") / ("thetree-p05-current-" + uuid.uuid4().hex[:12])
|
||||
maps = {}
|
||||
for variant in ("baseline", "candidate"):
|
||||
stage = stages / variant
|
||||
stage.mkdir(parents=True)
|
||||
for name, expected in baseline_map.items():
|
||||
target = stage / name
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(ROOT / name, target)
|
||||
assert sha(target) == expected
|
||||
maps[variant] = dict(baseline_map)
|
||||
if variant == "candidate":
|
||||
for row in targets:
|
||||
name = row["target"]
|
||||
candidate = PACKET / "candidates" / name
|
||||
assert sha(candidate) == row["rebased_candidate_sha256"], name
|
||||
if row["kind"] == "new":
|
||||
assert name not in baseline_map, name
|
||||
else:
|
||||
assert baseline_map[name] == sha(PACKET / "base-current" / name), name
|
||||
(stage / name).parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(candidate, stage / name)
|
||||
maps[variant][name] = sha(candidate)
|
||||
write(OUT / (variant + "-source.json"), maps[variant])
|
||||
after = module.source_map()
|
||||
assert after == baseline_map
|
||||
def roster(variant):
|
||||
return ["tests/run_tests.gd"] + sorted(p for p in maps[variant] if p.startswith("tests/") and
|
||||
Path(p).name.startswith("test") and p.endswith(".gd") and "export" not in Path(p).parts and Path(p).name != "test_kernel_and_save.gd")
|
||||
result = {"baseline_stage": str(stages / "baseline"), "candidate_stage": str(stages / "candidate"),
|
||||
"baseline_source_map": str(OUT / "baseline-source.json"), "candidate_source_map": str(OUT / "candidate-source.json"),
|
||||
"baseline_suites": len(roster("baseline")), "candidate_suites": len(roster("candidate")),
|
||||
"save_preservation_sha256": SAVE_SHA, "source_changes": [], "engine_runs": 0,
|
||||
"packet_integration_guards_are_historical_references_only": True,
|
||||
"candidate_acceptance": False, "production_applied": False}
|
||||
write(OUT / "preparation.json", result)
|
||||
print(json.dumps(result, ensure_ascii=False), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"scope": "Counting contract for P01 atomic requirements, not a populated acceptance register",
|
||||
"denominator_frozen": false,
|
||||
"atomic_register": null,
|
||||
"excluded_from_function_denominator": ["B-14.1", "B-14.2", "B-14.3", "B-14.4"],
|
||||
"counting_unit": "one unique independently judgeable specification condition",
|
||||
"completion_views": ["source_condition", "definition", "runtime", "normal_ui", "save_migration", "boundary_verification", "assets_platform", "evidence"],
|
||||
"implementation_required_views": ["source_condition", "definition", "runtime", "normal_ui", "save_migration"],
|
||||
"acceptance_required_views": "all applicable completion views",
|
||||
"view_states": ["not_started", "partial", "blocked", "pass", "fail", "not_applicable"],
|
||||
"not_applicable_requires": ["reason", "source_or_explicit_user_decision", "reviewer"],
|
||||
"partial_credit": false,
|
||||
"equal_count_weight": 1,
|
||||
"implementation_percent": "100 * fully_implemented_atomic_count / frozen_atomic_denominator",
|
||||
"acceptance_percent": "100 * fully_accepted_atomic_count / frozen_atomic_denominator",
|
||||
"unknown_before_freeze": null,
|
||||
"rounding": "Calculate from integer counts, display to two decimal places, retain counts",
|
||||
"atomic_record_required_fields": ["id", "source_anchors", "source_sha256", "user_amendment_refs", "condition", "normal_entry", "implementation_paths", "save_contract", "applicable_completion_views", "view_results", "evidence_refs", "source_head", "primary_package", "responsible_role", "reviewer", "status", "updated_at"],
|
||||
"published_snapshot_required_fields": ["source_head", "denominator_version", "denominator_ids", "implemented_ids", "accepted_ids", "blocked_ids", "partial_ids", "missing_evidence_ids", "difference_ids", "calculated_at"],
|
||||
"denominator_change_requires": ["added_ids", "removed_ids", "merged_ids", "reason", "previous_version", "new_version"],
|
||||
"must_remain_in_denominator": ["mandatory_unimplemented", "mandatory_unverified", "mandatory_undefined_values", "mandatory_unproduced_content"],
|
||||
"must_not_count_as_current_acceptance": ["test_count", "file_count", "package_count", "unadopted_candidate", "old_source_evidence_without_current_applicability", "fallback_asset", "accelerated_replay_as_human_playtime"],
|
||||
"effort_weighted_progress": "Separate future estimate; never relabel atomic count as effort percentage"
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Recheck retained source and raw regression evidence without rerunning it.
|
||||
|
||||
This accepts only the source regression baseline, never original-game acceptance.
|
||||
The original plan's immutable baseline remains a historical record.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from hashlib import sha256
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
PLAN = ROOT / "docs/plans/original-completion-2026-10-02"
|
||||
OUTPUT = ROOT / "artifacts/original-completion-2026-10-02/p00-p02-baseline"
|
||||
|
||||
|
||||
def digest(path):
|
||||
return sha256(path.read_bytes()).hexdigest() if path.is_file() else None
|
||||
|
||||
|
||||
def main():
|
||||
OUTPUT.mkdir(parents=True, exist_ok=True)
|
||||
baseline = json.loads((PLAN / "baseline.json").read_text())
|
||||
map_path = ROOT / "artifacts/facility-need-pricing-production-scale-adoption-2026-10-01-r01/after.json"
|
||||
adopted = json.loads(map_path.read_text())
|
||||
current = {path: digest(ROOT / path) for path in adopted}
|
||||
changes = [path for path in adopted if current[path] != adopted[path]]
|
||||
pins = {path: {"expected": expected, "actual": digest(ROOT / path),
|
||||
"passed": digest(ROOT / path) == expected}
|
||||
for path, expected in baseline["source_pins"].items()}
|
||||
review_path = ROOT / "artifacts/facility-need-pricing-r02-full155-independent-review-2026-10-01-r01/actual155-source-marker-review.json"
|
||||
review = json.loads(review_path.read_text())
|
||||
rows = []
|
||||
for previous in review["rows"]:
|
||||
path = ROOT / previous["test"]
|
||||
log = Path(previous["actual_log"])
|
||||
content = log.read_text() if log.is_file() else ""
|
||||
marker = previous["actual_marker"]
|
||||
match = re.search(r"checks=(\d+)|(\d+)(?: [A-Za-z]+)* checks", marker)
|
||||
observed_count = int(next(g for g in match.groups() if g is not None)) if match else None
|
||||
source_matches = digest(path) == previous["actual_source_sha256"]
|
||||
log_matches = digest(log) == previous["actual_log_sha256"]
|
||||
marker_matches = bool(content) and content.count(marker) == 1
|
||||
diagnostics_clear = bool(content) and not any(s in content for s in ("SCRIPT ERROR:", "\nERROR:", "FAIL:"))
|
||||
passed = (source_matches and log_matches and marker_matches and diagnostics_clear
|
||||
and observed_count == previous["actual_checks"]
|
||||
and previous["exit_code"] == 0 and previous["failed"] is False)
|
||||
rows.append({"test": previous["test"], "source_matches": source_matches,
|
||||
"retained_log": str(log), "log_matches": log_matches,
|
||||
"unique_completion_marker": marker_matches,
|
||||
"diagnostics_clear": diagnostics_clear,
|
||||
"actual_checks": observed_count, "passed": passed})
|
||||
discovered = {"tests/run_tests.gd"} | {
|
||||
str(p.relative_to(ROOT)) for p in (ROOT / "tests").rglob("test*.gd")
|
||||
if "export" not in p.relative_to(ROOT).parts
|
||||
and str(p.relative_to(ROOT)) != "tests/s0/test_kernel_and_save.gd"}
|
||||
recorded = {r["test"] for r in rows}
|
||||
actual_checks = sum(r["actual_checks"] or 0 for r in rows)
|
||||
head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip()
|
||||
result = {
|
||||
"scope": "retained regression applicability, not full original acceptance",
|
||||
"audited_at_jst": datetime.now(ZoneInfo("Asia/Tokyo")).isoformat(),
|
||||
"source_head": head, "baseline_head_matches": head == baseline["source_head"],
|
||||
"source_map": str(map_path.relative_to(ROOT)), "source_map_sha256": digest(map_path),
|
||||
"adopted_files": len(adopted), "changed_paths": changes, "source_pins": pins,
|
||||
"retained_review": str(review_path.relative_to(ROOT)), "retained_review_sha256": digest(review_path),
|
||||
"suites": len(rows), "actual_checks": actual_checks,
|
||||
"uncovered_discovered_tests": sorted(discovered - recorded),
|
||||
"stale_recorded_tests": sorted(recorded - discovered),
|
||||
"duplicate_tests": len(rows) != len(recorded),
|
||||
"rows": rows, "new_engine_runs": 0,
|
||||
"retained_regression_applicable": (head == baseline["source_head"] and not changes
|
||||
and all(p["passed"] for p in pins.values()) and recorded == discovered
|
||||
and len(rows) == len(recorded) == 155 and actual_checks == 12310
|
||||
and all(row["passed"] for row in rows)),
|
||||
"original_full_acceptance": False,
|
||||
}
|
||||
(OUTPUT / "source-current.json").write_text(json.dumps(current, ensure_ascii=False, indent=2) + "\n")
|
||||
(OUTPUT / "verification.json").write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
||||
print(json.dumps({k: v for k, v in result.items() if k not in ("rows", "source_pins")}, ensure_ascii=False))
|
||||
return 0 if result["retained_regression_applicable"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Re-audit immutable r03 logs; retain original marker-collector rejection."""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
BASE = ROOT / 'artifacts/original-completion-2026-10-02'
|
||||
INPUT = BASE / 'full-regression-r03'
|
||||
OUTPUT = BASE / 'full-regression-r03-review-r01'
|
||||
|
||||
def sha(p):
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
def read(p):
|
||||
return json.loads(p.read_text())
|
||||
|
||||
def write(p, value):
|
||||
with p.open('x') as f:
|
||||
f.write(json.dumps(value, ensure_ascii=False, indent=2) + '\n')
|
||||
|
||||
def require(ok, label):
|
||||
if not ok:
|
||||
raise ValueError(label)
|
||||
|
||||
def module(path, name):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
result = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(result)
|
||||
return result
|
||||
|
||||
def completion(test, text, previous):
|
||||
# Only the two rejected schemas change. All other retained exact markers
|
||||
# and every engine/exit/source/raw-byte gate remain in force.
|
||||
if test == 'tests/performance/test_snapshot_ownership.gd':
|
||||
matches = re.findall(r'^Snapshot ownership: (\d+) checks, 0 failures\n', text, re.M)
|
||||
require(len(matches) == 1, 'Unique ownership completion')
|
||||
return int(matches[0]), f'Snapshot ownership: {matches[0]} checks, 0 failures\n'
|
||||
if test == 'tests/ui/test_supply_auto_purchase_ui.gd':
|
||||
matches = re.findall(r'^SUPPLY_AUTO_PURCHASE_UI checks=(\d+) failures=0\n', text, re.M)
|
||||
require(len(matches) == 1, 'Unique auto UI completion')
|
||||
return int(matches[0]), f'SUPPLY_AUTO_PURCHASE_UI checks={matches[0]} failures=0\n'
|
||||
require(previous['passed'] and text.count(previous['completion_marker']) == 1, 'Retained exact marker')
|
||||
return previous['actual_checks'], previous['completion_marker']
|
||||
|
||||
def main():
|
||||
require(not OUTPUT.exists(), 'Fresh output only')
|
||||
original = read(INPUT / 'verification.json')
|
||||
require(not original['passed'] and original['runner_exit_code'] == 0 and original['changed_paths'] == [], 'Expected collector-only rejection')
|
||||
changed = ['tests/performance/test_snapshot_ownership.gd', 'tests/ui/test_supply_auto_purchase_ui.gd']
|
||||
require(original['failed_tests'] == changed, 'Only expected marker rejections')
|
||||
pins = read(INPUT / 'source-start.json')
|
||||
require(pins == read(INPUT / 'source-end.json') and sha(INPUT / 'source-start.json') == sha(INPUT / 'source-end.json') == original['source_map_sha256'], 'Stable full input bytes')
|
||||
require(original['wrapper_unchanged'] and sha(INPUT / 'wrapper-input.py') == original['wrapper_sha256'] == original['wrapper_sha256_after'], 'Execution wrapper unchanged')
|
||||
require(sha(INPUT / 'suites/all-tests.json') == original['all_results_sha256'], 'Raw runner result SHA')
|
||||
wrapper = module(INPUT / 'wrapper-input.py', 'execution_wrapper')
|
||||
wrapper.ROOT = ROOT
|
||||
require(wrapper.source_map() == pins, 'Current production equals execution source')
|
||||
roster = read(INPUT / 'expected-tests.json')
|
||||
rows = read(INPUT / 'suites/all-tests.json')
|
||||
require(roster == [r['test'] for r in rows] == [r['test'] for r in original['rows']] and len(set(roster)) == len(roster), 'Unique ordered actual roster')
|
||||
adapter_path = BASE / 'windows-current-release-adapter-prep-r01/windows_adapter.py'
|
||||
independent = module(adapter_path, 'independent_completion')
|
||||
independent.SPECIAL_MARKERS['tests/ui/test_supply_auto_purchase_ui.gd'] = r'SUPPLY_AUTO_PURCHASE_UI checks=(\d+) failures=0'
|
||||
reviewed = []
|
||||
for raw, old in zip(rows, original['rows']):
|
||||
test = raw['test']
|
||||
require(raw['exit_code'] == 0 and raw['failed'] is False and old['source_sha256'] == pins[test], 'Actual pass and test source')
|
||||
log = INPUT / 'suites' / raw['log']
|
||||
require(raw['log'] == old['log'] and Path(raw['log']).name == raw['log'] and sha(log) == old['log_sha256'], 'Exact ordinary raw log')
|
||||
text = log.read_text()
|
||||
require('Godot Engine v4.7.2' in text and not re.search(r'(?m)^\s*(?:SCRIPT ERROR|ERROR|Parse Error):', text), 'Clean engine completion')
|
||||
count, marker = completion(test, text, old)
|
||||
other_count, other_marker = independent.completion(test, text)
|
||||
require((count, marker.rstrip('\n')) == (other_count, other_marker) and count > 0, 'Independent parser agreement')
|
||||
reviewed.append(dict(old, actual_checks=count, completion_marker=marker, passed=True))
|
||||
# The ownership source is unchanged and enumerates every runtime section
|
||||
# in both the new-game and populated captures. Added auto_purchase_plans
|
||||
# accounts for two additional checks, with no weakened assertion.
|
||||
old_map = read(BASE / 'full-regression-r02/source-end.json')
|
||||
ownership = 'tests/performance/test_snapshot_ownership.gd'
|
||||
require(old_map[ownership] == pins[ownership], 'Ownership test source unchanged')
|
||||
ownership_text = (ROOT / ownership).read_text()
|
||||
require('for section: Variant in expected[&"systems"][&"runtime"]:' in ownership_text and 'verify_detached(runtime, "new game")' in ownership_text and 'verify_detached(runtime, "populated game")' in ownership_text, 'Dynamic section count, two captures')
|
||||
require('SUPPLY_AUTO_PURCHASE_UI checks=%d failures=%d' in (ROOT / changed[1]).read_text(), 'UI literal summary source')
|
||||
negatives = []
|
||||
for test, good in [(changed[0], 'Snapshot ownership: 91 checks, 0 failures\n'), (changed[1], 'SUPPLY_AUTO_PURCHASE_UI checks=28 failures=0\n')]:
|
||||
for name, value in [('duplicate', good + good), ('failure', good.replace('0 failures', '1 failures').replace('failures=0', 'failures=1')), ('missing', '')]:
|
||||
try:
|
||||
completion(test, value, {})
|
||||
except ValueError:
|
||||
negatives.append({'test': test, 'case': name, 'rejected': True})
|
||||
else:
|
||||
raise ValueError('Negative marker accepted')
|
||||
before = {str(p.relative_to(INPUT)): sha(p) for p in INPUT.rglob('*') if p.is_file()}
|
||||
shutil.copytree(INPUT, OUTPUT)
|
||||
(OUTPUT / 'verification.json').rename(OUTPUT / 'original-collector-verification.json')
|
||||
shutil.copyfile(Path(__file__), OUTPUT / 'review-input.py')
|
||||
report = dict(original, passed=True, checks=sum(r['actual_checks'] for r in reviewed), failed_tests=[], rows=reviewed, wrapper_copy_path=str(OUTPUT / 'wrapper-input.py'))
|
||||
report['reaudit'] = {'scope': 'immutable actual r03 logs; engine not rerun', 'original_collector_passed': False, 'original_collector_verification_sha256': sha(INPUT / 'verification.json'), 'original_execution_directory': str(INPUT), 'review_sha256': sha(Path(__file__)), 'independent_parser_source_sha256': sha(adapter_path), 'narrow_marker_schema_corrections': changed, 'negative_controls': negatives, 'no_engine_execution': True, 'source_test_assertions_changed': False, 'original_full_acceptance': False, 'windows_acceptance': False}
|
||||
write(OUTPUT / 'verification.json', report)
|
||||
require(before == {str(p.relative_to(INPUT)): sha(p) for p in INPUT.rglob('*') if p.is_file()} and wrapper.source_map() == pins, 'Original evidence and production unchanged')
|
||||
write(OUTPUT / 'review-integrity.json', {'original_files_unchanged': True, 'original_files': before, 'actual_suites':len(reviewed), 'actual_checks':report['checks'], 'source_changes': [], 'negative_controls_rejected': len(negatives), 'execution_wrapper_unchanged':True,'game_complete':False})
|
||||
print(json.dumps({k: report[k] for k in ['passed','suites','checks','failed_tests','changed_paths']}, ensure_ascii=False))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
"""Reclaim only two ended, isolated failed-formal r01 Godot caches."""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
ART = ROOT / "artifacts/original-completion-2026-10-02"
|
||||
PACKET = ART / "p03-formal13-ui-to14-final-r01"
|
||||
CACHES = [PACKET / "stages" / name / ".godot" for name in ("old", "new")]
|
||||
RECEIPT = ART / "ended-private-test-cache-reclaim-r04.json"
|
||||
|
||||
|
||||
def require(value, message):
|
||||
if not value:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def preserved():
|
||||
values = {}
|
||||
for p in PACKET.rglob("*"):
|
||||
require(not p.is_symlink(), "Symlink refused")
|
||||
if p.is_file() and ".godot" not in p.relative_to(PACKET).parts:
|
||||
values[p.relative_to(PACKET).as_posix()] = sha(p)
|
||||
return values
|
||||
|
||||
|
||||
def main():
|
||||
require(not RECEIPT.exists(), "Fresh reclaim receipt required")
|
||||
before = preserved()
|
||||
require(len(before) > 2400, "Complete retained two-stage packet expected")
|
||||
rows = []
|
||||
for cache in CACHES:
|
||||
require(cache.is_dir() and cache.resolve() == cache, "Exact owned cache directory required")
|
||||
handles = subprocess.run(["lsof", "+D", str(cache)], text=True, capture_output=True)
|
||||
require(handles.returncode == 1 and not handles.stdout and not handles.stderr, "Active/unverified cache handles")
|
||||
processes = subprocess.run(["ps", "-axo", "pid=,command="], text=True, capture_output=True, check=True).stdout
|
||||
require(not any(str(cache.parent) in line and ("Godot --" in line or "/godot --" in line) for line in processes.splitlines()), "Live owned Godot stage")
|
||||
entries = list(cache.rglob("*"))
|
||||
require(entries and not any(p.is_symlink() for p in entries), "Unsafe/empty cache inventory")
|
||||
files = [p for p in entries if p.is_file()]
|
||||
require(all(p.is_file() or p.is_dir() for p in entries), "Nonregular cache entry")
|
||||
rows.append({"path": cache.relative_to(ROOT).as_posix(), "logical_bytes": sum(p.stat().st_size for p in files), "file_count": len(files), "lsof_exit_code": handles.returncode, "active_handles": 0, "live_owned_Godot": 0, "cache_SHA_before": {p.relative_to(cache).as_posix(): sha(p) for p in files}})
|
||||
require(preserved() == before, "Preserved packet changed before removal")
|
||||
for cache in CACHES:
|
||||
for p in sorted(cache.rglob("*"), key=lambda p: len(p.parts), reverse=True):
|
||||
p.unlink() if p.is_file() else p.rmdir()
|
||||
cache.rmdir()
|
||||
require(preserved() == before and all(not p.exists() for p in CACHES), "Noncache/QA UID/raw bytes changed")
|
||||
receipt = {"passed": True, "scope": "regenerable .godot only from ended failed isolated formal r01, all source/QA UID/raw/profile packet entries retained", "rows": rows, "logical_bytes_reclaimed": sum(r["logical_bytes"] for r in rows), "preserved_noncache_files": len(before), "preserved_noncache_SHA": before, "preserved_SHA_changes": [], "production_source_edits": 0, "player_profile_cleanup": False, "new_engine_runs": 0}
|
||||
with RECEIPT.open("x") as stream:
|
||||
stream.write(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n")
|
||||
print(json.dumps({k: receipt[k] for k in ("passed", "logical_bytes_reclaimed", "preserved_noncache_files", "preserved_SHA_changes")}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Reclaim exact ended test-stage caches, preserving source, UID, and raw proof."""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
ART = ROOT / "artifacts/original-completion-2026-10-02"
|
||||
STAGES = [
|
||||
ART / "ui-audio-lifecycle-diagnostic-r01/stage",
|
||||
ART / "ui-audio-lifecycle-diagnostic-r02/stage",
|
||||
ART / "ui-audio-and-presentation-candidate-r04/focused-run-r01/stage",
|
||||
ART / "runtime-scale-diagnostic-max20000-r02/stage",
|
||||
ART / "p03-cleaning-day-boundary-paired-r01/stages/day_boundary",
|
||||
Path("/private/tmp/thetree-p03-rescue-96db3161ccdb/stage"),
|
||||
]
|
||||
RECEIPT = ART / "ended-private-probe-cache-reclaim-r01.json"
|
||||
|
||||
|
||||
def require(value, label):
|
||||
if not value:
|
||||
raise AssertionError(label)
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def preserved(stage):
|
||||
result = {}
|
||||
for path in stage.rglob("*"):
|
||||
require(not path.is_symlink(), "Stage symlink refused")
|
||||
if path.is_file() and ".godot" not in path.relative_to(stage).parts:
|
||||
result[path.relative_to(stage).as_posix()] = sha(path)
|
||||
require(len(result) >= 1224, "Full stage noncache source expected")
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
require(not RECEIPT.exists(), "Fresh receipt required")
|
||||
processes = subprocess.run(["ps", "-axo", "pid=,comm="], capture_output=True, text=True)
|
||||
require(processes.returncode == 0 and not processes.stderr, "Readable executable inventory before cleanup")
|
||||
live_engines = [line for line in processes.stdout.splitlines() if "godot" in line.lower()]
|
||||
require(not live_engines, "No live Godot before cleanup, including lowercase executable names")
|
||||
rows = []
|
||||
for stage in STAGES:
|
||||
cache = stage / ".godot"
|
||||
require(stage.resolve() == stage and cache.is_dir() and cache.resolve() == cache, "Exact ended stage/cache required")
|
||||
handles = subprocess.run(["lsof", "+D", str(cache)], capture_output=True, text=True)
|
||||
require(handles.returncode == 1 and not handles.stdout and not handles.stderr, "No active cache handles")
|
||||
entries = list(cache.rglob("*"))
|
||||
require(entries and all(not p.is_symlink() and (p.is_file() or p.is_dir()) for p in entries), "Regular owned cache entries only")
|
||||
files = [p for p in entries if p.is_file()]
|
||||
rows.append({"stage": str(stage), "cache": str(cache), "logical_bytes": sum(p.stat().st_size for p in files), "cache_files": len(files), "cache_sha_before": {p.relative_to(cache).as_posix():sha(p) for p in files}, "noncache_sha_before": preserved(stage), "lsof_exit_code": 1, "active_handles": 0})
|
||||
require(all(preserved(stage) == row["noncache_sha_before"] for stage,row in zip(STAGES,rows)), "Noncache stable before removal")
|
||||
for stage in STAGES:
|
||||
cache = stage / ".godot"
|
||||
for path in sorted(cache.rglob("*"), key=lambda p:len(p.parts), reverse=True):
|
||||
path.unlink() if path.is_file() else path.rmdir()
|
||||
cache.rmdir()
|
||||
require(all(preserved(stage) == row["noncache_sha_before"] and not (stage/".godot").exists() for stage,row in zip(STAGES,rows)), "All noncache/source/QA UID bytes retained")
|
||||
with RECEIPT.open("x", encoding="utf-8") as file:
|
||||
json.dump({"passed": True, "scope": "Exact declared ended private test-stage .godot caches only; all source/actual QA UID/outer raw/typed/formal evidence retained", "declared_stage_count": len(STAGES), "rows": rows, "logical_bytes_reclaimed": sum(r["logical_bytes"] for r in rows), "preserved_noncache_files": sum(len(r["noncache_sha_before"]) for r in rows), "noncache_SHA_changes": [], "production_source_edits": 0, "player_profile_cleanup": False, "engine_runs": 0}, file, ensure_ascii=False, indent=2)
|
||||
file.write("\n")
|
||||
print(json.dumps({"passed": True, "logical_bytes_reclaimed": sum(r["logical_bytes"] for r in rows), "noncache_files_preserved": sum(len(r["noncache_sha_before"]) for r in rows)}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Independent raw review of the preserved Staff failure/geometry contrast."""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
ART = ROOT / "artifacts/original-completion-2026-10-02"
|
||||
BASE = ART / "ui-audio-staff-repeat-r01/run-r01"
|
||||
FIX = ART / "ui-audio-staff-geometry-repeat-prep-r01/run-r01"
|
||||
OUT = ART / "ui-audio-staff-geometry-root-review-r01.json"
|
||||
|
||||
|
||||
def require(ok, message):
|
||||
if not ok:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def sha(p):
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def read(p):
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def main():
|
||||
require(not OUT.exists(), "Fresh root review")
|
||||
counts = {}; rows = []; all_pids = []
|
||||
for name, folder, failure in (("baseline", BASE, 1), ("geometry", FIX, 0)):
|
||||
receipt = read(folder / "execution.json")
|
||||
require(receipt["source_changes"] == receipt["input_changes"] == [] and receipt["engine_unchanged"] and receipt["source_files"] == 1246, "Same isolated stage input")
|
||||
require(receipt["original_expectations_unchanged"] and receipt["observer_added"] is False and receipt["formal_acceptance"] is False, "Limited unchanged expectations")
|
||||
require(len(receipt["rows"]) == 3 and receipt["actual_checks"] == 393 and receipt["strict_pass"] is (failure == 0), "Honest contrast result")
|
||||
total = 0
|
||||
for row in receipt["rows"]:
|
||||
n = row["repeat"]
|
||||
raw = folder / "logs" / ("repeat-%d.log" % n)
|
||||
text = raw.read_text(encoding="utf-8")
|
||||
profile = folder / ("profile-%d.json" % n)
|
||||
require(sha(raw) == row["log_sha256"] and sha(profile) == row["profile_report_sha256"], "Actual raw/profile pins")
|
||||
require(re.findall(r"^Staff management UI: (\d+) checks, (\d+) failures$", text, re.M) == [("131", str(failure))], "Unique raw actual marker")
|
||||
require("Godot Engine v4.7.2.stable.official.ed1daf0bf" in text and "SCRIPT ERROR" not in text and "Parse Error" not in text and "resources still in use" not in text and "leaked at exit" not in text, "Actual engine/no script/resource errors")
|
||||
errors = [line for line in text.splitlines() if line.startswith("ERROR:")]
|
||||
require(errors == (["ERROR: unreachable drag preserves staff and explains route failure"] if failure else []), "Only exact preserved baseline failure")
|
||||
guard = read(profile)
|
||||
require(guard == row["actual_profile"] and guard["verified"] and guard["conditions"] == [True]*5 and guard["conditions_checked"] == 5 and guard["fresh_before_parent_initialize"] and guard["profile_writes_before_guard"] == 0, "Actual five private guards")
|
||||
require(row["checks"] == 131 and row["failures"] == failure and row["exit_code"] == failure and row["engine_ended"], "Actual process/count/exit binding")
|
||||
all_pids.append(row["owned_engine_pid"]); total += 131
|
||||
rows.append({"case": name, "pid": row["owned_engine_pid"], "checks": 131, "failures": failure, "raw_sha256": sha(raw), "profile_sha256": sha(profile)})
|
||||
counts[name] = {"checks": total, "failures": failure*3, "execution_sha256": sha(folder / "execution.json")}
|
||||
require(len(set(all_pids)) == 6, "Six actual distinct owned processes")
|
||||
status = subprocess.run(["ps", "-p", ",".join(map(str,all_pids)), "-o", "pid=,comm="], capture_output=True, text=True)
|
||||
require(status.returncode == 1 and status.stdout == status.stderr == "", "All six owned PIDs ended")
|
||||
source = ROOT / "tests/ui/test_staff_management.gd"
|
||||
geometry = ART / "ui-audio-and-presentation-candidate-r07/candidate/tests/ui/test_staff_management.gd"
|
||||
original = source.read_text(encoding="utf-8")
|
||||
token = "func _drag_staff_to(target: Vector2, cell: Variant = null, before_release := Callable()) -> void:\n"
|
||||
require(original.count(token) == 1 and geometry.read_text(encoding="utf-8") == original.replace(token, token + "\t# Let restored roster layout finish before reading the card to press.\n\tawait process_frame\n"), "Only layout-frame synchronization before reading geometry")
|
||||
require(sha(source) == "6bd701cf3ac2386768db34b59d6298bed3f59a3aed0795e0854134051cdcf2e0" and sha(geometry) == "42595c39a933bfceffaab54d5169bbacfaca1d593cc3bcd099e72a354b77c428", "Source/script pins")
|
||||
historical = ART / "ui-audio-and-presentation-candidate-r06/focused-run-r01/execution.json"
|
||||
require(sha(historical) == "cb8ace997b3e22fff39a5fdb62cbd5f75a1c6a097b180486edbdcdebb1216011", "r06 failed execution retained")
|
||||
OUT.write_text(json.dumps({"passed": True, "scope": "Limited QA geometry contrast; no production adoption/full9/game acceptance", "cases": counts, "rows": rows, "all_owned_PIDs_ended": True, "source_expectations_and_input_API_unchanged": True, "geometry_difference": "one process_frame before card/start geometry is read", "detailed_deferred_layout_cause_confirmed": False, "r06_failed_execution_SHA_retained": sha(historical), "production_game_changes": [], "engine_runs_by_root_review": 0, "full9_focused_accepted": False, "whole_P03_accepted": False, "normal_human_play_accepted": False, "native_Windows_accepted": False}, ensure_ascii=False, indent=2)+"\n", encoding="utf-8")
|
||||
print("Root geometry contrast PASS: original393/3 retained, synchronized393/0;all6PIDs ended;input/expectations unchanged")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Root readonly review; this never imports or starts the game engine."""
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path('/Users/korizatou114/Projects/とりにく工房ゲーム制作部')
|
||||
ART = ROOT / 'artifacts/original-completion-2026-10-02'
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def verify_seal(name):
|
||||
folder = ART / name
|
||||
seal = folder / 'input-seal.json'
|
||||
pins = json.loads(seal.read_text())['files']
|
||||
actual = {str(p.relative_to(folder)) for p in folder.rglob('*') if p.is_file()}
|
||||
assert actual == set(pins) | {'input-seal.json'}, (name, actual ^ (set(pins) | {'input-seal.json'}))
|
||||
assert all(sha(folder / key) == value for key, value in pins.items()), name
|
||||
return {'seal_sha256': sha(seal), 'pinned_files': len(pins), 'exact_set': True, 'byte_mismatches': 0}
|
||||
|
||||
|
||||
def main():
|
||||
close_name = 'ui-audio-graceful-close-candidate-r01'
|
||||
merge_name = 'ui-audio-kitchen-business-merge-prep-r01'
|
||||
close = ART / close_name
|
||||
merge = ART / merge_name
|
||||
reviewed = {close_name: verify_seal(close_name), merge_name: verify_seal(merge_name)}
|
||||
source_file = ART / 'p05-current-stage-input-r04/candidate-source.json'
|
||||
source = json.loads(source_file.read_text())
|
||||
differences = [key for key, expected in source.items() if not (ROOT / key).is_file() or sha(ROOT / key) != expected]
|
||||
assert len(source) == 1224 and not differences, differences
|
||||
# Reproduce the actual three-way merge independently, then apply its sole
|
||||
# kitchen submission routing addition. All old bytes are preserved.
|
||||
merged = subprocess.run(['git', 'merge-file', '-p', str(merge / 'originals/audio-r07.gd'), str(merge / 'originals/base.gd'), str(merge / 'originals/kitchen-r08.gd')], capture_output=True, check=False)
|
||||
assert merged.returncode == 0 and not merged.stderr, merged.stderr
|
||||
old = b'_runtime.call(&"submit_command", KITCHEN_COMMAND, payload, &"player", selected_facility_id, 75, -1, key)'
|
||||
new = b'_submit_ui_command(KITCHEN_COMMAND, payload, &"player", selected_facility_id, 75, -1, key)'
|
||||
assert merged.stdout.count(old) == 1
|
||||
combined_file = merge / 'candidate/scripts/ui/business_management_panel.gd'
|
||||
assert merged.stdout.replace(old, new, 1) == combined_file.read_bytes()
|
||||
combined = combined_file.read_text()
|
||||
kitchen = (merge / 'originals/kitchen-r08.gd').read_text()
|
||||
strip = combined.replace('var _audio_submitter := Callable()\n', '', 1)
|
||||
strip = strip.replace('_submit_ui_command(KITCHEN_COMMAND, payload, &"player", selected_facility_id, 75, -1, key)', '_runtime.call(&"submit_command", KITCHEN_COMMAND, payload, &"player", selected_facility_id, 75, -1, key)', 1)
|
||||
strip = strip.replace('_submit_ui_command(COMMAND, payload, &"player", selected_facility_id, 75, -1, key)', '_runtime.call(&"submit_command", COMMAND, payload, &"player", selected_facility_id, 75, -1, key)', 1)
|
||||
strip = strip.split('\n\nfunc configure_audio(submitter: Callable) -> void:', 1)[0]
|
||||
assert strip == kitchen
|
||||
proof = {
|
||||
'scope': 'Root independent static review only; engine execution and adoption pending',
|
||||
'sealed_packets': reviewed,
|
||||
'source_files': len(source), 'source_mismatches': differences,
|
||||
'three_way_merge_actual_exit': merged.returncode,
|
||||
'additional_kitchen_submission_routing_occurrences': 1,
|
||||
'combined_business_sha256': sha(combined_file),
|
||||
'kitchen_exact_reconstruction_without_audio_hook': True,
|
||||
'close_candidate_full_diff_and_helper_read_by_root': True,
|
||||
'required_actual_checks': ['running Clock whole typed Runtime/config invariance', 'active/inactive real root close', 'modal/root/owned Window GUI input suppression', 'multi Main removal and late registration', 'all owned WeakRefs dead within deadline before one quit', 'normal last owner removal restores original auto_accept_quit', 'all Main dependent fixtures and fresh full roster'],
|
||||
'game_engine_processes_started': 0,
|
||||
'production_game_changes': 0,
|
||||
'formal_acceptance': False,
|
||||
'native_Windows': 'pending'
|
||||
}
|
||||
output = ART / 'ui-audio-kitchen-static-root-review-r01.json'
|
||||
assert not output.exists(), output
|
||||
output.write_text(json.dumps(proof, ensure_ascii=False, indent=2) + '\n')
|
||||
print(json.dumps({'receipt': str(output), 'sha256': sha(output), 'source_files': len(source), 'pinned_files': sum(x['pinned_files'] for x in reviewed.values()), 'engine_processes': 0, 'production_changes': 0}))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Read-only root review of current distribution and macOS same-PCK proofs."""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
ART = ROOT / "artifacts/original-completion-2026-10-02"
|
||||
OUT = ART / "windows-current-release-root-review-r01"
|
||||
PACKAGE = ART / "windows-current-release-candidate-r03"
|
||||
PROOF = ART / "windows-current-release-same-pck-r01"
|
||||
KIT = ART / "native-windows-current-release-kit-r01"
|
||||
|
||||
|
||||
def sha(path):
|
||||
value = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
value.update(block)
|
||||
return value.hexdigest()
|
||||
|
||||
|
||||
def read(path):
|
||||
return json.loads(path.read_text())
|
||||
|
||||
|
||||
def require(value, message):
|
||||
if not value:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def main():
|
||||
require(not OUT.exists(), "Fresh review output required")
|
||||
adapter_path = ART / "windows-current-release-adapter-prep-r04/windows_adapter.py"
|
||||
require(sha(adapter_path) == "fbc11778be400204922d69599e873d06d75adca28d9cf94d171b2fbac211ff74", "Adapter bytes changed")
|
||||
spec = importlib.util.spec_from_file_location("reviewed_windows_adapter", adapter_path)
|
||||
adapter = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(adapter)
|
||||
pins = read(PACKAGE / "source-start.json")
|
||||
require(len(pins) == 1224 and adapter.inventory(ROOT) == pins, "Current source map differs")
|
||||
tracked = {}
|
||||
|
||||
def pin(path, expected=None):
|
||||
actual = sha(path)
|
||||
require(expected is None or actual == expected, f"Evidence SHA differs: {path}")
|
||||
tracked[path.relative_to(ROOT).as_posix()] = actual
|
||||
return actual
|
||||
|
||||
verification = read(PROOF / "verification.json")
|
||||
require(verification["passed"] and verification["actual_checks"] == 626 and len(verification["rows"]) == 5, "Five actual suites required")
|
||||
require(verification["Godot_processes_started"] == 5 and verification["source_changes"] == [], "Actual process/source receipt differs")
|
||||
require(verification["native_Windows"] == "pending" and not verification["formal_acceptance"] and not verification["visual_acceptance"], "Scope was promoted")
|
||||
pin(PROOF / "verification.json")
|
||||
pin(PROOF / "root-source-after-verification.json")
|
||||
integrity = read(PACKAGE / "package-integrity.json")
|
||||
pin(PACKAGE / "package-integrity.json")
|
||||
export = read(PACKAGE / "export-validation.json")
|
||||
pin(PACKAGE / "export-validation.json")
|
||||
require(export["export_passed"] and export["runtime_inputs"] == 852 and export["source_changes"] == [] and export["clone_import_changes"] == [], "Export/source gate differs")
|
||||
require(export["import"]["exit_code"] == export["export"]["exit_code"] == 0, "Export process failed")
|
||||
require(export["diagnostic_preset"]["modify_resources"] is False and not export["diagnostic_preset"]["exe_resource_presentation_acceptance"], "Unmodified EXE scope missing")
|
||||
for phase in ("import", "export"):
|
||||
pin(PACKAGE / (phase + ".stdout.log"), export[phase]["stdout_sha256"])
|
||||
pin(PACKAGE / (phase + ".engine.log"), export[phase]["engine_log_sha256"])
|
||||
archive = PACKAGE / "TheTree-Windows-current-source-release-candidate.zip"
|
||||
pin(archive, "736cbc4809266fb34143e7c1b41e928912bec4d35ccf6818095d502b53fdbc14")
|
||||
require(archive.stat().st_size == integrity["zip_bytes"] and integrity["CRC_pass"] and integrity["each_member_SHA_pass"], "ZIP receipt differs")
|
||||
with zipfile.ZipFile(archive) as z:
|
||||
require(z.testzip() is None and sorted(z.namelist()) == sorted(integrity["members"]), "ZIP CRC/members failed")
|
||||
for member, expected in integrity["members"].items():
|
||||
value = hashlib.sha256()
|
||||
with z.open(member) as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
value.update(block)
|
||||
require(value.hexdigest() == expected, f"ZIP member SHA differs: {member}")
|
||||
pin(PACKAGE / "distribution" / member, expected)
|
||||
require(read(PACKAGE / "distribution/TheTree/SOURCE_MANIFEST.json") == pins, "Distributed source inventory differs")
|
||||
for kind in ("EXE", "PCK", "ZIP"):
|
||||
require(verification[kind + "_sha256"] == (integrity["zip_sha256"] if kind == "ZIP" else export[kind + "_sha256"]), f"Subset used different {kind}")
|
||||
rows = []
|
||||
for row in verification["rows"]:
|
||||
name = row["name"]
|
||||
require(row["passed"] and row["exit_code"] == 0 and row["actual_checks"] == row["expected_checks"] and row["error"] is None, f"Suite failed: {name}")
|
||||
require(row["PCK_sha256"] == verification["PCK_sha256"], f"Different PCK: {name}")
|
||||
driver = Path(row["argv"][row["argv"].index("--script") + 1])
|
||||
pin(driver, row["driver_sha256"])
|
||||
profile_path = PROOF / (name + "-actual-profile.json")
|
||||
pin(profile_path, row["profile_report_sha256"])
|
||||
guard = read(profile_path)
|
||||
require(guard["verified"] and guard["conditions_checked"] == 5 and guard["conditions"] == [True] * 5, f"Profile guard failed: {name}")
|
||||
require(guard["actual_user_data_dir"] == guard["expected_user_data_dir"] == row["fresh_actual_profile"], f"Actual profile differs: {name}")
|
||||
require(guard["globalized_probe"] == row["fresh_actual_profile"] + "/guard-probe" and guard["fresh_before_parent_initialize"] and guard["profile_writes_before_guard"] == 0 and guard["runtime_settings_only"] and not guard["project_settings_persisted"], f"Profile safety differs: {name}")
|
||||
for suffix, key in (("stdout.log", "stdout_sha256"), ("engine.log", "engine_log_sha256")):
|
||||
raw = PROOF / (name + "." + suffix)
|
||||
pin(raw, row[key])
|
||||
text = raw.read_text()
|
||||
require("Godot Engine v4.7.2.stable.official.ed1daf0bf" in text and not re.search(r"(?m)^\s*(?:SCRIPT ERROR|ERROR|Parse Error):", text) and "Unicode parsing error" not in text, f"Engine diagnostic: {name}")
|
||||
require(text.splitlines().count(row["completion_marker"]) == 1, f"Completion marker not unique: {name}")
|
||||
count, marker = adapter.completion(row["test"], text)
|
||||
require(count == row["actual_checks"] and marker == row["completion_marker"], f"Raw count differs: {name}")
|
||||
rows.append({"test": row["test"], "actual_checks": row["actual_checks"], "exit_code": row["exit_code"], "profile_conditions": 5})
|
||||
manifest = read(KIT / "kit-manifest.json")
|
||||
pin(KIT / "kit-manifest.json", "2126a2bddc5003ee5e71a4f4c660a9414b69babe2726442229180953028d713c")
|
||||
actual_kit = {p.relative_to(KIT).as_posix() for p in KIT.rglob("*") if p.is_file()}
|
||||
require(actual_kit == set(manifest["files"]) | {"kit-manifest.json"}, "Unexpected kit files")
|
||||
for name, expected in manifest["files"].items():
|
||||
pin(KIT / name, expected)
|
||||
expected = read(KIT / "expected.json")
|
||||
require(expected["source_files"] == 1224 and expected["full_actual_suites"] == 164 and expected["full_actual_checks"] == 13248, "Kit context differs")
|
||||
require(expected["Godot_processes_started"] == expected["Windows_launches"] == 0 and expected["native_Windows"] == "pending" and expected["PowerShell_syntax_parse"] == "unverified" and expected["final_acceptance_rate"] is None, "Kit execution was promoted")
|
||||
require(sum(x["expected_checks_from_actual_current_full"] for x in expected["drivers"]) == 626, "Kit expected subset count differs")
|
||||
for driver in expected["drivers"]:
|
||||
marker = next(x["completion_marker"] for x in verification["rows"] if x["test"] == driver["test"])
|
||||
hits = [m for pattern in driver["completion_patterns"] if (m := re.fullmatch(pattern, marker))]
|
||||
require(len(hits) == 1 and int(hits[0][1]) == driver["expected_checks_from_actual_current_full"], "Kit regex differs from actual retained marker")
|
||||
require(adapter.inventory(ROOT) == pins and all(sha(ROOT / name) == digest for name, digest in tracked.items()), "Inputs changed during root review")
|
||||
OUT.mkdir()
|
||||
result = {"passed": True, "scope": "root raw/SHA review of current export, package, macOS same-PCK626 and artifact-only native kit", "source_files": 1224, "runtime_export_inputs": 852, "source_changes": [], "same_PCK_actual_checks": 626, "same_PCK_suites": rows, "reviewed_pins": tracked, "kit_files": len(manifest["files"]), "ZIP_CRC_and_member_SHA_pass": True, "engine_runs_by_review": 0, "native_Windows": "pending", "PowerShell_parser": "unverified", "CSharp_compile": "unverified", "visual_acceptance": False, "performance_acceptance": False, "finished_EXE_resources_acceptance": False, "whole_original_acceptance": False, "final_acceptance_rate": None}
|
||||
(OUT / "review.json").write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
||||
print(json.dumps({k: v for k, v in result.items() if k not in ("reviewed_pins", "same_PCK_suites")}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Recheck immutable rendered day-boundary paired-save subset evidence."""
|
||||
from pathlib import Path
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
ART = ROOT / "artifacts/original-completion-2026-10-02"
|
||||
P = ART / "p03-cleaning-day-boundary-paired-r02"
|
||||
OUT = ART / "p03-day-boundary-root-review-r01.json"
|
||||
spec = importlib.util.spec_from_file_location("day_boundary_source_guard", ROOT / "docs/plans/original-completion-2026-10-02/execution/review_rescue_scale_checkpoint.py")
|
||||
guard = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(guard)
|
||||
sha, read, require = guard.sha, guard.read, guard.require
|
||||
|
||||
|
||||
def main():
|
||||
require(not OUT.exists(), "Fresh review required")
|
||||
seal = read(P / "evidence-seal.json")
|
||||
require(len(seal["nonstage_files"]) == 131 and all(sha(P / n) == h for n,h in seal["nonstage_files"].items()), "All131 immutable raw proof pins")
|
||||
receipt = read(P / "acceptance-subset-receipt.json")
|
||||
pins = read(P / "supplied-source-map.json")
|
||||
require(len(pins) == 1224 and pins == read(P / "input-source-before.json") == read(P / "input-source-end.json") == guard.source() and read(P / "input-source-change.json") == [], "Actual current adopted source exact before/end")
|
||||
summary = read(P / "summary.json")
|
||||
counts = {}
|
||||
require(summary["error"] is None and len(summary["phases"]) == 6 and len({r["pid"] for r in summary["phases"]}) == 6, "Actual six distinct phases")
|
||||
for row in summary["phases"]:
|
||||
log = P / "phases" / row["name"] / "raw.log"
|
||||
text = log.read_text()
|
||||
require(sha(log) == row["raw_log_sha256"] and row["passed"] and row["exit_code"] == 0 and not any(s in text for s in ("SCRIPT ERROR", "Parse Error", "ERROR:")), "Actual raw/clean exit")
|
||||
role = row["name"].removeprefix("day_boundary-")
|
||||
if role in ("write", "read"):
|
||||
markers = re.findall(r"^P03_MIDPOINT_COMPLETE scenario=day_boundary phase=" + role + r" nonce=([^ ]+) checks=(\d+) failures=(\d+)$", text, re.M)
|
||||
require(len(markers) == 1 and markers[0][2] == "0" and int(markers[0][1]) == row["checks"], "Unique actual completion count")
|
||||
profile = read(P / ("evidence/day_boundary/" + role + "-profile-before-parent.json"))
|
||||
require(profile["pid"] == row["pid"] and profile["nonce"] == markers[0][0] and profile["verified"] and profile["conditions"] == [True]*6 and profile["conditions_checked"] == 6 and profile["phase"] == role, "Actual private6 conditions before parent")
|
||||
result = read(P / ("evidence/day_boundary/" + role + "-result.json"))
|
||||
counts[role] = row["checks"]
|
||||
require(counts == {"write": 83, "read": 100}, "Actual183/0 counts")
|
||||
comparisons = read(P / "whole-typed-comparisons.json")
|
||||
require(comparisons["actual_pairs"] == 12 and comparisons["all_passed"] and len(comparisons["pairs"]) == 12, "Twelve full typed pairs")
|
||||
for pair in comparisons["pairs"]:
|
||||
left, right = P / pair["left"], P / pair["right"]
|
||||
require(left.read_bytes() == right.read_bytes() and sha(left) == pair["left_sha256"] and sha(right) == pair["right_sha256"] and pair["field_exclusions"] == [] and pair["event_deletion_count"] == 0, "Independent entire typed bytes, no exclusions/deletions")
|
||||
if "writer_raw_event" in pair:
|
||||
w,r = pair["writer_raw_event"], pair["reader_raw_event"]
|
||||
require({k:v for k,v in w.items() if k not in ("event_type", "payload")} == {k:v for k,v in r.items() if k not in ("event_type", "payload")}, "Exact IO identity/envelope")
|
||||
require(pair["writer_event_replaced_count"] == pair["reader_event_count"] == 1 and w["event_type"] == "SAVE_CHECKPOINT_SUCCEEDED" and r["event_type"] == "SAVE_RECOVERY_LOADED", "One actual manual save to normal UI formal recovery IO")
|
||||
require(w["absolute_minute"] == r["absolute_minute"] == 2879 and w["payload"]["save_id"] == r["payload"]["save_id"] == "manual" and w["payload"]["save_path"] == r["payload"]["save_path"], "Bound actual checkpoint and save")
|
||||
require(r["payload"]["selected_candidate_id"] == r["payload"]["selected_candidate_kind"] == "formal" and r["payload"]["selected_generation"] == -1 and r["payload"]["operation"] == "recovery" and r["payload"]["ok"] is True, "Formal chosen by actual normal recovery")
|
||||
package = Path(receipt["formal_package_retained_path"])
|
||||
require({f.relative_to(package).as_posix():sha(f) for f in package.rglob("*") if f.is_file()} == receipt["formal_4files_SHA_unchanged"] and len(receipt["formal_4files_SHA_unchanged"]) == 4, "Live original formal4 entries unchanged at root review")
|
||||
evidence = P / "evidence/day_boundary"
|
||||
require(read(evidence / "write-package-end.json") == read(evidence / "read-package-end.json"), "Writer/reader formal proof equality")
|
||||
pngs = []
|
||||
direct = {"write-checkpoint.png", "read-checkpoint.png", "read-final.png", "read-followup.png"}
|
||||
for name,h in receipt["rendered_PNG"].items():
|
||||
path = P / name
|
||||
require(sha(path) == h and path.read_bytes()[:8] == b"\x89PNG\r\n\x1a\n" and struct.unpack(">II",path.read_bytes()[16:24]) == (1280,720), "Actual1280x720 pinned PNG")
|
||||
pngs.append({"path": path.relative_to(ROOT).as_posix(), "sha256": h, "review": "direct_pixels" if path.name in direct else "same_bytes_as_reviewed_reader"})
|
||||
require(sha(evidence / "write-final.png") == sha(evidence / "read-final.png") and sha(evidence / "write-followup.png") == sha(evidence / "read-followup.png"), "Other2 captures exactly reviewed pixels")
|
||||
settled = read(evidence / "read-boundary-summary.json")
|
||||
require(settled["closed_at"] == 2880 and settled["day"] == 2 and settled["category_totals"]["maintenance"] == 2 and settled["reconciliation_difference"] == 0 and settled["net_profit"] == -2 and settled["balanced"], "Actual full day2G settlement reconciles")
|
||||
require(guard.source() == pins, "Source unchanged at review end")
|
||||
guard.write(OUT, {"passed": True, "scope": "Normal Main Save/Load handlers and rendered closed-cleaning day boundary, public construction/resources fixture; not whole normal play", "actual_checks": 183, "failures": 0, "source_files": 1224, "source_changes": [], "evidence_seal_sha256": sha(P / "evidence-seal.json"), "raw131_pins_rechecked": True, "whole_typed_pairs": comparisons["pairs"], "formal4_entries": receipt["formal_4files_SHA_unchanged"], "actual_IO": "SAVE_CHECKPOINT_SUCCEEDED to SAVE_RECOVERY_LOADED/formal; event_type/payload only, same identity/envelope/save_id", "uncapped_branch_events_each": 15, "same_process_future_events_each": 6, "pngs": pngs, "visual_review": {"direct": 4, "same_bytes": 2, "ledger_values": "23:59 gold4649/prior1G;00:00 gold4647/maintenance2G;00:01 same gold4647", "labels": "closed cleaning/no customer fees; ledger balance/no arrears displayed", "read_checkpoint_vs_write": "reader ledger selected by scenario; writer business panel", "finished_art_accepted": False}, "root_rendered_review_pending": False, "whole_P03_accepted": False, "whole_A05_accepted": False, "whole_normal_UI_accepted": False, "native_Windows_accepted": False, "performance_accepted": False, "engine_runs_by_review": 0})
|
||||
print("Root183/0 review PASS:131 pins,12typed pairs,formal4 unchanged,6PNG reviewed4direct+2same")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Root read-only review of two actual, limited Runtime/save boundaries."""
|
||||
from pathlib import Path
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
ART = ROOT / 'artifacts/original-completion-2026-10-02'
|
||||
OUT = ART / 'kitchen-and-rescue-power-root-review-r01.json'
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def read(path):
|
||||
return json.loads(path.read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
def require(value, reason):
|
||||
if not value:
|
||||
raise AssertionError(reason)
|
||||
|
||||
|
||||
def pin_rows(base, rows):
|
||||
for row in rows:
|
||||
path = base / row['path']
|
||||
require(path.is_file() and not path.is_symlink(), 'Regular proof ' + str(path))
|
||||
require(sha(path) == row['sha256'] and path.stat().st_size == row['bytes'], 'Proof SHA/size ' + str(path))
|
||||
|
||||
|
||||
def io1(folder, labels):
|
||||
rows = []
|
||||
for label in labels:
|
||||
proof = read(folder / ('read-' + label + '-io-comparison.json'))
|
||||
require(proof['bound_identity_saveid'] and proof['whole_typed_bytes_equal'], 'Whole typed comparison')
|
||||
require(proof['event_deletion_count'] == 0 and proof['mapped_fields'] == ['event_type', 'payload'], 'IO1 fields only')
|
||||
require(proof['writer_replaced'] == proof['reader_count'] and proof['writer_replaced'] in [0, 1], 'One exact IO event')
|
||||
writer, reader = proof['writer_io'], proof['reader_io']
|
||||
envelope = copy.deepcopy(writer)
|
||||
envelope['event_type'], envelope['payload'] = reader['event_type'], reader['payload']
|
||||
require(envelope == reader and writer['payload']['save_id'] == reader['payload']['save_id'], 'All envelope fields/save identity')
|
||||
require(writer['event_type'] == 'SAVE_CHECKPOINT_SUCCEEDED' and reader['event_type'] == 'SAVE_LOADED', 'Actual save/load event types')
|
||||
for event, operation, reason in [(writer, 'save', 'manual'), (reader, 'load', '')]:
|
||||
payload = event['payload']
|
||||
require(event['source_service'] == 'save_service' and event['causation_command_id'] == 0 and event['entity_ids'] == [], 'Actual IO provenance')
|
||||
require(payload['operation'] == operation and payload['reason'] == reason and payload['request_id'] == 0, 'Actual IO reason/request')
|
||||
require(payload['ok'] is True and payload['code'] == 'OK' and payload['save_id'] and payload['save_path'].startswith('user://tests/'), 'Actual successful private slot')
|
||||
require(payload['saved_absolute_minute'] == payload['checkpoint_absolute_minute'] == event['absolute_minute'], 'Exact saved minute')
|
||||
require(all(type(payload[k]) is int and payload[k] >= 0 for k in ['duration_us', 'runtime_duration_us']), 'Actual typed durations')
|
||||
actual = folder / ('read-' + label + '.var')
|
||||
expected = folder / ('read-' + label + '-expected-reader-io.var')
|
||||
require(actual.read_bytes() == expected.read_bytes() and actual.stat().st_size > 0, 'Complete actual typed buffers equal')
|
||||
# Independently check every JSON field as well as the actual typed buffers.
|
||||
writer_json = read(folder / ('write-' + label + '.json'))
|
||||
reader_json = read(folder / ('read-' + label + '.json'))
|
||||
events = writer_json['systems']['runtime']['domain_events']['recent_events']
|
||||
actual_replacements = 0
|
||||
for event in events:
|
||||
if event['event_id'] == writer['event_id']:
|
||||
require(event == writer, 'Exact original event before substitution')
|
||||
event['event_type'], event['payload'] = reader['event_type'], reader['payload']
|
||||
actual_replacements += 1
|
||||
require(actual_replacements == proof['writer_replaced'] and writer_json == reader_json, 'Independent entire JSON field equality')
|
||||
rows.append({'label': label, 'typed_sha256': sha(actual), 'IO1_sha256': sha(folder / ('read-' + label + '-io-comparison.json')), 'replacements': actual_replacements})
|
||||
return rows
|
||||
|
||||
|
||||
def formal(folder):
|
||||
files = {str(p.relative_to(folder / 'formal-package')): sha(p) for p in sorted((folder / 'formal-package').rglob('*')) if p.is_file()}
|
||||
require(set(files) == {'world.bin', 'manifest.json', 'journal.log', 'thumbnail.png'}, 'Exactly four formal files')
|
||||
require(files == read(folder / 'write-package-end.json') == read(folder / 'read-package-end.json'), 'Formal source SHA immutable')
|
||||
return files
|
||||
|
||||
|
||||
def raw_phase(raw, checks, title):
|
||||
text = raw.read_text(encoding='utf-8')
|
||||
require(re.findall(r'^' + re.escape(title) + r': (\d+) checks, (\d+) failures$', text, re.M) == [(str(checks), '0')], 'Unique actual count marker')
|
||||
require(len(re.findall(r'^ ✓ ', text, re.M)) == checks, 'Actual successful assertion count')
|
||||
require('Godot Engine v4.7.2.stable.official.ed1daf0bf' in text, 'Pinned engine banner')
|
||||
require(not re.search(r'(?m)^(?:ERROR:|SCRIPT ERROR:|WARNING:.*[Ll]eak)|Parse Error|Unicode parsing error|resources still in use|leaked at exit| ✗ ', text), 'No raw functional/script/resource errors')
|
||||
|
||||
|
||||
def main():
|
||||
require(not OUT.exists(), 'Fresh root review outside sealed candidates')
|
||||
kitchen = ART / 'p09-kitchen-single-link-candidate-r08'
|
||||
result = read(kitchen / 'execution/result.json')
|
||||
require(result['passed'] and result['production_changes'] == [] and len(result['phases']) == 10, 'Actual narrow phases')
|
||||
seal = ART / 'p09-kitchen-r08-narrow-actual-seal-r01'
|
||||
inventory = read(seal / 'immutable-narrow-proof-inventory.json')
|
||||
pin_rows(kitchen, inventory)
|
||||
pids = []
|
||||
for phase in result['phases']:
|
||||
raw = kitchen / 'execution/phases' / phase['phase'] / 'raw.log'
|
||||
require(sha(raw) == phase['raw_sha256'] and phase['passed'], 'Actual phase raw binding')
|
||||
require(phase['exit_code'] == (91 if phase['expected_guard_rejection91'] else 0), 'Actual phase exit')
|
||||
pids.append(phase['pid'])
|
||||
actual_runs = [p for p in result['phases'] if p['checks'] is not None]
|
||||
require([p['checks'] for p in actual_runs] == [41, 49, 116, 72], 'Four actual narrow counts')
|
||||
for phase in actual_runs:
|
||||
raw_phase(kitchen / 'execution/phases' / phase['phase'] / 'raw.log', phase['checks'], 'Kitchen single link')
|
||||
kitchen_states = {}
|
||||
for case, labels in [('legacy', ['checkpoint']), ('new', ['checkpoint', 'relinked-next1', 'minute44', 'final', 'tail5'])]:
|
||||
folder = kitchen / 'execution/proofs' / case
|
||||
kitchen_states[case] = {'states': io1(folder, labels), 'formal_files': formal(folder)}
|
||||
branch = read(kitchen / 'execution/proofs/new/read-branch-comparison.json')
|
||||
require(branch['writer_count'] == branch['reader_count'] == 5 and branch['event_deletion_count'] == 0 and branch['uncapped_full_typed_equal'], 'Five uncapped new branch events')
|
||||
require((kitchen / 'execution/proofs/new/read-branch-events.var').read_bytes() == (kitchen / 'execution/proofs/new/read-branch-events-expected-reader-io.var').read_bytes(), 'Whole branch typed bytes')
|
||||
power = ART / 'p06-rescue-power-paired-r03'
|
||||
r = read(power / 'result.json')
|
||||
require(r['passed'] and r['source_files'] == 1224 and r['production_source_changes'] == 0 and r['source_equals_current1224'] and r['actual_checks'] == 691 and len(r['phases']) == 6, 'Actual P06 phases/source')
|
||||
for phase in r['phases']:
|
||||
folder = power / 'phases' / phase['name']
|
||||
require(sha(folder / 'stdout.log') == phase['stdout_sha256'] and sha(folder / 'stderr.log') == phase['stderr_sha256'], 'Actual P06 raw pins')
|
||||
require(phase['passed'] and phase['exit_code'] == 0 and not phase['timed_out'] and phase['error_lines'] == [], 'Actual P06 phase passed')
|
||||
start = read(folder / 'started.json')
|
||||
require(start['pid'] == phase['pid'] and start['argv'] == phase['argv'] and start['started_utc'] == phase['started_utc'] and phase['ended_utc'] > phase['started_utc'], 'Actual process timing/command binding')
|
||||
pids.append(phase['pid'])
|
||||
if phase['name'] in ['write', 'read']:
|
||||
raw_phase(folder / 'stdout.log', phase['checks'], 'P06 rescue power restart')
|
||||
require([p['checks'] for p in r['phases'] if 'checks' in p] == [336, 355], 'Fresh P06 writer/reader counts')
|
||||
labels = ['checkpoint', 'outage-next1', 'outage-next3', 'power-restore-command', 'power-restored-next1', 'next1', 'treatment-progress', 'pre-completion', 'final', 'tail5']
|
||||
power_states = io1(power / 'proofs/ev', labels)
|
||||
power_formal = formal(power / 'proofs/ev')
|
||||
power_branch = read(power / 'proofs/ev/read-branch-comparison.json')
|
||||
require(power_branch['writer_count'] == power_branch['reader_count'] == 9 and power_branch['event_deletion_count'] == 0 and power_branch['uncapped_full_typed_equal'], 'Nine uncapped P06 branch events')
|
||||
require((power / 'proofs/ev/read-branch-events.var').read_bytes() == (power / 'proofs/ev/read-branch-events-expected-reader-io.var').read_bytes(), 'Whole P06 branch typed bytes')
|
||||
require(read(power / 'production-before.json') == read(power / 'production-end.json'), 'Production pins unchanged')
|
||||
require(len(set(pids)) == 16, 'Sixteen distinct owned processes')
|
||||
ended = subprocess.run(['ps', '-p', ','.join(map(str, pids)), '-o', 'pid=,comm='], capture_output=True, text=True)
|
||||
require(ended.returncode == 1 and ended.stdout == ended.stderr == '', 'All owned PIDs actually ended')
|
||||
fee = ART / 'p09-kitchen-s3-cost-fixture-candidate-r01'
|
||||
v = read(fee / 'verification.json')
|
||||
require(sha(fee / 'candidates/tests/s3/test_runtime_actor_cycle.gd') == v['candidate_sha256'] == '955cca90ee1be985e37ef166eaedb5c49f7001993afa38011fe3bf20ddb4a9b3', 'Reviewed fee candidate pin')
|
||||
require(v['production_changes'] == [] and v['engine_runs'] == 0 and v['old_assertions_preserved_except_explicit_paid_cost'] and not v['core_changed'], 'Fee fixture only, actual independent accounts')
|
||||
affected = ART / 'p09-kitchen-r08-affected9-r01/execution/result.json'
|
||||
require(sha(affected) == v['failed_affected9_result_sha256'] and not read(affected)['passed'], 'Failed affected9 evidence preserved')
|
||||
output = {'passed': True, 'scope': 'Limited actual kitchen and held rescue/EV outage save boundaries; candidate kitchen unadopted', 'kitchen': {'checks': 278, 'failures': 0, 'result_sha256': sha(kitchen / 'execution/result.json'), 'proof_files_rehashed': len(inventory), 'states': kitchen_states, 'uncapped_branch_events': 5, 'affected9_accepted': False, 'S3_actual_paid_cost_fixture_static_reviewed': True}, 'rescue_power': {'checks': 691, 'failures': 0, 'result_sha256': sha(power / 'result.json'), 'states': power_states, 'formal_files': power_formal, 'uncapped_branch_events': 9}, 'owned_PIDs': pids, 'all_owned_PIDs_ended': True, 'production_game_changes': [], 'engine_processes_by_root_review': 0, 'all8views_changed': False, 'denominator_changed': False, 'strict_behind_geometry_accepted': False, 'normal_human_play_accepted': False, 'emergency_safe_alighting_accepted': False, 'whole_P06_accepted': False, 'whole_P09_accepted': False, 'native_Windows_accepted': False, 'whole_game_accepted': False}
|
||||
with OUT.open('x', encoding='utf-8') as stream:
|
||||
stream.write(json.dumps(output, ensure_ascii=False, indent=2) + '\n')
|
||||
print('Root actual review PASS: kitchen278/0, rescue-power691/0, 16 ended PIDs; typed/formal/IO1 pins exact; affected9 and whole acceptance pending')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Root-only immutable raw review and limited four-document checkpoint."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import collections
|
||||
import datetime
|
||||
import difflib
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
PLAN = ROOT / "docs/plans/original-completion-2026-10-02"
|
||||
ART = ROOT / "artifacts/original-completion-2026-10-02"
|
||||
REVIEW = ART / "p03-rescue-and-p28-root-review-r01"
|
||||
OUT = ART / "checkpoint-rescue-and-p28-root-r03"
|
||||
NAMES = ("IMPLEMENTATION_PLAN.md", "PROGRESS_INTEGRATION.md", "work_packages.json", "progress_snapshot.json")
|
||||
|
||||
|
||||
def require(value, message):
|
||||
if not value:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def read(path):
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def write(path, value):
|
||||
require(not path.exists(), "Receipt overwrite refused: " + str(path))
|
||||
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def source():
|
||||
path = ART / "windows-current-release-adapter-prep-r04/windows_adapter.py"
|
||||
require(sha(path) == "fbc11778be400204922d69599e873d06d75adca28d9cf94d171b2fbac211ff74", "Source inventory adapter pin")
|
||||
spec = importlib.util.spec_from_file_location("rescue_checkpoint_inventory", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module.inventory(ROOT)
|
||||
|
||||
|
||||
def protected():
|
||||
return {n: sha(PLAN / n) for n in ("atomic_requirements.json", "progress_contract.json", "current_evidence_links.json", "atomic_denominator_freeze.json", "source_atomic_coverage.json", "baseline.json", "decisions.json")}
|
||||
|
||||
|
||||
def io_check(q, case, label):
|
||||
require(q["whole_typed_bytes_equal"] and q["bound_identity_saveid"] and q["event_deletion_count"] == 0, "Whole typed IO proof: " + case + "/" + label)
|
||||
require(q["mapped_fields"] == ["event_type", "payload"] and q["writer_replaced"] == q["reader_count"] and q["writer_replaced"] in (0, 1), "Exact IO replacement cardinality")
|
||||
if label == "checkpoint":
|
||||
require(q["writer_replaced"] == 1, "Actual checkpoint IO1")
|
||||
w, r = q["writer_io"], q["reader_io"]
|
||||
require(set(w) == set(r) and {k:v for k,v in w.items() if k not in ("event_type", "payload")} == {k:v for k,v in r.items() if k not in ("event_type", "payload")}, "All IO envelope fields retained")
|
||||
require(w["event_type"] == "SAVE_CHECKPOINT_SUCCEEDED" and r["event_type"] == "SAVE_LOADED" and w["source_service"] == "save_service" and w["entity_ids"] == [] and w["causation_command_id"] == 0, "IO provenance")
|
||||
for event, operation in ((w, "save"), (r, "load")):
|
||||
p = event["payload"]
|
||||
require(p["ok"] is True and p["code"] == "OK" and p["operation"] == operation and p["request_id"] == 0 and p["save_id"] == "manual", "Actual successful manual save/load")
|
||||
require(p["saved_absolute_minute"] == p["checkpoint_absolute_minute"] == event["absolute_minute"], "Actual checkpoint minute")
|
||||
require(p["reason"] == ("manual" if operation == "save" else ""), "Exact save/load reason")
|
||||
require(all(type(p[k]) is int and p[k] >= 0 for k in ("duration_us", "runtime_duration_us")), "Observed integer IO timings")
|
||||
require(w["payload"]["save_path"] == r["payload"]["save_path"] and w["payload"]["save_id"] == r["payload"]["save_id"], "Bound same formal package")
|
||||
|
||||
|
||||
def review():
|
||||
require(not REVIEW.exists(), "Fresh root review required")
|
||||
baseline = read(ART / "p05-current-stage-input-r04/candidate-source.json")
|
||||
require(len(baseline) == 1224 and source() == baseline, "Current adopted source1224 exact")
|
||||
packet = ART / "p03-rescue-paired-candidate-r02"
|
||||
execution = packet / "execution"
|
||||
data = read(execution / "result.json")
|
||||
require(data["passed"] and data["failure"] is None, "Actual execution passed")
|
||||
prep = read(execution / "preparation.json")
|
||||
phases = data["phases"]
|
||||
require(len(phases) == 12 and len({r["pid"] for r in phases}) == 12, "Import/parse/4 guard negatives/6 actual distinct processes")
|
||||
counts = {}
|
||||
for row in phases:
|
||||
raw_path = execution / "phases" / row["phase"] / "raw.log"
|
||||
require(sha(raw_path) == row["raw_sha256"], "Actual raw phase pin")
|
||||
raw = raw_path.read_text()
|
||||
markers = re.findall(r"^Staff rescue midpoint: (\d+) checks, (\d+) failures$", raw, re.M)
|
||||
require("Godot Engine v4.7.2" in raw and row["passed"], "Actual engine header/phase success")
|
||||
require("SCRIPT ERROR" not in raw and "Parse Error" not in raw, "No script/parse diagnostic")
|
||||
if row["expected_guard_rejection91"]:
|
||||
require(row["exit_code"] == 91 and not markers and "P03_RESCUE exact mode/profile/resource/nonce/formal/fresh output guards failed" in raw, "Expected guard rejection91")
|
||||
else:
|
||||
require(row["exit_code"] == 0 and "ERROR:" not in raw, "Clean actual phase exit")
|
||||
if row["checks"] is not None:
|
||||
require(markers == [(str(row["checks"]), "0")] and row["failures"] == 0, "Unique actual completion/count")
|
||||
counts[row["phase"]] = row["checks"]
|
||||
require(sum(counts.values()) == 650 and len(counts) == 6, "Six actual rescue phases650/0")
|
||||
require(read(execution / "production-before.json") == read(execution / "production-end.json") == baseline and read(execution / "production-change.json") == [], "All production source unchanged")
|
||||
actual_uid = read(execution / "actual-uid.json")
|
||||
require(sha(packet / "candidates" / (actual_uid["target"] + ".uid")) == actual_uid["sha256"], "Actual imported QA UID retained")
|
||||
pairs = []
|
||||
journals = {}
|
||||
cases = {}
|
||||
for case in ("transport", "ev", "treatment"):
|
||||
proof = execution / "proofs" / case
|
||||
w = next(r for r in phases if r["phase"] == case + "-write")
|
||||
r = next(r for r in phases if r["phase"] == case + "-read")
|
||||
for role, phase in (("write", w), ("read", r)):
|
||||
context = read(proof / (role + "-context.json"))
|
||||
result = read(proof / (role + "-result.json"))
|
||||
require(context["pid"] == result["pid"] == phase["pid"] and context["case"] == result["case"] == case and result["failures"] == 0, "Actual process/context binding")
|
||||
require(context["profile"] == prep["profiles"][case] and context["user_dir"] == prep["user_dirs"][case] and Path(context["res_dir"]).resolve() == Path(prep["stage"]).resolve(), "Actual private profile and resource path")
|
||||
require(result["normal_ui_accepted"] is False and result["rendered_accepted"] is False and result["whole_game_accepted"] is False, "Scope preserved")
|
||||
package = read(proof / "write-package-end.json")
|
||||
require(package == read(proof / "read-package-end.json") and len(package) == 4, "Four formal entries unchanged")
|
||||
package_copy = {p.relative_to(proof / "formal-package").as_posix(): sha(p) for p in (proof / "formal-package").rglob("*") if p.is_file()}
|
||||
require(package_copy == package, "Artifact retains exact original formal package")
|
||||
live = Path(prep["user_dirs"][case]) / "tests" / ("p03_rescue_" + context["nonce"] + "_" + case) / "manual"
|
||||
require({p.relative_to(live).as_posix(): sha(p) for p in live.rglob("*") if p.is_file()} == package, "Live private formal package unchanged at root review")
|
||||
for label in ("checkpoint", "next1", "treatment-progress", "pre-completion", "final", "tail5"):
|
||||
left = proof / ("read-" + label + "-expected-reader-io.var")
|
||||
right = proof / ("read-" + label + ".var")
|
||||
require(left.read_bytes() == right.read_bytes(), "Independent full typed byte comparison")
|
||||
io_check(read(proof / ("read-" + label + "-io-comparison.json")), case, label)
|
||||
pairs.append({"case": case, "label": label, "bytes": left.stat().st_size, "sha256": sha(left)})
|
||||
q = read(proof / "read-branch-comparison.json")
|
||||
left = proof / "read-branch-events-expected-reader-io.var"
|
||||
right = proof / "read-branch-events.var"
|
||||
require(left.read_bytes() == right.read_bytes() and q["uncapped_full_typed_equal"] and q["event_deletion_count"] == 0 and q["mapped_index"] == 0 and q["reader_count"] == q["writer_count"], "Independent uncapped branch comparison")
|
||||
journals[case] = {"events": q["reader_count"], "sha256": sha(left)}
|
||||
cases[case] = {"writer_pid": w["pid"], "reader_pid": r["pid"], "actual_checks": w["checks"] + r["checks"], "checkpoint_minute": result["checkpoint_minute"], "formal_entries": package}
|
||||
span = ART / "runtime-scale-diagnostic-setup-span-candidate-r01"
|
||||
manifest = read(span / "packet-manifest.json")
|
||||
require(all(sha(span / name) == expected for name, expected in manifest["files"].items()), "Setup span packet all19 SHA")
|
||||
driver = (span / "runtime_scale_driver.gd").read_text()
|
||||
stripped = re.sub(r"(?ms)^[ \t]*# QA_SETUP_SPAN_BEGIN\n.*?^[ \t]*# QA_SETUP_SPAN_END\n", "", driver).replace("_setup_observed_rebuild()", "_runtime.rebuild_path_graph_from_world()").replace("\n\n\nfunc _observe", "\n\nfunc _observe")
|
||||
require(stripped == (span / "instrumentation-base-r02.gd").read_text(), "Exact executed r02 driver after removing QA spans only")
|
||||
failed_path = ART / "p28-maxscale-timeout-root-review-r01.json"
|
||||
failed = read(failed_path)
|
||||
require(failed["evidence_integrity_passed"] and not failed["execution_passed"], "Retained failed execution root evidence")
|
||||
# Read the actual timeout result independently of the prior review.
|
||||
scale = read(ART / "runtime-scale-diagnostic-max20000-r02/execution.json")
|
||||
build = next(s for s in scale["steps"] if s["name"] == "build")
|
||||
require(build["timed_out"] and build["exit_code"] == -15 and 900 <= build["wall_seconds"] < 901 and scale["measurements"] is None and scale["P28_accepted"] is False, "Actual900s timeout remains a failure; no measurements")
|
||||
require(source() == baseline, "Source unchanged at root review end")
|
||||
REVIEW.mkdir()
|
||||
pins = {p.relative_to(ROOT).as_posix(): sha(p) for p in sorted(execution.rglob("*")) if p.is_file()}
|
||||
write(REVIEW / "review.json", {"passed": True, "scope": "Limited actual rescue paired-state evidence plus integrity of failed maxscale preparation; no whole acceptance", "source_files": 1224, "source_map_sha256": sha(ART / "p05-current-stage-input-r04/candidate-source.json"), "rescue_execution_sha256": sha(execution / "result.json"), "raw_file_sha256": pins, "actual_rescue_checks": 650, "failures": 0, "cases": cases, "whole_typed_pairs": pairs, "uncapped_journals": journals, "event_deletions": 0, "IO_mapped_fields": ["event_type", "payload"], "scale_timeout_review_sha256": sha(failed_path), "setup_span_manifest_sha256": sha(span / "packet-manifest.json"), "setup_span_static_files": len(manifest["files"]), "setup_span_executed": False, "scale_execution_passed": False, "measurements": None, "normal_UI_accepted": False, "natural_collapse_accepted": False, "P03_accepted": False, "P28_accepted": False, "native_Windows_accepted": False, "whole_original_accepted": False, "source_changes": [], "engine_runs_by_root_review": 0})
|
||||
print("Root review passed: rescue650/0,18 whole pairs; maxscale900s execution remains failed")
|
||||
|
||||
|
||||
def prepare():
|
||||
require(not OUT.exists(), "Fresh document checkpoint")
|
||||
proof_path = REVIEW / "review.json"
|
||||
proof = read(proof_path)
|
||||
require(proof["passed"] and proof["actual_rescue_checks"] == 650 and not proof["P03_accepted"] and not proof["P28_accepted"], "Root limited evidence")
|
||||
pins = read(ART / "p05-current-stage-input-r04/candidate-source.json")
|
||||
require(source() == pins, "Current source matches review")
|
||||
at = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9))).isoformat()
|
||||
OUT.mkdir(); (OUT / "before").mkdir(); (OUT / "reviewed").mkdir()
|
||||
before = {n: sha(PLAN / n) for n in NAMES}; guard = protected()
|
||||
for n in NAMES:
|
||||
(OUT / "before" / n).write_bytes((PLAN / n).read_bytes())
|
||||
def ref(path, scope):
|
||||
return {"path": path.relative_to(ROOT).as_posix(), "sha256_at_integration": sha(path), "scope": scope, "linked_at_jst": at, "current_game_acceptance_inferred": False}
|
||||
rescue = ref(proof_path, "Actual rescue650/0,3 distinct-PID pairs/18whole typed states/uncapped7+7+3events/formal4entries unchanged; controlled patient/Operations disabled")
|
||||
scale = ref(ART / "p28-maxscale-timeout-root-review-r01.json", "Actual BUILD900.024s/-15 timeout; evidence integrity only, target counts and all measurements null")
|
||||
span = ref(ART / "runtime-scale-diagnostic-setup-span-candidate-r01/packet-manifest.json", "19-file sealed setup-span candidate; exact executed r02 after removing QA progress, actual run pending")
|
||||
packages = read(PLAN / "work_packages.json")
|
||||
p03 = next(p for p in packages if p["id"] == "P03")
|
||||
previous = p03["current_summary"]
|
||||
status = "現本体164回帰と正式13→14/配送/清掃460に加え、制御した救護搬送/EV乗車/治療の3別processペア650 checks/0をroot照合。全人物/全施設/旧関係意味変換・正常UI全体は未完。"
|
||||
p03["current_summary"] = status; p03["updated_at_jst"] = at
|
||||
for task in p03["remaining_deliverables"]:
|
||||
task["evidence"].append(rescue); task["updated_at_jst"] = at
|
||||
if task["id"] != "P03-02": task["progress_summary"] = status
|
||||
else: task["progress_summary"] += " 同一14救護3pair650/0も限定照合。旧配置変換の受入へ転用しない。"
|
||||
if task["id"] == "P03-03":
|
||||
task["remaining_gaps"][0] = "全人物/全施設と救護の自然気絶・停止・経路喪失等の全途中paired受入は未完。今回3checkpointの制御救護に限定。"
|
||||
p28 = next(p for p in packages if p["id"] == "P28")
|
||||
target = next(t for t in p28["remaining_deliverables"] if t["id"] == "P28-01")
|
||||
require(target["state"] == "planned", "P28-01 was planned before actual attempt")
|
||||
target["state"] = "in_progress"
|
||||
target["evidence"] += [scale, span]; target["updated_at_jst"] = at
|
||||
target["progress_summary"] = "実Runtime最大負荷診断は準備BUILDで900.024秒timeout(-15)。目標実数/保存/別process/計測に未到達。setup span候補を準備し原因特定を継続。"
|
||||
p28["current_summary"] += " 最大負荷実診断は準備900秒timeoutで失敗。全性能値は未測定。"
|
||||
snapshot = read(PLAN / "progress_snapshot.json")
|
||||
snapshot["integrated_at_jst"] = at
|
||||
snapshot["current_rescue_and_scale_checkpoint"] = {"root_review": rescue, "source_files": 1224, "actual_rescue_checks": 650, "paired_cases": 3, "whole_typed_state_pairs": 18, "uncapped_event_counts": [7,7,3], "scale_timeout": scale, "scale_setup_span_candidate": span, "scale_measurements": None, "P03_accepted": False, "P28_accepted": False, "native_Windows_accepted": False, "whole_original_accepted": False, "source_changes_by_integration": 0, "adoption_receipt": OUT.relative_to(ROOT).as_posix() + "/adoption.json"}
|
||||
counts = dict(collections.Counter(t["state"] for p in packages for t in p["remaining_deliverables"]))
|
||||
require(counts["completed"] == snapshot["current_delivery_state_counts"]["completed"], "No deliverable completions added")
|
||||
require(counts["in_progress"] == snapshot["current_delivery_state_counts"]["in_progress"] + 1 and counts["planned"] == snapshot["current_delivery_state_counts"]["planned"] - 1, "Only P28-01 planned to in_progress")
|
||||
snapshot["current_delivery_state_counts"] = counts
|
||||
section = "\n\n### 救護3地点の再開と最大負荷準備の失敗\n\n[Root再照合](../../../artifacts/original-completion-2026-10-02/p03-rescue-and-p28-root-review-r01/review.json)で、搬送/EV乗車/治療の3地点を別PIDで正式保存・再開し、650 checks/0、18全状態typed pairs、無制限event journal各7/7/3、元formal4ファイルの不変を確認した。save/loadのIO1件は同じID・時刻・全envelope・save_idに拘束しevent_type/payloadのみ代替、event削除0。実カタログTessaの雇用/DAY/移動/救護と公開health0患者fixture、Operations無効、headlessの限定証拠で、自然気絶・通常UI・全P03・Windowsの受入へ転用しない。既存の正式13→14/配送/清掃460と異なる実行で、全8観点原子の完了数は増やさない。\n\n[最大負荷失敗の証拠](../../../artifacts/original-completion-2026-10-02/p28-maxscale-timeout-root-review-r01.json)は、実BUILD900.024秒でtimeout/-15を保持する。目標実数、正式保存、別process照合、x1/x3、FPS/保存時間の値は未取得/null。証拠の整合合格を実行合格へ変換しない。元対象を減らさず準備の区間を観測する[sealed候補](../../../artifacts/original-completion-2026-10-02/runtime-scale-diagnostic-setup-span-candidate-r01/packet-manifest.json)は未実行。通常Operationsの人口cap100は変更していない。\n\n[文書採用記録](../../../artifacts/original-completion-2026-10-02/checkpoint-rescue-and-p28-root-r01/adoption.json)に旧/採用後4文書SHAとsource/protected不変を保持する。全体率と最終分母はnull、P03/P28は着手中を維持する。\n"
|
||||
section = section.replace("checkpoint-rescue-and-p28-root-r01/adoption.json", OUT.name + "/adoption.json")
|
||||
text = (PLAN / "IMPLEMENTATION_PLAN.md").read_text()
|
||||
require(text.count(previous) == 3, "Expected P03 package/two task current statements")
|
||||
text = text.replace(previous, status)
|
||||
p28_before = "現在地: 複数の軽量化は採用済み。Runtime全体の性能予算は未達。"
|
||||
require(text.count(p28_before) == 1, "Exact previous P28 current statement")
|
||||
text = text.replace(p28_before, "現在地: " + p28["current_summary"])
|
||||
text = re.sub(r"(?m)^(- P28-01 .*保存を測る。 受入: .*を残す。)$", r"\1 状態: 着手中。実準備900秒timeoutで測定未到達、setup区間観測候補を準備。", text)
|
||||
text = text.replace("| 性能と長期プレイ | 既存ハーネスを再監査。通常Operations cap100、200客の診断案を作成 |", "| 性能と長期プレイ | 最大負荷実診断は準備900秒timeout。通常Operations cap100を保持しsetup区間の観測候補を準備 |")
|
||||
contents = {"IMPLEMENTATION_PLAN.md": text + section, "PROGRESS_INTEGRATION.md": (PLAN / "PROGRESS_INTEGRATION.md").read_text() + section, "work_packages.json": json.dumps(packages, ensure_ascii=False, indent=2) + "\n", "progress_snapshot.json": json.dumps(snapshot, ensure_ascii=False, indent=2) + "\n"}
|
||||
for n, value in contents.items():
|
||||
(OUT / "reviewed" / n).write_text(value, encoding="utf-8")
|
||||
diff = difflib.unified_diff((OUT / "before" / n).read_text().splitlines(True), value.splitlines(True), fromfile="before/"+n, tofile="reviewed/"+n)
|
||||
(OUT / (n + ".diff")).write_text("".join(diff), encoding="utf-8")
|
||||
require(source() == pins and protected() == guard and all(sha(PLAN / n) == v for n,v in before.items()), "No production changes during preparation")
|
||||
write(OUT / "preparation.json", {"before": before, "reviewed": {n:sha(OUT / "reviewed" / n) for n in NAMES}, "protected": guard, "source_map": pins, "root_review_sha256": sha(proof_path), "production_document_writes": 0, "prepared_at_jst": at})
|
||||
print("Prepared four documents: whole acceptance and completions unchanged")
|
||||
|
||||
|
||||
def adopt():
|
||||
data = read(OUT / "preparation.json")
|
||||
require(not (OUT / "adoption.json").exists(), "Fresh adoption receipt")
|
||||
require(source() == data["source_map"] and protected() == data["protected"], "Protected/source unchanged")
|
||||
require(sha(REVIEW / "review.json") == data["root_review_sha256"], "Root evidence unchanged")
|
||||
require(all(sha(PLAN / n) == v for n,v in data["before"].items()) and all(sha(OUT / "reviewed" / n) == v for n,v in data["reviewed"].items()), "Before/reviewed document pins")
|
||||
for n in NAMES:
|
||||
temp = PLAN / (n + ".rescue-checkpoint-tmp")
|
||||
require(not temp.exists(), "No unexpected temp")
|
||||
temp.write_bytes((OUT / "reviewed" / n).read_bytes()); temp.replace(PLAN / n)
|
||||
require(source() == data["source_map"] and protected() == data["protected"] and all(sha(PLAN/n) == v for n,v in data["reviewed"].items()), "Exact adopted documents/source")
|
||||
write(OUT / "adoption.json", {"passed": True, "before": data["before"], "adopted": data["reviewed"], "root_review_sha256": data["root_review_sha256"], "source_files": len(data["source_map"]), "source_changes": [], "protected_unchanged": data["protected"], "diff_reviewed_by_root": True, "engine_runs": 0, "completed_deliveries_added": 0, "P03_accepted": False, "P28_accepted": False, "whole_original_accepted": False})
|
||||
print("Adopted four documents; source changes0; P03/P28 remain in progress")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("mode", choices=("review", "prepare", "adopt"))
|
||||
globals()[parser.parse_args().mode]()
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"scope": "future collector exact two marker schemas, no game/test edits",
|
||||
"before_sha256": "2244e215d3fd30dc2c771ceb9dfc1c54b452520937a29dcdb9b61e1b96e2f104",
|
||||
"after_sha256": "9e94acde149624bebce59855206e5eb32541086b0f1217c155a90f47d3da7766",
|
||||
"actual_r03_not_rerun": true,
|
||||
"actual_immutable_review": "artifacts/original-completion-2026-10-02/full-regression-r03-review-r01/verification.json",
|
||||
"test_conditions_changed": false
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Guarded standard collector: logical original suites and actual native argv.
|
||||
|
||||
Run the canonical runner function in this same owned Python process so an
|
||||
abruptly killed wrapper cannot strand a separate runner's unobserved children.
|
||||
Actual native Popen objects remain owned by the runner's unchanged inner/outer
|
||||
finally adapters. The wrapper never signals groups or discovered processes.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
from pathlib import Path
|
||||
import argparse,contextlib,hashlib,importlib.util,json,os,shutil,sys
|
||||
sys.dont_write_bytecode=True
|
||||
ROOT=Path(__file__).resolve().parents[4]
|
||||
def sha(p):return hashlib.sha256(Path(p).read_bytes()).hexdigest()
|
||||
def load(name,path):
|
||||
spec=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m
|
||||
def write(p,value):
|
||||
with p.open('x',encoding='utf-8')as stream:stream.write(json.dumps(value,ensure_ascii=False,indent=2)+'\n')
|
||||
def expected_roster():
|
||||
return ['tests/run_tests.gd']+sorted(str(p.relative_to(ROOT))for p in(ROOT/'tests').rglob('test*.gd')if 'export'not in p.relative_to(ROOT).parts and str(p.relative_to(ROOT))!='tests/s0/test_kernel_and_save.gd')
|
||||
def main(argv=None):
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--project-root',type=Path,default=ROOT);parser.add_argument('--output',type=Path,required=True)
|
||||
parser.add_argument('--godot',default='godot');parser.add_argument('--timeout',type=int,default=1200)
|
||||
parser.add_argument('--standard-runner',type=Path);parser.add_argument('--source-input-refs',type=Path)
|
||||
parser.add_argument('--marker-contracts',type=Path)
|
||||
args=parser.parse_args(argv);root=args.project_root.resolve();out=args.output.resolve()
|
||||
runner_path=(args.standard_runner or root/'tools/run_all_tests.py').resolve()
|
||||
runner=load('actual_guarded_standard_runner',runner_path);protocol=load('actual_guarded_standard_reader',runner_path.parent/'regression/review_protocol.py')
|
||||
marker_path=runner_path.parent/'strict_test_markers.py';module=load('guarded_original_marker',marker_path)
|
||||
contract_path=(args.marker_contracts or root/'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json').resolve();contracts=module.load_contracts(contract_path)
|
||||
before=runner.source_refs(root);selected=runner.read(args.source_input_refs)if args.source_input_refs else before
|
||||
expected={n:r['sha256']for n,r in selected.items()};wrapper_sha=sha(Path(__file__));marker_sha=sha(marker_path);contract_sha=sha(contract_path)
|
||||
out.mkdir(parents=True,exist_ok=False)
|
||||
for source,name in [(Path(__file__),'wrapper-input.py'),(marker_path,'marker-module-input.py'),(contract_path,'marker-contract-input.json'),(runner_path,'standard-runner-input.py')]:shutil.copyfile(source,out/name)
|
||||
write(out/'source-start.json',expected);write(out/'protected-current-source-start.json',before)
|
||||
logical=['tests/run_tests.gd']+sorted(n for n in expected if n.startswith('tests/')and n.endswith('.gd')and Path(n).name.startswith('test')and'export'not in Path(n).parts and n!='tests/s0/test_kernel_and_save.gd')
|
||||
write(out/'expected-tests.json',logical);started=datetime.now(ZoneInfo('Asia/Tokyo')).isoformat()
|
||||
command=[str(runner_path),'--project-root',str(root),'--output',str(out/'suites'),'--godot',args.godot,'--timeout',str(args.timeout),'--marker-contracts',str(contract_path)]
|
||||
if args.source_input_refs:command+=['--source-input-refs',str(args.source_input_refs.resolve())]
|
||||
write(out/'standard-driver-start.json',{'event':'OWNED_IN_PROCESS_STANDARD_CALL','actual_python_pid':os.getpid(),'owner_wrapper_pid':os.getpid(),'native_PID_is_not_wrapper_PID':True,'logical_arguments':command,'runner_sha256':sha(runner_path)})
|
||||
code=1;call_error=None
|
||||
try:
|
||||
with(out/'standard-driver.raw.log').open('x',encoding='utf-8')as stream,contextlib.redirect_stdout(stream),contextlib.redirect_stderr(stream):code=runner.main(command[1:])
|
||||
except BaseException as caught:call_error=type(caught).__name__+': '+str(caught)
|
||||
write(out/'standard-driver-end.json',{'event':'OWNED_IN_PROCESS_STANDARD_RETURN','actual_python_pid':os.getpid(),'owner_wrapper_pid':os.getpid(),'exit_code':code,'call_error':call_error,'raw_sha256':sha(out/'standard-driver.raw.log')})
|
||||
after=runner.source_refs(root);write(out/'protected-current-source-end.json',after)
|
||||
write(out/'source-end.json',{n:sha(r['path'])for n,r in selected.items()})
|
||||
changes=sorted(n for n in set(before)|set(after)if before.get(n)!=after.get(n));errors=[];review=None
|
||||
try:review=protocol.review(out/'suites',root,expected,contracts,os.getpid())
|
||||
except BaseException as caught:errors.append(type(caught).__name__+': '+str(caught))
|
||||
unchanged=sha(Path(__file__))==wrapper_sha and sha(marker_path)==marker_sha and sha(contract_path)==contract_sha
|
||||
passed=code==0 and call_error is None and not changes and unchanged and not errors and review is not None and review['passed']
|
||||
results=out/'suites/all-tests.json';rows=review['rows']if review else []
|
||||
report={'scope':'current pinned-source guarded standard regression; no original/native/full-game acceptance','passed':passed,
|
||||
'protocol':'guarded-standard-v1','wrapper_sha256':wrapper_sha,'wrapper_sha256_after':sha(Path(__file__)),
|
||||
'wrapper_copy_path':str(out/'wrapper-input.py'),'wrapper_unchanged':unchanged,'all_results_sha256':sha(results)if results.exists()else None,
|
||||
'marker_module_sha256':marker_sha,'marker_contract_sha256':contract_sha,'standard_runner_sha256':expected['tools/run_all_tests.py'],
|
||||
'source_map_sha256':sha(out/'source-start.json'),'source_files':len(expected),'started_at_jst':started,
|
||||
'finished_at_jst':datetime.now(ZoneInfo('Asia/Tokyo')).isoformat(),'runner_exit_code':code,'standard_runner_pid':os.getpid(),
|
||||
'standard_runner_execution_kind':'same owned Python process; distinct actual native PID receipts per phase',
|
||||
'suites':len(rows),'checks':review['actual_all_roster_checks']if review else None,'count_from_raw_only':True,
|
||||
'failed_tests':[r['test']for r in rows if not r['passed']],'changed_paths':changes,'collector_errors':errors+(review['collector_errors']if review else []),
|
||||
'call_error':call_error,'rows':rows,'actual_review':review,'original_full_acceptance':False,'windows_acceptance':False,
|
||||
'P28_maximum_scene_performance_acceptance':False}
|
||||
write(out/'verification.json',report);print(json.dumps({k:v for k,v in report.items()if k not in ['rows','actual_review']},ensure_ascii=False),flush=True)
|
||||
return 0 if passed else 1
|
||||
if __name__=='__main__':raise SystemExit(main())
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Run the standard source regression and bind every result to exact inputs."""
|
||||
from datetime import datetime
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "artifacts/original-completion-2026-10-02/full-regression-r01"
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def source_map():
|
||||
paths = [p for folder in ("autoload", "scripts", "scenes", "data", "tests", "assets", "tools")
|
||||
for p in (ROOT / folder).rglob("*") if p.is_file() and p.name != ".DS_Store"]
|
||||
paths += [ROOT / p for p in ("project.godot", "export_presets.cfg", "Makefile", "AGENTS.md")]
|
||||
return {str(p.relative_to(ROOT)): sha(p) for p in sorted(paths)}
|
||||
|
||||
|
||||
def write(name, data):
|
||||
(OUT / name).write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
global OUT
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, default=OUT)
|
||||
args = parser.parse_args()
|
||||
OUT = args.output.resolve()
|
||||
OUT.mkdir(parents=True, exist_ok=False)
|
||||
before = source_map()
|
||||
roster = ["tests/run_tests.gd"] + sorted(str(p.relative_to(ROOT)) for p in (ROOT / "tests").rglob("test*.gd")
|
||||
if "export" not in p.relative_to(ROOT).parts and p.name != "test_kernel_and_save.gd")
|
||||
assert len(roster) == len(set(roster))
|
||||
write("source-start.json", before)
|
||||
write("expected-tests.json", roster)
|
||||
started_at = datetime.now(ZoneInfo("Asia/Tokyo")).isoformat()
|
||||
code = subprocess.run(["python3", "tools/run_all_tests.py", "--output", str(OUT / "suites")], cwd=ROOT).returncode
|
||||
after = source_map()
|
||||
write("source-end.json", after)
|
||||
changes = sorted(path for path in set(before) | set(after) if before.get(path) != after.get(path))
|
||||
retained = json.loads((ROOT / "artifacts/facility-need-pricing-r02-full155-independent-review-2026-10-01-r01/actual155-source-marker-review.json").read_text())
|
||||
old_markers = {row["test"]: row["actual_marker"] for row in retained["rows"]}
|
||||
results = json.loads((OUT / "suites/all-tests.json").read_text())
|
||||
reviewed = []
|
||||
for row in results:
|
||||
log = OUT / "suites" / row["log"]
|
||||
content = log.read_text()
|
||||
if row["test"] in old_markers:
|
||||
marker = old_markers[row["test"]]
|
||||
matches = [marker] if content.count(marker) == 1 else []
|
||||
else:
|
||||
matches = re.findall(r"^.*?: \d+ checks, 0 failures\n", content, re.MULTILINE)
|
||||
match = re.search(r"checks=(\d+)|(\d+)(?: [A-Za-z]+)* checks", matches[0]) if len(matches) == 1 else None
|
||||
count = int(next(g for g in match.groups() if g is not None)) if match else None
|
||||
passed = row["exit_code"] == 0 and not row["failed"] and count is not None and "Godot Engine v4.7.2" in content
|
||||
reviewed.append(dict(test=row["test"], actual_checks=count, passed=passed,
|
||||
source_sha256=before.get(row["test"]), log_sha256=sha(log),
|
||||
log=row["log"], completion_marker=matches[0] if len(matches) == 1 else None,
|
||||
unicode_diagnostic_lines=content.count("Unicode parsing error")))
|
||||
actual_names = [r["test"] for r in reviewed]
|
||||
verified = code == 0 and not changes and actual_names == roster and all(r["passed"] for r in reviewed)
|
||||
report = dict(scope="current source regression, not original full-game acceptance", passed=verified,
|
||||
source_head=subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip(),
|
||||
source_map_sha256=sha(OUT / "source-start.json"), source_files=len(before),
|
||||
started_at_jst=started_at, finished_at_jst=datetime.now(ZoneInfo("Asia/Tokyo")).isoformat(),
|
||||
runner_exit_code=code, suites=len(reviewed), checks=sum(r["actual_checks"] or 0 for r in reviewed),
|
||||
failed_tests=[r["test"] for r in reviewed if not r["passed"]], changed_paths=changes,
|
||||
rows=reviewed, original_full_acceptance=False, windows_acceptance=False)
|
||||
write("verification.json", report)
|
||||
print(json.dumps({k:v for k,v in report.items() if k != "rows"}, ensure_ascii=False), flush=True)
|
||||
return 0 if verified else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
"""Run the standard source regression and bind every result to exact inputs."""
|
||||
from datetime import datetime
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "artifacts/original-completion-2026-10-02/full-regression-r01"
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def source_map():
|
||||
paths = [p for folder in ("autoload", "scripts", "scenes", "data", "tests", "assets", "tools")
|
||||
for p in (ROOT / folder).rglob("*") if p.is_file() and p.name != ".DS_Store"]
|
||||
paths += [ROOT / p for p in ("project.godot", "export_presets.cfg", "Makefile", "AGENTS.md")]
|
||||
return {str(p.relative_to(ROOT)): sha(p) for p in sorted(paths)}
|
||||
|
||||
|
||||
def write(name, data):
|
||||
(OUT / name).write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
global OUT
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, default=OUT)
|
||||
args = parser.parse_args()
|
||||
OUT = args.output.resolve()
|
||||
OUT.mkdir(parents=True, exist_ok=False)
|
||||
wrapper_sha = sha(Path(__file__))
|
||||
shutil.copyfile(Path(__file__), OUT / "wrapper-input.py")
|
||||
before = source_map()
|
||||
roster = ["tests/run_tests.gd"] + sorted(str(p.relative_to(ROOT)) for p in (ROOT / "tests").rglob("test*.gd")
|
||||
if "export" not in p.relative_to(ROOT).parts and p.name != "test_kernel_and_save.gd")
|
||||
assert len(roster) == len(set(roster))
|
||||
write("source-start.json", before)
|
||||
write("expected-tests.json", roster)
|
||||
started_at = datetime.now(ZoneInfo("Asia/Tokyo")).isoformat()
|
||||
code = subprocess.run(["python3", "tools/run_all_tests.py", "--output", str(OUT / "suites")], cwd=ROOT).returncode
|
||||
after = source_map()
|
||||
write("source-end.json", after)
|
||||
changes = sorted(path for path in set(before) | set(after) if before.get(path) != after.get(path))
|
||||
retained = json.loads((ROOT / "artifacts/facility-need-pricing-r02-full155-independent-review-2026-10-01-r01/actual155-source-marker-review.json").read_text())
|
||||
old_markers = {row["test"]: row["actual_marker"] for row in retained["rows"]}
|
||||
results = json.loads((OUT / "suites/all-tests.json").read_text())
|
||||
reviewed = []
|
||||
for row in results:
|
||||
log = OUT / "suites" / row["log"]
|
||||
content = log.read_text()
|
||||
if row["test"] == "tests/supply/test_supply_auto_purchase.gd":
|
||||
# This new suite publishes a key/value summary, not the colon
|
||||
# form used by the other newly adopted regression suites.
|
||||
matches = re.findall(r"^SUPPLY_AUTO_PURCHASE checks=\d+ failures=0\n", content, re.MULTILINE)
|
||||
elif row["test"] in old_markers:
|
||||
marker = old_markers[row["test"]]
|
||||
matches = [marker] if content.count(marker) == 1 else []
|
||||
else:
|
||||
matches = re.findall(r"^.*?: \d+ checks, 0 failures\n", content, re.MULTILINE)
|
||||
match = re.search(r"checks=(\d+)|(\d+)(?: [A-Za-z]+)* checks", matches[0]) if len(matches) == 1 else None
|
||||
count = int(next(g for g in match.groups() if g is not None)) if match else None
|
||||
passed = row["exit_code"] == 0 and not row["failed"] and count is not None and "Godot Engine v4.7.2" in content
|
||||
reviewed.append(dict(test=row["test"], actual_checks=count, passed=passed,
|
||||
source_sha256=before.get(row["test"]), log_sha256=sha(log),
|
||||
log=row["log"], completion_marker=matches[0] if len(matches) == 1 else None,
|
||||
unicode_diagnostic_lines=content.count("Unicode parsing error")))
|
||||
actual_names = [r["test"] for r in reviewed]
|
||||
wrapper_unchanged = sha(Path(__file__)) == wrapper_sha
|
||||
verified = code == 0 and not changes and wrapper_unchanged and actual_names == roster and all(r["passed"] for r in reviewed)
|
||||
report = dict(scope="current source regression, not original full-game acceptance", passed=verified,
|
||||
wrapper_sha256=wrapper_sha,
|
||||
wrapper_unchanged=wrapper_unchanged,
|
||||
wrapper_sha256_after=sha(Path(__file__)),
|
||||
wrapper_copy_path=str(OUT / "wrapper-input.py"),
|
||||
all_results_sha256=sha(OUT / "suites/all-tests.json"),
|
||||
standard_runner_sha256=before["tools/run_all_tests.py"],
|
||||
source_head=subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip(),
|
||||
source_map_sha256=sha(OUT / "source-start.json"), source_files=len(before),
|
||||
started_at_jst=started_at, finished_at_jst=datetime.now(ZoneInfo("Asia/Tokyo")).isoformat(),
|
||||
runner_exit_code=code, suites=len(reviewed), checks=sum(r["actual_checks"] or 0 for r in reviewed),
|
||||
failed_tests=[r["test"] for r in reviewed if not r["passed"]], changed_paths=changes,
|
||||
rows=reviewed, original_full_acceptance=False, windows_acceptance=False)
|
||||
write("verification.json", report)
|
||||
print(json.dumps({k:v for k,v in report.items() if k != "rows"}, ensure_ascii=False), flush=True)
|
||||
return 0 if verified else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,196 @@
|
||||
{
|
||||
"review_date_jst": "2026-10-02",
|
||||
"reviewer": "atomic_requirements independent task",
|
||||
"review_mode": "read-only code and retained evidence audit",
|
||||
"production_edits": 0,
|
||||
"new_godot_runs": 0,
|
||||
"source_head": "285528b7a3b1b46a2a67af5aa90d9085539b42f4",
|
||||
"reviewed_source_sha256": {
|
||||
"scripts/core/save_service.gd": "7c107a509b3afccb478ba89d051fbaa9a8c93165326fac8e4cef2249c05879d0",
|
||||
"scripts/core/simulation_snapshot.gd": "fc528a3d1db1f439c10f70d6cba2f72bd65abc0d3866eb8b582df60918af0adf",
|
||||
"autoload/simulation_runtime.gd": "d3ad4aab6ded19eb137c5494481cc7333e846746a25b18dbad3c8b663e9f6b72",
|
||||
"tests/s6/test_save_preflight.gd": "49e24247649839b6f50d19b2284368efb7f8f8b189c5010491ad428e97c1c8b9"
|
||||
},
|
||||
"focused_evidence": {
|
||||
"checks": 485,
|
||||
"suites": 7,
|
||||
"all_passed_verified": true,
|
||||
"logs": [
|
||||
{
|
||||
"test": "tests/s6/test_save_preflight.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 128,
|
||||
"seconds": 3.15,
|
||||
"log": "tests__s6__test_save_preflight.gd-r02.log",
|
||||
"log_sha256": "171e2af4cdd6c9d00928110642f20de6d678f300947d16b53e4485f4988206c9",
|
||||
"completion_check_counts": [
|
||||
128
|
||||
],
|
||||
"completion_matches": true,
|
||||
"passed_verified": true
|
||||
},
|
||||
{
|
||||
"test": "tests/s6/test_save_operations.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 127,
|
||||
"seconds": 2.79,
|
||||
"log": "tests__s6__test_save_operations.gd-r02.log",
|
||||
"log_sha256": "c353636c82f54e49a88ab56ca000c14a78687921e5cc5ceeea8982a2275ed2c9",
|
||||
"completion_check_counts": [
|
||||
127
|
||||
],
|
||||
"completion_matches": true,
|
||||
"passed_verified": true
|
||||
},
|
||||
{
|
||||
"test": "tests/s6/test_runtime_save_lifecycle.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 96,
|
||||
"seconds": 6.68,
|
||||
"log": "tests__s6__test_runtime_save_lifecycle.gd-r02.log",
|
||||
"log_sha256": "6ab68137561c286f85cfb1b7f970023207659cb061f8ba1db41e3ed1f1a33157",
|
||||
"completion_check_counts": [
|
||||
96
|
||||
],
|
||||
"completion_matches": true,
|
||||
"passed_verified": true
|
||||
},
|
||||
{
|
||||
"test": "tests/run_tests.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 73,
|
||||
"seconds": 1.13,
|
||||
"log": "tests__run_tests.gd-r02.log",
|
||||
"log_sha256": "91ecdbe39f081ef6124499b9cec042600e7154d72c89f029298966aa560606ab",
|
||||
"completion_check_counts": [
|
||||
73
|
||||
],
|
||||
"completion_matches": true,
|
||||
"passed_verified": true
|
||||
},
|
||||
{
|
||||
"test": "tests/transport/test_transport_save.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 29,
|
||||
"seconds": 1.7,
|
||||
"log": "tests__transport__test_transport_save.gd-r02.log",
|
||||
"log_sha256": "f0d51dfd583cd9b08d19790a39fedc9169c8802cab72f7817d9ecfa40fe93306",
|
||||
"completion_check_counts": [
|
||||
29
|
||||
],
|
||||
"completion_matches": true,
|
||||
"passed_verified": true
|
||||
},
|
||||
{
|
||||
"test": "tests/ui/test_dimension_migration_ui.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 12,
|
||||
"seconds": 2.33,
|
||||
"log": "tests__ui__test_dimension_migration_ui.gd-r02.log",
|
||||
"log_sha256": "db9f3e78c7670558b9871c1c78abbad8be27bb45fd8d3c79e7535df4d3adfe4d",
|
||||
"completion_check_counts": [
|
||||
12
|
||||
],
|
||||
"completion_matches": true,
|
||||
"passed_verified": true
|
||||
},
|
||||
{
|
||||
"test": "tests/supply/test_supply_save_migration.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 20,
|
||||
"seconds": 1.18,
|
||||
"log": "tests__supply__test_supply_save_migration.gd-r02.log",
|
||||
"log_sha256": "b26de7e09b864e70fc36822dec40851ef08a056404e25373a9c1d6e26d45211d",
|
||||
"completion_check_counts": [
|
||||
20
|
||||
],
|
||||
"completion_matches": true,
|
||||
"passed_verified": true
|
||||
}
|
||||
],
|
||||
"source_map_count": 318,
|
||||
"source_map_drift_paths": [
|
||||
"tests/performance/test_digest_performance.gd"
|
||||
],
|
||||
"changed_files_pins_match": true
|
||||
},
|
||||
"new_difference_induced_major_findings": 0,
|
||||
"findings": [
|
||||
{
|
||||
"id": "SFR-01",
|
||||
"priority": 1,
|
||||
"state": "candidate_only_static_reproduction_path",
|
||||
"preexisting": true,
|
||||
"file": "scripts/core/save_service.gd",
|
||||
"line": 932,
|
||||
"symbol": "_reconcile_interrupted_formal_save",
|
||||
"condition": "identifiable unsupported formal version + valid transaction marker + current tmp/bak",
|
||||
"effect": "formal package deleted/replaced by old backup before unsupported-version rejection",
|
||||
"affected_entries": [
|
||||
"load_snapshot",
|
||||
"load_with_recovery",
|
||||
"inspect_recovery_candidates",
|
||||
"migrate_save",
|
||||
"load_recovery_candidate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "SFR-02",
|
||||
"priority": 2,
|
||||
"state": "candidate_only_static_reproduction_path",
|
||||
"preexisting": true,
|
||||
"file": "scripts/core/save_service.gd",
|
||||
"line": 334,
|
||||
"symbol": "_load_recovery_candidate_impl",
|
||||
"condition": "explicit backup selection + stale marker/tmp, formal either present or missing",
|
||||
"effect": "reconcile may delete tmp/marker or promote tmp before nominal read_only result"
|
||||
}
|
||||
],
|
||||
"candidate_artifact": "artifacts/original-completion-2026-10-02/p03-recovery-readonly-candidate/candidate_receipt.json",
|
||||
"adoption_recommendation": "Reproduce and verify both preservation defects in an isolated candidate stage before claiming P03 acceptance; no main edits during ongoing full regression.",
|
||||
"all_p03_accepted": false,
|
||||
"full157_regression_result_included": false,
|
||||
"limitations": [
|
||||
"128 preflight does not combine unknown formal with valid marker/current backup",
|
||||
"256 MiB is a serialized world limit, not total peak memory",
|
||||
"inclusive upper-size test stops at manifest mismatch, not successful full-size decompression",
|
||||
"20000-cell measurement is world-only, not full Runtime acceptance",
|
||||
"new candidate has not been executed"
|
||||
],
|
||||
"report_sha256": "57de52db51a6b0b3dcbcf20f6df3a825b83ba2d9a9a7707e9bab7026cb3fc6c6",
|
||||
"existing_contract_static_review": {
|
||||
"save_operations_lines": [
|
||||
349,
|
||||
450
|
||||
],
|
||||
"runtime_save_lifecycle_lines": [
|
||||
170,
|
||||
200,
|
||||
230,
|
||||
380
|
||||
],
|
||||
"preserved": [
|
||||
"registry continuity and purity",
|
||||
"current schema rejection",
|
||||
"registered legacy migration and exact original backup",
|
||||
"failed migration original preservation",
|
||||
"current-format interrupted migration backup/promotion/rollback",
|
||||
"current-format normal recovery"
|
||||
],
|
||||
"strengthened": [
|
||||
"unsupported recognizable formal never replaced before rejection",
|
||||
"explicit candidate read does not reconcile",
|
||||
"registered legacy failed source keeps normal stale artifacts"
|
||||
],
|
||||
"candidate_execution_verified": false,
|
||||
"candidate_driver_operations": 8,
|
||||
"candidate_foreign_shapes": 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Integrate the preservation-fixed full regression without promoting game acceptance."""
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
PLAN = ROOT / "docs/plans/original-completion-2026-10-02"
|
||||
PREFIX = "artifacts/original-completion-2026-10-02/"
|
||||
NOW = datetime.now(ZoneInfo("Asia/Tokyo")).isoformat()
|
||||
|
||||
|
||||
def read(path):
|
||||
return json.loads((ROOT / path).read_text())
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256((ROOT / path).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def write(path, value):
|
||||
(ROOT / path).write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
|
||||
def evidence(path, scope):
|
||||
return {"path": path, "sha256_at_integration": sha(path), "scope": scope,
|
||||
"linked_at_jst": NOW, "current_game_acceptance_inferred": False}
|
||||
|
||||
|
||||
def main():
|
||||
regression_path = PREFIX + "full-regression-r02/verification.json"
|
||||
regression = read(regression_path)
|
||||
assert regression["passed"] and regression["suites"] == 158 and regression["checks"] == 12707 and not regression["changed_paths"]
|
||||
opening = PREFIX + "p02-opening-restart-r02/"
|
||||
assert all(r["passed"] for r in read(opening + "result.json")) and read(opening + "source-change.json") == []
|
||||
full_map = read(PREFIX + "full-regression-r02/source-end.json")
|
||||
opening_map = read(opening + "source-start.json")
|
||||
assert all(full_map.get(p) == digest for p, digest in opening_map.items())
|
||||
assert all(sha(p) == digest for p, digest in full_map.items())
|
||||
acceptance_path = PREFIX + "p03-save-preservation-r02/adoption-acceptance.json"
|
||||
assert not (ROOT / acceptance_path).exists(), "Keep prior checkpoint generations"
|
||||
write(acceptance_path, {"scope": "limited SaveService preservation fix adoption, not full P03/game acceptance", "source_map": PREFIX + "full-regression-r02/source-end.json", "source_map_sha256": sha(PREFIX + "full-regression-r02/source-end.json"), "current_source_matches": True,
|
||||
"merged_regression": evidence(regression_path, "158 suites/12707 checks/0 failures/source changes0"),
|
||||
"opening_restart": evidence(opening + "result.json", "rendered normal UI signals; 83 writer+20 reader checks"),
|
||||
"merge_receipt": evidence(PREFIX + "p03-save-preservation-r02/merge.json", "immutable focused acceptance/merge receipt"),
|
||||
"standard_full_regression_after_merge_completed": True, "normal_pointer_human_play": False, "windows_acceptance": False, "original_full_acceptance": False, "accepted_at_jst": NOW})
|
||||
plan_rel = "docs/plans/original-completion-2026-10-02/"
|
||||
snapshot = read(plan_rel + "progress_snapshot.json")
|
||||
snapshot.setdefault("regression_history", []).append(snapshot["latest_regression_at_evidence_time"])
|
||||
snapshot["latest_regression_at_evidence_time"] = {"path": regression_path, "sha256": sha(regression_path),
|
||||
"started_at_jst": regression["started_at_jst"], "finished_at_jst": regression["finished_at_jst"],
|
||||
"suites": 158, "checks": 12707, "failures": 0, "source_changes_during_run": [],
|
||||
"source_map_path": PREFIX + "full-regression-r02/source-end.json", "source_map_sha256": sha(PREFIX + "full-regression-r02/source-end.json"), "source_map_entries": len(full_map),
|
||||
"current_source_delta_observed_after_run": [], "future_source_changes_auto_accepted": False,
|
||||
"original_full_acceptance": False, "windows_acceptance": False,
|
||||
"current_applicability": "current production source matches all 1208 input paths at checkpoint; P05 unadopted candidate excluded"}
|
||||
snapshot["save_preservation_adoption"].update(state="merged_full_regression_and_limited_restart_verified", standard_full_regression_after_merge_completed=True, acceptance_path=acceptance_path, acceptance_sha256=sha(acceptance_path))
|
||||
snapshot["opening_restart_current"] = evidence(opening + "result.json", "103 checks/source change0; current mapped game source319 matches full input1208")
|
||||
snapshot["remaining_acceptance"] = [s for s in snapshot["remaining_acceptance"] if not s.startswith("未知版/明示候補読取の本体採用後")]
|
||||
snapshot["integrated_at_jst"] = NOW
|
||||
packages = read(plan_rel + "work_packages.json")
|
||||
by_id = {p["id"]: p for p in packages}
|
||||
by_id["P02"]["current_summary"] = "保存修正後の現版158 suites/12707 checks/失敗0/source差分0、限定通常UI103再開checks。全代表保存・同一Windows実機・全通常操作は未受入。"
|
||||
by_id["P03"]["current_summary"] = "保存preflight/原状保持/Path guardを本体採用し、現版158回帰12707 checksと通常宿屋103再開checksを確認。全機能保存/移行/再開は未完。"
|
||||
for pid, did in [("P02", "P02-01"), ("P03", "P03-01"), ("P03", "P03-02")]:
|
||||
row = next(d for d in by_id[pid]["remaining_deliverables"] if d["id"] == did)
|
||||
row["evidence"].append(evidence(regression_path, "現保存修正版の標準158回帰、12707 checks/source差分0"))
|
||||
row["updated_at_jst"] = NOW
|
||||
row = next(d for d in by_id["P03"]["remaining_deliverables"] if d["id"] == "P03-01")
|
||||
row["evidence"].append(evidence(acceptance_path, "保存原状保持限定修正の統合受入"))
|
||||
row["progress_summary"] = "保存preflight、未知/未来formal保護、明示backup純読取を本体採用し現版158回帰12707 checksが合格。限定宿屋の同版保存再開103 checksも確認。全機能保存契約は未完。"
|
||||
row["remaining_gaps"] = [g for g in row["remaining_gaps"] if not g.startswith("採用SaveService SHA")]
|
||||
for pid, did in [("P02", "P02-02"), ("P03", "P03-03")]:
|
||||
row = next(d for d in by_id[pid]["remaining_deliverables"] if d["id"] == did)
|
||||
row["evidence"].append(evidence(opening + "result.json", "保存修正後の描画付き通常UI/宿屋営業の83+20再開checks、human pointer受入ではない"))
|
||||
row["evidence"].append(evidence(opening + "source-change.json", "再開実行中のソース差分0"))
|
||||
row["progress_summary"] = "保存修正後の同版で通常UIから宿屋開業/3日決算/保存/別process再開を103 checks確認。全機能途中再開は未完。"
|
||||
row["remaining_gaps"] = [g for g in row.get("remaining_gaps", []) if "今後のSAVE採用後" not in g]
|
||||
row["updated_at_jst"] = NOW
|
||||
p05 = next(d for d in by_id["P05"]["remaining_deliverables"] if d["id"] == "P05-01")
|
||||
p05.update(state="in_progress", progress_summary="現版158からcanonical20overlayの候補161を構築。partial受入のr01環境guard誤判定/r02旧manual helper契約誤期待を保持、QA修正/追加境界/最終全回帰は未完。", updated_at_jst=NOW,
|
||||
remaining_gaps=["旧QA helperの重複キー契約を正しい現本体の拒否契約へ補正し再検証。", "enabled-auto5境界、標準新testのfixture整合、PNG目視、最終同一source全回帰、本体採用が未完。"])
|
||||
p05.setdefault("evidence", []).extend([evidence(PREFIX + "p05-current-stage-input-r01/preparation.json", "current base158/candidate161 isolated inputs, engine0"), evidence(PREFIX + "p05-acceptance-partial-r01/result.json", "失敗履歴:engine objectdb_snapshotsを既存セーブと誤判定"), evidence(PREFIX + "p05-acceptance-partial-r02/result.json", "失敗履歴:旧manual driverはduplicate成功を誤要求;本体未採用")])
|
||||
snapshot["current_delivery_state_counts"] = dict(Counter(d["state"] for p in packages for d in p["remaining_deliverables"]))
|
||||
snapshot["p05_partial_rejected"] = evidence(PREFIX + "p05-acceptance-partial-r02/result.json", "QA contract correction pending; does not prove candidate regression failure")
|
||||
write(plan_rel + "work_packages.json", packages)
|
||||
write(plan_rel + "progress_snapshot.json", snapshot)
|
||||
main_plan = PLAN / "IMPLEMENTATION_PLAN.md"
|
||||
body = main_plan.read_text()
|
||||
body = body.replace("今回157回帰の実行版は別の1206対象ソース地図で固定", "今回158回帰の実行版は別の1208対象ソース地図で固定")
|
||||
body = body.replace("SAVE追加採用前の01:14:41〜01:26:28 JST、157スイート、12,473チェック、失敗0、実行中ソース差分0", "保存追加採用後の01:41:43〜01:53:50 JST、158スイート、12,707チェック、失敗0、実行中ソース差分0")
|
||||
body = body.replace("SAVE追加採用後全回帰、原本全実装、全資産、操作感、Windows", "原本全実装、全資産、全通常操作、Windows")
|
||||
body = body.replace("SAVE追加修正前の157 suites/12473 checks/失敗0/source差分0を保持。採用後全回帰、代表保存、通常操作、同一Windows配布実機は未受入。", by_id["P02"]["current_summary"])
|
||||
body = body.replace("preflightとPath guardの限定修正後157回帰あり。追加の未知版原状保持修正を741 focused checksで本体採用、採用後全回帰pending。全機能保存/移行/再開は未完。", by_id["P03"]["current_summary"])
|
||||
body = body.replace("修正後の全回帰は未完。", "現版158回帰12707 checks/source差分0を確認。")
|
||||
body += "\n### 実行追記 01:53以降\n\n保存修正後の[158回帰](../../../artifacts/original-completion-2026-10-02/full-regression-r02/verification.json)は12,707 checks、失敗0、source差分0。同版の[通常UI開業/再開](../../../artifacts/original-completion-2026-10-02/p02-opening-restart-r02/result.json)は83+20 checks。以前の157回帰は履歴として保持する。\n\nP05はbase158/candidate161を隔離構築し部分検証に着手したが、runner環境判定と旧QA helperの重複キー契約不一致で停止した。失敗履歴を保持して検証を補正中。候補を本体採用済みとは数えない。全施設/人物/ルート/エンド/図鑑/完成資産/長期プレイ/最大Runtime性能/同一Windows実機は未達を保持する。\n"
|
||||
main_plan.write_text(body)
|
||||
print("Checkpoint linked: current158/12707, limited103 restart, P05 partial rejected; game incomplete.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Integrate root-reviewed day-boundary evidence without promoting acceptance."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import collections
|
||||
import datetime
|
||||
import difflib
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
HERE = Path(__file__).resolve().parent
|
||||
spec = importlib.util.spec_from_file_location("checkpoint_guard", HERE / "review_rescue_scale_checkpoint.py")
|
||||
guard = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(guard)
|
||||
ROOT, PLAN, ART = guard.ROOT, guard.PLAN, guard.ART
|
||||
OUT = ART / "checkpoint-day-boundary-root-r01"
|
||||
PROOF = ART / "p03-day-boundary-root-review-r01.json"
|
||||
NAMES = guard.NAMES
|
||||
read, sha, require, write = guard.read, guard.sha, guard.require, guard.write
|
||||
|
||||
|
||||
def proof_guard():
|
||||
p = read(PROOF)
|
||||
require(p["passed"] and p["actual_checks"] == 183 and p["failures"] == 0, "Actual183/0 root proof")
|
||||
require(len(p["whole_typed_pairs"]) == 12 and p["raw131_pins_rechecked"], "Full typed/raw coverage")
|
||||
require(p["root_rendered_review_pending"] is False and len(p["pngs"]) == 6, "Root reviewed rendered proof")
|
||||
require(all(p[k] is False for k in ("whole_P03_accepted", "whole_A05_accepted", "whole_normal_UI_accepted", "native_Windows_accepted", "performance_accepted")), "Limited scope")
|
||||
seal = ART / "p03-cleaning-day-boundary-paired-r02/evidence-seal.json"
|
||||
require(sha(seal) == p["evidence_seal_sha256"], "Immutable underlying seal")
|
||||
return p
|
||||
|
||||
|
||||
def states(packages):
|
||||
return {p["id"]: {"state": p["state"], "deliveries": {t["id"]: t["state"] for t in p["remaining_deliverables"]}} for p in packages}
|
||||
|
||||
|
||||
def prepare():
|
||||
require(not OUT.exists(), "Fresh checkpoint required")
|
||||
p = proof_guard()
|
||||
source = guard.source()
|
||||
require(source == read(ART / "p05-current-stage-input-r04/candidate-source.json") and len(source) == 1224, "Reviewed source1224")
|
||||
protected = guard.protected()
|
||||
before = {n: sha(PLAN / n) for n in NAMES}
|
||||
packages = read(PLAN / "work_packages.json")
|
||||
original_states = states(packages)
|
||||
snapshot = read(PLAN / "progress_snapshot.json")
|
||||
at = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9))).isoformat()
|
||||
evidence = {"path": PROOF.relative_to(ROOT).as_posix(), "sha256_at_integration": sha(PROOF), "scope": "Controlled public closed-cleaning day-boundary Main Save/Load183/0;12 whole typed pairs,IO1 actual formal recovery,uncapped15events,6PNG; not whole normal play", "linked_at_jst": at, "current_game_acceptance_inferred": False}
|
||||
addition = " 制御した閉店清掃の日跨ぎMain保存/再開183/0・12全状態pair・描画6枚も限定照合。"
|
||||
old_p03 = next(x for x in packages if x["id"] == "P03")["current_summary"]
|
||||
old_p09 = next(x for x in packages if x["id"] == "P09")["current_summary"]
|
||||
for pkg in packages:
|
||||
if pkg["id"] not in ("P03", "P09"):
|
||||
continue
|
||||
pkg["current_summary"] += addition
|
||||
pkg["updated_at_jst"] = at
|
||||
for task in pkg["remaining_deliverables"]:
|
||||
if task["id"] not in ("P03-01", "P03-03", "P09-01"):
|
||||
continue
|
||||
require(not any(x["path"] == evidence["path"] for x in task["evidence"]), "No duplicate evidence")
|
||||
task["evidence"].append(evidence)
|
||||
task["progress_summary"] += addition
|
||||
task["updated_at_jst"] = at
|
||||
require(states(packages) == original_states, "All package/delivery states unchanged")
|
||||
require(dict(collections.Counter(t["state"] for pkg in packages for t in pkg["remaining_deliverables"])) == snapshot["current_delivery_state_counts"], "Delivery counts unchanged")
|
||||
snapshot["integrated_at_jst"] = at
|
||||
snapshot["current_day_boundary_checkpoint"] = {"root_review": evidence, "source_files": 1224, "actual_checks": 183, "failures": 0, "whole_typed_pairs": 12, "uncapped_branch_events_each": 15, "same_process_future_events_each": 6, "rendered_PNG": 6, "actual_IO": p["actual_IO"], "whole_P03_accepted": False, "whole_A05_accepted": False, "whole_normal_UI_accepted": False, "native_Windows_accepted": False, "performance_accepted": False, "source_changes_by_integration": 0, "adoption_receipt": OUT.relative_to(ROOT).as_posix() + "/adoption.json"}
|
||||
section = "\n\n### 閉店清掃の日跨ぎ保存と決算の限定照合\n\n[Root証拠](../../../artifacts/original-completion-2026-10-02/p03-day-boundary-root-review-r01.json)で、Mainの保存/読込handlerから別PID再開を通し183 checks/0、131 raw pins、12全状態typed pairs、元formal4ファイル不変を確認した。公開建設と資源fixtureで用意した閉店清掃施設を23:59(2879分)に保存し、00:00(2880分)の全保有日2G維持費と00:01の再徴収なしを描画6枚(root直接4枚・同一bytes2枚)で照合した。実Loadは通常UIのformal候補を選ぶSAVE_RECOVERY_LOADEDで、Save IO1件のevent_type/payloadだけを同じID/時刻/envelope/save_idに拘束して比較し、event削除・field除外は0。無制限branch journal各15件、同process future各6件も全量照合した。\n\n公開fixtureと制御した時刻進行を含む限定証拠である。全A05、全P03、全通常操作、完成素材、性能、Windowsの受入は未完のまま保持する。既存460/650/249や164回帰と別実行であり、件数を全体受入率へ加算しない。[文書採用記録](../../../artifacts/original-completion-2026-10-02/checkpoint-day-boundary-root-r01/adoption.json)に旧/採用4文書SHAとsource/protected不変を保存する。パッケージ/作業状態、全原子8観点、最終分母と全体率は変更しない。\n"
|
||||
text = (PLAN / "IMPLEMENTATION_PLAN.md").read_text(encoding="utf-8")
|
||||
require(text.count(old_p03) == 3 and text.count(old_p09) == 2, "Exact package/task current statements")
|
||||
text = text.replace(old_p03, old_p03 + addition).replace(old_p09, old_p09 + addition)
|
||||
contents = {"IMPLEMENTATION_PLAN.md": text + section, "PROGRESS_INTEGRATION.md": (PLAN / "PROGRESS_INTEGRATION.md").read_text(encoding="utf-8") + section, "work_packages.json": json.dumps(packages, ensure_ascii=False, indent=2) + "\n", "progress_snapshot.json": json.dumps(snapshot, ensure_ascii=False, indent=2) + "\n"}
|
||||
OUT.mkdir(); (OUT / "before").mkdir(); (OUT / "reviewed").mkdir()
|
||||
for name, value in contents.items():
|
||||
(OUT / "before" / name).write_bytes((PLAN / name).read_bytes())
|
||||
(OUT / "reviewed" / name).write_text(value, encoding="utf-8")
|
||||
diff = difflib.unified_diff((OUT / "before" / name).read_text(encoding="utf-8").splitlines(True), value.splitlines(True), fromfile="before/"+name, tofile="reviewed/"+name)
|
||||
(OUT / (name + ".diff")).write_text("".join(diff), encoding="utf-8")
|
||||
require(guard.source() == source and guard.protected() == protected and all(sha(PLAN / n) == h for n,h in before.items()), "Preparation has no production changes")
|
||||
write(OUT / "preparation.json", {"before": before, "reviewed": {n: sha(OUT / "reviewed" / n) for n in NAMES}, "source": source, "protected": protected, "root_proof_sha256": sha(PROOF), "all_states": original_states, "prepared_at_jst": at, "production_document_writes": 0})
|
||||
print("Prepared day boundary four-document checkpoint; all states/counts unchanged")
|
||||
|
||||
|
||||
def adopt():
|
||||
data = read(OUT / "preparation.json")
|
||||
require(not (OUT / "adoption.json").exists(), "Fresh adoption receipt")
|
||||
proof_guard()
|
||||
require(sha(PROOF) == data["root_proof_sha256"] and guard.source() == data["source"] and guard.protected() == data["protected"], "Root evidence/source/protected pins")
|
||||
require(all(sha(PLAN / n) == h for n,h in data["before"].items()) and all(sha(OUT / "reviewed" / n) == h for n,h in data["reviewed"].items()), "Reviewed document pins")
|
||||
require(states(read(OUT / "reviewed/work_packages.json")) == data["all_states"], "No status promotions")
|
||||
for name in NAMES:
|
||||
temp = PLAN / (name + ".day-boundary-checkpoint-tmp")
|
||||
require(not temp.exists(), "Unexpected temporary file")
|
||||
temp.write_bytes((OUT / "reviewed" / name).read_bytes()); temp.replace(PLAN / name)
|
||||
require(guard.source() == data["source"] and guard.protected() == data["protected"] and all(sha(PLAN / n) == h for n,h in data["reviewed"].items()), "Exact adopted documents/source")
|
||||
write(OUT / "adoption.json", {"passed": True, "before": data["before"], "adopted": data["reviewed"], "root_proof_sha256": sha(PROOF), "source_files": 1224, "source_changes": [], "protected_unchanged": data["protected"], "all_states_unchanged": True, "diff_reviewed_by_root": True, "engine_runs": 0, "completed_deliveries_added": 0, "whole_P03_accepted": False, "whole_A05_accepted": False, "whole_original_accepted": False})
|
||||
print("Adopted day boundary evidence; source0/all states unchanged")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("mode", choices=("prepare", "adopt"))
|
||||
getattr(sys.modules[__name__], parser.parse_args().mode)()
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Record the root-reviewed failed setup diagnostic without accepting P28."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import datetime
|
||||
import difflib
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
HERE = Path(__file__).resolve().parent
|
||||
spec = importlib.util.spec_from_file_location('setup_checkpoint_guard', HERE / 'review_rescue_scale_checkpoint.py')
|
||||
guard = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(guard)
|
||||
ROOT, PLAN, ART = guard.ROOT, guard.PLAN, guard.ART
|
||||
OUT = ART / 'checkpoint-p28-setupspan-root-r01'
|
||||
PROOF = ART / 'p28-setupspan-timeout-root-review-r03.json'
|
||||
NAMES = guard.NAMES
|
||||
|
||||
|
||||
def states(packages):
|
||||
return {p['id']: {'state': p['state'], 'deliveries': {t['id']: t['state'] for t in p['remaining_deliverables']}} for p in packages}
|
||||
|
||||
|
||||
def verify_proof():
|
||||
p = guard.read(PROOF)
|
||||
assert p['actual_timeout_exit'] == -15 and p['strict_timeout_seconds'] == 900.018546084
|
||||
assert p['completed_public_tick_spans'] == 241 and p['whole_tick429_to430_seconds'] == 268.722914
|
||||
assert p['final_target_counts'] is None and p['measurement'] is None and not p['formal_Save_Load']
|
||||
assert not p['P28_accepted'] and not p['whole_game_accepted'] and not p['internal_hotspot_identified']
|
||||
assert guard.sha(ART / 'p28-setupspan-timeout-r03/receipt.json') == p['receipt_sha256']
|
||||
assert guard.sha(ART / 'p28-setupspan-timeout-r03/evidence-seal.json') == p['seal_sha256']
|
||||
return p
|
||||
|
||||
|
||||
def prepare():
|
||||
assert not OUT.exists()
|
||||
proof = verify_proof()
|
||||
source, protected = guard.source(), guard.protected()
|
||||
assert source == guard.read(ART / 'p05-current-stage-input-r04/candidate-source.json') and len(source) == 1224
|
||||
before = {n: guard.sha(PLAN / n) for n in NAMES}
|
||||
packages, snapshot = guard.read(PLAN / 'work_packages.json'), guard.read(PLAN / 'progress_snapshot.json')
|
||||
original_states = states(packages)
|
||||
at = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9))).isoformat()
|
||||
evidence = {'path': PROOF.relative_to(ROOT).as_posix(), 'sha256_at_integration': guard.sha(PROOF), 'scope': 'Actual BUILD900.018546084s/-15 timeout; 241 completed public ticks; whole tick429→430 took268.722914s; last601→602 incomplete; final targets/Save/measurements absent', 'linked_at_jst': at, 'current_game_acceptance_inferred': False}
|
||||
addition = ' setup区間観測の別実行も900.018秒timeout。429→430分の公開tickは268.723秒、601→602分は未完。内部hotspot・最終target数・保存・性能測定は未確定。'
|
||||
pkg = next(p for p in packages if p['id'] == 'P28')
|
||||
old_summary = pkg['current_summary']
|
||||
pkg['current_summary'] += addition
|
||||
pkg['updated_at_jst'] = at
|
||||
task = next(t for t in pkg['remaining_deliverables'] if t['id'] == 'P28-01')
|
||||
assert not any(x['path'] == evidence['path'] for x in task['evidence'])
|
||||
task['evidence'].append(evidence)
|
||||
task['progress_summary'] += addition
|
||||
task['updated_at_jst'] = at
|
||||
assert states(packages) == original_states
|
||||
snapshot['integrated_at_jst'] = at
|
||||
snapshot['current_p28_setupspan_checkpoint'] = {'root_review': evidence, 'source_files': 1224, 'strict_timeout_seconds': proof['strict_timeout_seconds'], 'actual_exit': -15, 'progress_raw_records': 4484, 'completed_public_tick_spans': 241, 'completed_command_spans': 1974, 'whole_tick429_to430_seconds': proof['whole_tick429_to430_seconds'], 'last_public_tick601_to602_completed': False, 'internal_hotspot_identified': False, 'final_target_counts': None, 'formal_Save_Load': False, 'measurements': None, 'population_cap_changed': False, 'P28_accepted': False, 'native_Windows_accepted': False, 'whole_game_accepted': False, 'adoption_receipt': OUT.relative_to(ROOT).as_posix() + '/adoption.json'}
|
||||
section = '\n\n### 最大負荷の準備区間観測と未完の更新\n\n[Root再照合](../../../artifacts/original-completion-2026-10-02/p28-setupspan-timeout-root-review-r03.json)は、同じ現source1224と元の負荷量・900秒制限を保持した別実行の失敗記録である。実BUILDは900.018546084秒でtimeout/-15。28 raw pins、1226 stage noncache pins、実5PID終了、4484進捗行、241完了tick区間を照合した。初期の公開経路グラフrebuildは0.487650秒、429→430分の公開tick全体は268.722914秒。施設建設・客と物流の投入後、601→602分のtickに入って終了せず、最終target数・保存・再読込・x1/x3測定には到達していない。内部関数の実profileは未取得で、hotspotを断定しない。\n\n正式保存とraw証拠を保全したまま、終了済みprivate stageの再生成可能なcacheだけを回収した。通常Operationsのcap100、最大負荷目標、制限時間は変更していない。次は公開Kernel phase境界の観測候補を用意する。P28と全体は未受入、全原子8観点・最終分母・全体率・全package/delivery状態は変更しない。[文書採用記録](../../../artifacts/original-completion-2026-10-02/checkpoint-p28-setupspan-root-r01/adoption.json)に4文書の旧/採用SHAを保持する。\n'
|
||||
body = (PLAN / 'IMPLEMENTATION_PLAN.md').read_text()
|
||||
assert old_summary in body
|
||||
body = body.replace(old_summary, old_summary + addition)
|
||||
body = body.replace('通常Operations cap100を保持しsetup区間の観測候補を準備', '通常Operations cap100を保持。setup別実行も900.018秒timeout、公開tick429→430が268.723秒')
|
||||
values = {'IMPLEMENTATION_PLAN.md': body + section, 'PROGRESS_INTEGRATION.md': (PLAN / 'PROGRESS_INTEGRATION.md').read_text() + section, 'work_packages.json': json.dumps(packages, ensure_ascii=False, indent=2) + '\n', 'progress_snapshot.json': json.dumps(snapshot, ensure_ascii=False, indent=2) + '\n'}
|
||||
OUT.mkdir(); (OUT / 'before').mkdir(); (OUT / 'reviewed').mkdir()
|
||||
for name, value in values.items():
|
||||
(OUT / 'before' / name).write_bytes((PLAN / name).read_bytes())
|
||||
(OUT / 'reviewed' / name).write_text(value)
|
||||
(OUT / (name + '.diff')).write_text(''.join(difflib.unified_diff((PLAN / name).read_text().splitlines(True), value.splitlines(True), fromfile='before/'+name, tofile='reviewed/'+name)))
|
||||
assert guard.source() == source and guard.protected() == protected and all(guard.sha(PLAN / n) == h for n, h in before.items())
|
||||
guard.write(OUT / 'preparation.json', {'before': before, 'reviewed': {n: guard.sha(OUT / 'reviewed' / n) for n in NAMES}, 'source': source, 'protected': protected, 'all_states': original_states, 'root_proof_sha256': guard.sha(PROOF), 'prepared_at_jst': at, 'production_document_writes': 0})
|
||||
print('Prepared failed P28 setup checkpoint; no acceptance or state changes')
|
||||
|
||||
|
||||
def adopt():
|
||||
d = guard.read(OUT / 'preparation.json')
|
||||
verify_proof()
|
||||
assert not (OUT / 'adoption.json').exists()
|
||||
assert guard.sha(PROOF) == d['root_proof_sha256'] and guard.source() == d['source'] and guard.protected() == d['protected']
|
||||
assert all(guard.sha(PLAN / n) == h for n, h in d['before'].items())
|
||||
assert all(guard.sha(OUT / 'reviewed' / n) == h for n, h in d['reviewed'].items())
|
||||
assert states(guard.read(OUT / 'reviewed/work_packages.json')) == d['all_states']
|
||||
for name in NAMES:
|
||||
temp = PLAN / (name + '.p28-setup-checkpoint-tmp')
|
||||
assert not temp.exists()
|
||||
temp.write_bytes((OUT / 'reviewed' / name).read_bytes()); temp.replace(PLAN / name)
|
||||
assert guard.source() == d['source'] and guard.protected() == d['protected']
|
||||
assert all(guard.sha(PLAN / n) == h for n, h in d['reviewed'].items())
|
||||
guard.write(OUT / 'adoption.json', {'passed': True, 'before': d['before'], 'adopted': d['reviewed'], 'root_proof_sha256': guard.sha(PROOF), 'source_files': 1224, 'source_changes': [], 'protected_unchanged': d['protected'], 'all_states_unchanged': True, 'diff_reviewed_by_root': True, 'engine_runs': 0, 'completed_deliveries_added': 0, 'P28_accepted': False, 'whole_original_accepted': False})
|
||||
print('Adopted failed P28 setup evidence; source/state/denominator unchanged')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('mode', choices=['prepare', 'adopt'])
|
||||
getattr(sys.modules[__name__], parser.parse_args().mode)()
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Integrate controlled P06 proof; retain all mandatory unfinished acceptance."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import collections
|
||||
import datetime
|
||||
import difflib
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
HERE = Path(__file__).resolve().parent
|
||||
spec = importlib.util.spec_from_file_location('checkpoint_guard', HERE / 'review_rescue_scale_checkpoint.py')
|
||||
guard = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(guard)
|
||||
ROOT, PLAN, ART = guard.ROOT, guard.PLAN, guard.ART
|
||||
OUT = ART / 'checkpoint-rescue-power-root-r01'
|
||||
PROOF = ART / 'p06-rescue-power-final-root-review-r01.json'
|
||||
NAMES = guard.NAMES
|
||||
read, sha, require, write = guard.read, guard.sha, guard.require, guard.write
|
||||
|
||||
|
||||
def states(packages):
|
||||
return {p['id']: {'state': p['state'], 'deliveries': {t['id']: t['state'] for t in p['remaining_deliverables']}} for p in packages}
|
||||
|
||||
|
||||
def proof_guard():
|
||||
p = read(PROOF)
|
||||
require(p['passed'] and p['actual_checks'] == 691 and p['failures'] == 0 and p['whole_typed_pairs'] == 10 and p['files_rehashed'] == 187, 'Root actual P06 proof')
|
||||
require(all(x['conditions'] == [True]*6 for x in p['private_profile_guards']), 'Actual six private guards')
|
||||
require(all(p[k] is False for k in ['natural_outage_accepted', 'valid_landing_emergency_alighting_accepted', 'whole_P06_accepted', 'normal_UI_accepted', 'native_Windows_accepted', 'whole_game_accepted']), 'Limited scope')
|
||||
require(sha(ART / 'p06-rescue-power-paired-r03/evidence-seal.json') == p['evidence_seal_sha256'], 'Immutable actual seal')
|
||||
require(sha(ART / 'kitchen-and-rescue-power-root-review-r01.json') == p['whole_raw_typed_formal_root_review_sha256'], 'Underlying root typed/raw proof')
|
||||
return p
|
||||
|
||||
|
||||
def prepare():
|
||||
require(not OUT.exists(), 'Fresh checkpoint')
|
||||
p = proof_guard(); source = guard.source(); protected = guard.protected()
|
||||
require(len(source) == 1224 and source == read(ART / 'p05-current-stage-input-r04/candidate-source.json'), 'Current adopted input1224')
|
||||
before = {n: sha(PLAN / n) for n in NAMES}
|
||||
packages = read(PLAN / 'work_packages.json'); old_states = states(packages)
|
||||
snapshot = read(PLAN / 'progress_snapshot.json')
|
||||
at = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9))).isoformat()
|
||||
evidence = {'path': PROOF.relative_to(ROOT).as_posix(), 'sha256_at_integration': sha(PROOF), 'scope': 'Controlled held boarded EV/rescue outage Runtime14 save pair691/0,10 complete typed states,uncapped9events,formal4immutable; emergency alighting/natural outage/UI/native pending', 'linked_at_jst': at, 'current_game_acceptance_inferred': False}
|
||||
addition = ' 制御したEV乗車中救護の停電保存/再開691/0・10全状態pair・無制限journal各9件を限定照合。安全階での緊急降車・自然停電・全館停止・通常UIは未受入。'
|
||||
old_p03 = next(x for x in packages if x['id'] == 'P03')['current_summary']
|
||||
old_p06 = next(x for x in packages if x['id'] == 'P06')['current_summary']
|
||||
for package in packages:
|
||||
if package['id'] not in ['P03', 'P06']: continue
|
||||
package['current_summary'] += addition; package['updated_at_jst'] = at
|
||||
for task in package['remaining_deliverables']:
|
||||
if task['id'] not in ['P03-01', 'P03-03', 'P06-02', 'P06-03']: continue
|
||||
require(not any(x['path'] == evidence['path'] for x in task['evidence']), 'No duplicate evidence')
|
||||
task['evidence'].append(evidence)
|
||||
task['progress_summary'] = (task.get('progress_summary') or '') + addition
|
||||
task['updated_at_jst'] = at
|
||||
if task['id'] in ['P06-02', 'P06-03']:
|
||||
require(task['state'] == 'planned', 'Exact prior planned state')
|
||||
task['state'] = 'in_progress'
|
||||
new_states = states(packages)
|
||||
for pkg in old_states:
|
||||
require(old_states[pkg]['state'] == new_states[pkg]['state'], 'No package state change')
|
||||
for task, old in old_states[pkg]['deliveries'].items():
|
||||
require(new_states[pkg]['deliveries'][task] == ('in_progress' if task in ['P06-02', 'P06-03'] else old), 'Only two actual initiated deliveries')
|
||||
counts = dict(collections.Counter(t['state'] for p in packages for t in p['remaining_deliverables']))
|
||||
require(counts == {'completed': 1, 'in_progress': 15, 'planned': 72}, 'Exact88 delivery counts; no completion added')
|
||||
snapshot['integrated_at_jst'] = at; snapshot['current_delivery_state_counts'] = counts
|
||||
snapshot['current_rescue_power_checkpoint'] = {'root_review': evidence, 'source_files': 1224, 'actual_checks': 691, 'failures': 0, 'whole_typed_pairs': 10, 'uncapped_future_events_each': 9, 'actual_private_guard_conditions_each': 6, 'formal_source_files_immutable': 4, 'source_changes_by_integration': 0, 'state_changes': {'P06-02': 'planned -> in_progress', 'P06-03': 'planned -> in_progress'}, 'whole_P06_accepted': False, 'natural_outage_accepted': False, 'valid_landing_emergency_alighting_accepted': False, 'normal_UI_accepted': False, 'native_Windows_accepted': False, 'adoption_receipt': OUT.relative_to(ROOT).as_posix() + '/adoption.json'}
|
||||
section = '\n\n### EV乗車中救護の停電保存・復帰の限定照合\n\n[Root証拠](../../../artifacts/original-completion-2026-10-02/p06-rescue-power-final-root-review-r01.json)で、現採用Runtime14・source1224のwriter336+別PIDreader355=691 checks/0、187 raw/proof pins、実6条件Guard、10全状態typed pairs、元formal4ファイル不変を確認した。救護搬送中のEV乗車を公開操作で用意し、全停車階の一時閉鎖と実マナ量の差額操作で停止枝を制御している。停止中の同じTask・trip・seat・countdown・人物参照を保持し、元Trafficの全設定と実マナ量を戻した後の最初のtickから、治療29/30分・完了・追加5分まで比較した。IO差は実Save/Loadの同じID・時刻・envelope・save_idに拘束したevent_type/payloadの完全代替だけで、field除外・event削除は0。無制限未来journal各9件もtyped全量一致。\n\n安全な停車階がある場合の緊急降車、自然なマナ枯渇、全館停止、通常UI、描画、Windowsと全P06は未受入。候補r01の証拠名衝突によるwriter282/1・reader未実行も保持した。厨房候補278/0と影響9のS3費用fixture失敗は別証拠として保持し、本体採用や全回帰へ転用しない。P06-02/P06-03のみ実着手に合わせて予定から着手中へ変更し、30パッケージの状態・認定済み完了数・全原子8観点・最終分母と全体率は変更しない。\n'
|
||||
text = (PLAN / 'IMPLEMENTATION_PLAN.md').read_text(encoding='utf-8')
|
||||
require(text.count(old_p03) == 3 and text.count(old_p06) == 1, 'Exact prior summary occurrence counts')
|
||||
text = text.replace(old_p03, old_p03 + addition).replace(old_p06, old_p06 + addition)
|
||||
for task_id in ['P06-02', 'P06-03']:
|
||||
lines = text.splitlines(True)
|
||||
matches = [i for i, line in enumerate(lines) if line.startswith('- ' + task_id + ' ')]
|
||||
require(len(matches) == 1, 'Exact P06 delivery paragraph')
|
||||
i = matches[0]; lines[i] = lines[i].rstrip('\n') + ' 状態: 着手中。' + addition + '\n'
|
||||
text = ''.join(lines)
|
||||
contents = {'IMPLEMENTATION_PLAN.md': text + section, 'PROGRESS_INTEGRATION.md': (PLAN / 'PROGRESS_INTEGRATION.md').read_text(encoding='utf-8') + section, 'work_packages.json': json.dumps(packages, ensure_ascii=False, indent=2) + '\n', 'progress_snapshot.json': json.dumps(snapshot, ensure_ascii=False, indent=2) + '\n'}
|
||||
OUT.mkdir(); (OUT / 'before').mkdir(); (OUT / 'reviewed').mkdir()
|
||||
for name, value in contents.items():
|
||||
(OUT / 'before' / name).write_bytes((PLAN / name).read_bytes())
|
||||
(OUT / 'reviewed' / name).write_text(value, encoding='utf-8')
|
||||
diff = difflib.unified_diff((OUT / 'before' / name).read_text(encoding='utf-8').splitlines(True), value.splitlines(True), fromfile='before/'+name, tofile='reviewed/'+name)
|
||||
(OUT / (name + '.diff')).write_text(''.join(diff), encoding='utf-8')
|
||||
require(guard.source() == source and guard.protected() == protected and all(sha(PLAN / n) == h for n, h in before.items()), 'Preparation no changes')
|
||||
write(OUT / 'preparation.json', {'before': before, 'reviewed': {n: sha(OUT / 'reviewed' / n) for n in NAMES}, 'source': source, 'protected': protected, 'root_proof_sha256': sha(PROOF), 'old_states': old_states, 'new_states': new_states, 'production_document_writes': 0})
|
||||
print('Prepared P06 four-document checkpoint; only planned2 -> in_progress2; no acceptance added')
|
||||
|
||||
|
||||
def adopt():
|
||||
data = read(OUT / 'preparation.json'); proof_guard()
|
||||
require(not (OUT / 'adoption.json').exists(), 'Fresh adoption')
|
||||
require(sha(PROOF) == data['root_proof_sha256'] and guard.source() == data['source'] and guard.protected() == data['protected'], 'Evidence/source/protected pins')
|
||||
require(all(sha(PLAN / n) == h for n, h in data['before'].items()) and all(sha(OUT / 'reviewed' / n) == h for n, h in data['reviewed'].items()), 'Reviewed four documents unchanged')
|
||||
require(states(read(OUT / 'reviewed/work_packages.json')) == data['new_states'], 'Exact reviewed state changes')
|
||||
for name in NAMES:
|
||||
temp = PLAN / (name + '.rescue-power-tmp'); require(not temp.exists(), 'Fresh temporary file')
|
||||
temp.write_bytes((OUT / 'reviewed' / name).read_bytes()); temp.replace(PLAN / name)
|
||||
require(guard.source() == data['source'] and guard.protected() == data['protected'] and all(sha(PLAN / n) == h for n, h in data['reviewed'].items()), 'Exact adopted docs/source')
|
||||
write(OUT / 'adoption.json', {'passed': True, 'before': data['before'], 'adopted': data['reviewed'], 'root_proof_sha256': sha(PROOF), 'source_files': 1224, 'source_changes': [], 'protected_unchanged': data['protected'], 'states_changed_only': ['P06-02', 'P06-03'], 'completed_deliveries_added': 0, 'diff_reviewed_by_root': True, 'engine_runs': 0, 'whole_P06_accepted': False, 'whole_original_accepted': False})
|
||||
print('Adopted P06 controlled evidence; source0/completions0;15 inprogress/72planned')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('mode', choices=['prepare', 'adopt'])
|
||||
getattr(sys.modules[__name__], parser.parse_args().mode)()
|
||||
@@ -0,0 +1,140 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Prepare/adopt four plan documents for the reviewed current Windows subset."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import collections
|
||||
import datetime
|
||||
import difflib
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
PLAN = ROOT / "docs/plans/original-completion-2026-10-02"
|
||||
ART = ROOT / "artifacts/original-completion-2026-10-02"
|
||||
OUT = ART / "checkpoint-windows-current-root-r01"
|
||||
NAMES = ("IMPLEMENTATION_PLAN.md", "PROGRESS_INTEGRATION.md", "work_packages.json", "progress_snapshot.json")
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def read(path):
|
||||
return json.loads(path.read_text())
|
||||
|
||||
|
||||
def dump(value):
|
||||
return json.dumps(value, ensure_ascii=False, indent=2) + "\n"
|
||||
|
||||
|
||||
def require(value, message):
|
||||
if not value:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def source():
|
||||
path = ART / "windows-current-release-adapter-prep-r04/windows_adapter.py"
|
||||
spec = importlib.util.spec_from_file_location("windows_checkpoint_adapter", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module.inventory(ROOT)
|
||||
|
||||
|
||||
def protected():
|
||||
names = ("atomic_requirements.json", "progress_contract.json", "current_evidence_links.json", "atomic_denominator_freeze.json", "source_atomic_coverage.json", "baseline.json", "decisions.json")
|
||||
return {name: sha(PLAN / name) for name in names}
|
||||
|
||||
|
||||
def prepare():
|
||||
require(not OUT.exists(), "Fresh checkpoint required")
|
||||
proof_path = ART / "windows-current-release-root-review-r01/review.json"
|
||||
proof = read(proof_path)
|
||||
require(proof["passed"] and proof["native_Windows"] == "pending" and not proof["whole_original_acceptance"], "Reviewed limited evidence required")
|
||||
pins = read(ART / "windows-current-release-candidate-r03/source-start.json")
|
||||
require(source() == pins, "Source changed after Windows review")
|
||||
at = datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=9))).isoformat()
|
||||
OUT.mkdir()
|
||||
(OUT / "before").mkdir()
|
||||
(OUT / "reviewed").mkdir()
|
||||
before = {name: sha(PLAN / name) for name in NAMES}
|
||||
guard = protected()
|
||||
for name in NAMES:
|
||||
(OUT / "before" / name).write_bytes((PLAN / name).read_bytes())
|
||||
|
||||
def ref(relative, scope):
|
||||
return {"path": relative, "sha256_at_integration": sha(ROOT / relative), "scope": scope, "linked_at_jst": at, "current_game_acceptance_inferred": False}
|
||||
|
||||
review = ref(proof_path.relative_to(ROOT).as_posix(), "Current1224/852-runtime export/ZIP10memberCRC+SHA, macOS samePCK626 raw+5profiles,195kitpins; native Windows pending")
|
||||
paired = ref("artifacts/original-completion-2026-10-02/windows-current-release-same-pck-r01/verification.json", "Actual macOS samePCK5 suites626/0; not native Windows/OS normal operations/performance")
|
||||
package = ref("artifacts/original-completion-2026-10-02/windows-current-release-candidate-r03/package-integrity.json", "Actual release ZIP CRC/all10memberSHA; unmodified official EXE template, finished EXE resources unaccepted")
|
||||
kit = ref("artifacts/original-completion-2026-10-02/native-windows-current-release-kit-r01/kit-manifest.json", "195-file artifact-only native kit; PowerShell parsing/C#compile/native launches remain unverified/0")
|
||||
status = "現Runtime14・1224入力のWindows release候補とZIP全10メンバーのCRC/SHAを照合。macOS上の同一PCK5 suites/626 checks失敗0。Windows実機・完成EXE資産は未受入。"
|
||||
packages = read(PLAN / "work_packages.json")
|
||||
p02 = next(p for p in packages if p["id"] == "P02")
|
||||
p29 = next(p for p in packages if p["id"] == "P29")
|
||||
task = next(t for t in p02["remaining_deliverables"] if t["id"] == "P02-03")
|
||||
task["evidence"] += [review, paired, kit]
|
||||
task["progress_summary"] = status + " native kitは準備済みで、実機機種情報/PowerShell解析/起動は未確認。"
|
||||
task["remaining_gaps"] = ["Windows機の到達性/OS/CPU/GPU/RAM/倍率/解像度と同一配布物の実機起動未確認。", "通常操作/保存終了再起動/日本語パス/音の実聴/Windows描画と性能は未受入。", "候補はapplication/modify_resources=false。完成製品のEXEアイコン等の受入と後続変更後の再書出しが必要。"]
|
||||
task["updated_at_jst"] = at
|
||||
p02["current_summary"] += " 現配布候補の書出し/ZIPとmacOS同一PCK626は限定照合済み。"
|
||||
p29["current_summary"] = status
|
||||
task29 = next(t for t in p29["remaining_deliverables"] if t["id"] == "P29-01")
|
||||
task29["state"] = "in_progress"
|
||||
task29["evidence"] += [review, package]
|
||||
task29["progress_summary"] = status
|
||||
task29["remaining_gaps"] = ["完成EXE資産/全ゲームの最終同一候補受入/後続ゲーム変更後の再書出しは未完。"]
|
||||
task29["updated_at_jst"] = at
|
||||
p29["updated_at_jst"] = at
|
||||
(OUT / "reviewed/work_packages.json").write_text(dump(packages))
|
||||
snapshot = read(PLAN / "progress_snapshot.json")
|
||||
snapshot["integrated_at_jst"] = at
|
||||
snapshot["current_delivery_state_counts"] = dict(collections.Counter(t["state"] for p in packages for t in p["remaining_deliverables"]))
|
||||
snapshot["current_windows_limited_checkpoint"] = {"root_review": review, "same_PCK": paired, "package": package, "native_kit": kit, "source_files": 1224, "runtime_export_inputs": 852, "actual_macOS_same_PCK_suites": 5, "actual_macOS_same_PCK_checks": 626, "native_Windows": "pending", "native_windows_launches": 0, "PowerShell_parser": "unverified", "CSharp_compile": "unverified", "finished_EXE_resources_accepted": False, "full_original_accepted": False, "source_changes_by_integration": 0, "adoption_receipt": "artifacts/original-completion-2026-10-02/checkpoint-windows-current-root-r01/adoption.json"}
|
||||
(OUT / "reviewed/progress_snapshot.json").write_text(dump(snapshot))
|
||||
section = "\n\n### 現版Windows候補の限定照合\n\n[Root照合](../../../artifacts/original-completion-2026-10-02/windows-current-release-root-review-r01/review.json)は、現Runtime14・1224 sourceと852 runtime入力からのrelease EXE/PCK、ZIP全10メンバーCRC/SHAを確認した。EXE `d34d36f3…`、PCK `6445885a…`、ZIP `736cbc48…` の同じ配布パックを用いたmacOS上5 suites/626 checksは失敗0で、各専用profileの実5条件とraw logを照合した。これはWindows実機の起動・通常操作・性能の受入ではない。\n\n195-file native kitは同じ配布SHAと実機情報/profile/rawを記録する準備物。PowerShell解析・C#compile・Windows起動は未確認、実機0を保持する。現候補はclone-only `application/modify_resources=false` で公式template EXEを使うため、完成EXEアイコン等の受入は未完。ENOSPCの部分ZIPと前回0B EXEの失敗履歴を保持し、成功へ書換えていない。P29-01は着手中へ更新し、全体・P02/P29・原子8観点の完了は増やさない。\n\n[文書採用記録](../../../artifacts/original-completion-2026-10-02/checkpoint-windows-current-root-r01/adoption.json)に旧/採用後4文書SHAと変更0のsource/protected台帳を保持する。後続ゲーム変更には別source・配布候補・回帰が必要。\n"
|
||||
text = (PLAN / "IMPLEMENTATION_PLAN.md").read_text()
|
||||
replacements = {
|
||||
"| Windows | 旧1394配布物のCRCと9ファイルmanifestを再確認 | 現採用版の書出し、機種情報、同一EXEでの実機操作・保存再開 |": "| Windows | 現1224 sourceのrelease候補・ZIP全10member CRC/SHA、macOS同一PCK626/0をroot照合 | Windows機種情報・同一EXE実機操作/保存再開・完成EXE資産 |",
|
||||
"状態: 着手中。旧1394配布物の整合確認のみ。同一現版Windows実機・スペック・表示環境は未確認。": "状態: 着手中。現1224 release候補/ZIPをroot照合、macOS同一PCK626/0。native kit準備済み。Windows実機/機種情報/完成EXE資産は未受入。",
|
||||
"現在地: 旧1394版Windows配布物のZIP・PCK検査とmacOSでのPCK実行は合格。現採用版のWindows書出し・同一配布実機受入は未完。": "現在地: " + status,
|
||||
"- P29-01 最新候補の書き出し。採用済みソースだけからWindows EXE/PCKを再生成し、同梱データとZIPを確認する。 受入: ソース・PCK・EXE・ZIPのハッシュ、同梱マニフェスト、CRCが対応する。": "- P29-01 最新候補の書き出し。採用済みソースだけからWindows EXE/PCKを再生成し、同梱データとZIPを確認する。 受入: ソース・PCK・EXE・ZIPのハッシュ、同梱マニフェスト、CRCが対応する。 状態: 着手中。現1224候補/ZIPと同一PCK626/0を限定照合。完成EXE資産・後続変更後の最終候補は未受入。",
|
||||
}
|
||||
for before_text, after_text in replacements.items():
|
||||
require(text.count(before_text) == 1, "Expected plan text missing: " + before_text)
|
||||
text = text.replace(before_text, after_text)
|
||||
(OUT / "reviewed/IMPLEMENTATION_PLAN.md").write_text(text + section)
|
||||
(OUT / "reviewed/PROGRESS_INTEGRATION.md").write_text((PLAN / "PROGRESS_INTEGRATION.md").read_text() + section)
|
||||
after = {name: sha(OUT / "reviewed" / name) for name in NAMES}
|
||||
for name in NAMES:
|
||||
delta = difflib.unified_diff((OUT / "before" / name).read_text().splitlines(True), (OUT / "reviewed" / name).read_text().splitlines(True), fromfile="before/" + name, tofile="reviewed/" + name)
|
||||
(OUT / (name + ".diff")).write_text("".join(delta))
|
||||
require(source() == pins and protected() == guard and all(sha(PLAN / n) == v for n, v in before.items()), "Production changed while preparing")
|
||||
(OUT / "preparation.json").write_text(dump({"before": before, "reviewed": after, "protected": guard, "source_map": pins, "root_review_sha256": sha(proof_path), "prepared_at_jst": at, "production_document_writes": 0, "game_source_changes": [], "completed_deliveries_added": 0}))
|
||||
print("Prepared four reviewed documents; production writes=0")
|
||||
|
||||
|
||||
def adopt():
|
||||
data = read(OUT / "preparation.json")
|
||||
require(not (OUT / "adoption.json").exists(), "Already adopted")
|
||||
require(source() == data["source_map"] and protected() == data["protected"], "Protected/current source changed")
|
||||
require(all(sha(PLAN / n) == v for n, v in data["before"].items()), "Production document changed")
|
||||
require(all(sha(OUT / "reviewed" / n) == v for n, v in data["reviewed"].items()), "Reviewed candidate changed")
|
||||
require(sha(ART / "windows-current-release-root-review-r01/review.json") == data["root_review_sha256"], "Root evidence changed")
|
||||
for name in NAMES:
|
||||
temp = PLAN / (name + ".windows-checkpoint-tmp")
|
||||
require(not temp.exists(), "Unexpected staging file")
|
||||
temp.write_bytes((OUT / "reviewed" / name).read_bytes())
|
||||
temp.replace(PLAN / name)
|
||||
require(source() == data["source_map"] and protected() == data["protected"], "Source/protected changed during adoption")
|
||||
require(all(sha(PLAN / n) == v for n, v in data["reviewed"].items()), "Adopted document SHA differs")
|
||||
(OUT / "adoption.json").write_text(dump({"passed": True, "before": data["before"], "adopted": data["reviewed"], "root_review_sha256": data["root_review_sha256"], "protected_unchanged": data["protected"], "source_files": len(data["source_map"]), "source_changes": [], "document_diff_reviewed_by_root": True, "engine_runs": 0, "new_completed_deliveries": 0, "native_Windows": "pending", "whole_original_acceptance": False}))
|
||||
print("Adopted four plan documents; source/protected changes=0")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("mode", choices=("prepare", "adopt"))
|
||||
args = parser.parse_args()
|
||||
(prepare if args.mode == "prepare" else adopt)()
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Exercise plan-validator rejection guards in temporary planning copies only."""
|
||||
from contextlib import redirect_stdout
|
||||
from datetime import datetime
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import tempfile
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
PLAN = Path(__file__).resolve().parents[1]
|
||||
ROOT = PLAN.parents[2]
|
||||
INPUTS = [
|
||||
"work_packages.json", "requirement_coverage.json", "baseline.json", "decisions.json",
|
||||
"progress_contract.json", "atomic_requirements.json", "atomic_denominator_freeze.json",
|
||||
"current_evidence_links.json", "progress_snapshot.json", "IMPLEMENTATION_PLAN.md",
|
||||
]
|
||||
|
||||
|
||||
def exercise(case, filename, mutate, expected):
|
||||
with tempfile.TemporaryDirectory(prefix="thetree-progress-guard-") as directory:
|
||||
stage = Path(directory) / "docs/plans/original-completion-2026-10-02"
|
||||
stage.mkdir(parents=True)
|
||||
for name in INPUTS:
|
||||
shutil.copyfile(PLAN / name, stage / name)
|
||||
decisions = json.loads((PLAN / "decisions.json").read_text())
|
||||
for decision in decisions["confirmed"]:
|
||||
if "reference" in decision:
|
||||
destination = stage / decision["reference"]
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(PLAN / decision["reference"], destination)
|
||||
data = json.loads((stage / filename).read_text())
|
||||
mutate(data)
|
||||
(stage / filename).write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n")
|
||||
spec = importlib.util.spec_from_file_location("isolated_plan_validation", PLAN / "validate_plan.py")
|
||||
validator = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(validator)
|
||||
validator.PLAN = stage
|
||||
validator.ROOT = ROOT
|
||||
exit_code = 0
|
||||
with redirect_stdout(io.StringIO()):
|
||||
try:
|
||||
validator.main()
|
||||
except SystemExit as exc:
|
||||
exit_code = exc.code
|
||||
result = json.loads((stage / "plan_validation.json").read_text())
|
||||
expected_result = (exit_code == 1 and expected in result["failed_checks"]) if expected else (exit_code == 0 and not result["failed_checks"])
|
||||
return {"case": case, "exit_code": exit_code, "expected_guard": expected,
|
||||
"expected_result": expected_result,
|
||||
"failed_checks": result["failed_checks"]}
|
||||
|
||||
|
||||
def main():
|
||||
rows = [
|
||||
exercise("unmodified_control", "progress_contract.json", lambda data: None, None),
|
||||
exercise("unresolved_final_percentage", "progress_contract.json",
|
||||
lambda data: data.update(total_game_acceptance_percent=1.0),
|
||||
"unresolved_final_denominator_not_reported_as_total"),
|
||||
exercise("stale_static_index_register", "current_evidence_links.json",
|
||||
lambda data: data.update(atomic_register_sha256="0" * 64),
|
||||
"evidence_index_register_pin_matches"),
|
||||
exercise("unreviewed_atom_promoted_to_pass", "atomic_requirements.json",
|
||||
lambda data: data["requirements"][0]["view_results"]["runtime"].update(state="pass"),
|
||||
"unreviewed_atomic_views_remain_unverified"),
|
||||
exercise("P03_whole_package_completed", "work_packages.json",
|
||||
lambda data: next(row for row in data if row["id"] == "P03").update(state="completed"),
|
||||
"P01_P03_not_whole_package_completed"),
|
||||
]
|
||||
report = {"scope": "plan-validator negative guards; isolated document copies, no game execution",
|
||||
"checked_at_jst": datetime.now(ZoneInfo("Asia/Tokyo")).isoformat(),
|
||||
"passed": all(row["expected_result"] for row in rows), "rows": rows,
|
||||
"production_game_source_edits": 0, "new_engine_runs": 0}
|
||||
(PLAN / "progress_validator_guard_checks.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")
|
||||
print(json.dumps(report, ensure_ascii=False))
|
||||
if not report["passed"]:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Reproduce preservation defects, then verify an isolated SaveService fix."""
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "artifacts/original-completion-2026-10-02/p03-save-preservation-r02"
|
||||
PACKET = ROOT / "artifacts/original-completion-2026-10-02/p03-recovery-readonly-candidate"
|
||||
TARGET = "scripts/core/save_service.gd"
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def write(path, value):
|
||||
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
OUT.mkdir(parents=True, exist_ok=False)
|
||||
spec = importlib.util.spec_from_file_location("source_regression", Path(__file__).with_name("run_current_regression.py"))
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
source = module.source_map()
|
||||
assert sha(ROOT / TARGET) == sha(PACKET / "save_service.base.gd")
|
||||
write(OUT / "source-start.json", source)
|
||||
run_id = uuid.uuid4().hex[:12]
|
||||
stages = Path("/private/tmp") / ("thetree-save-preservation-" + run_id)
|
||||
driver_sha = sha(PACKET / "test_recovery_readonly_candidate.gd")
|
||||
report = {"scope": "identified foreign format preservation and explicit read-only recovery", "stages": {}, "rows": [], "production_applied": False, "driver_sha256": driver_sha}
|
||||
for variant in ("base", "candidate"):
|
||||
stage = stages / variant
|
||||
stage.mkdir(parents=True)
|
||||
for name, expected in source.items():
|
||||
dest = stage / name
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(ROOT / name, dest)
|
||||
assert sha(dest) == expected
|
||||
profile = "TheTreeSavePreservation-" + run_id + "-" + variant
|
||||
config = (stage / "project.godot").read_text()
|
||||
(stage / "project.godot").write_text(config.replace("[application]", '[application]\nconfig/use_custom_user_dir=true\nconfig/custom_user_dir_name="' + profile + '"', 1))
|
||||
qa = stage / "tests/diagnostic/save_preservation"
|
||||
qa.mkdir(parents=True)
|
||||
shutil.copyfile(PACKET / "test_recovery_readonly_candidate.gd", qa / "acceptance.gd")
|
||||
assert sha(qa / "acceptance.gd") == driver_sha
|
||||
if variant == "candidate":
|
||||
shutil.copyfile(PACKET / "save_service.candidate.gd", stage / TARGET)
|
||||
report["stages"][variant] = {"path": str(stage), "profile": profile, "target_sha256": sha(stage / TARGET)}
|
||||
write(OUT / "execution.json", report)
|
||||
|
||||
def run(variant, label, script=None, reproduction=False):
|
||||
stage = Path(report["stages"][variant]["path"])
|
||||
command = ["godot", "--headless", "--path", str(stage)]
|
||||
command += ["--script", "res://" + script] if script else ["--editor", "--import"]
|
||||
log = OUT / (label + ".log")
|
||||
started = time.monotonic()
|
||||
with log.open("w") as stream:
|
||||
try:
|
||||
code = subprocess.run(command, cwd=stage, env=dict(os.environ), stdout=stream, stderr=subprocess.STDOUT, timeout=900).returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
code = 124
|
||||
content = log.read_text()
|
||||
counts = re.findall(r"^Recovery read-only candidate: (\d+) checks, (\d+) failures$", content, re.MULTILINE)
|
||||
actual_checks = int(counts[0][0]) if len(counts) == 1 else None
|
||||
if script and not reproduction and actual_checks is None:
|
||||
old_report = json.loads((ROOT / "artifacts/facility-need-pricing-r02-full155-independent-review-2026-10-01-r01/actual155-source-marker-review.json").read_text())
|
||||
old_markers = {row["test"]: row["actual_marker"] for row in old_report["rows"]}
|
||||
marker = old_markers.get(script)
|
||||
if marker is None:
|
||||
matches = re.findall(r"^.*?: \d+ checks, 0 failures\n", content, re.MULTILINE)
|
||||
marker = matches[0] if len(matches) == 1 else None
|
||||
assert marker is not None and content.count(marker) == 1, label
|
||||
match = re.search(r"checks=(\d+)|(\d+)(?: [A-Za-z]+)* checks", marker)
|
||||
assert match is not None
|
||||
actual_checks = int(next(g for g in match.groups() if g is not None))
|
||||
failures = int(counts[0][1]) if len(counts) == 1 else None
|
||||
if reproduction:
|
||||
passed = code != 0 and failures is not None and failures > 0 and "SCRIPT ERROR:" not in content
|
||||
else:
|
||||
passed = code == 0 and "SCRIPT ERROR:" not in content and "\nERROR:" not in content and "Unicode parsing error" not in content
|
||||
row = {"variant": variant, "label": label, "exit_code": code, "passed": passed,
|
||||
"expected_defect_reproduction": reproduction, "checks": actual_checks,
|
||||
"failures": failures, "seconds": round(time.monotonic() - started, 3), "log": log.name, "log_sha256": sha(log)}
|
||||
report["rows"].append(row)
|
||||
write(OUT / "execution.json", report)
|
||||
print(json.dumps(row), flush=True)
|
||||
assert passed, label
|
||||
|
||||
run("base", "base-import")
|
||||
run("candidate", "candidate-import")
|
||||
run("base", "base-defects", "tests/diagnostic/save_preservation/acceptance.gd", True)
|
||||
run("candidate", "candidate-acceptance", "tests/diagnostic/save_preservation/acceptance.gd")
|
||||
for script in ("tests/s6/test_save_preflight.gd", "tests/s6/test_save_operations.gd", "tests/s6/test_runtime_save_lifecycle.gd", "tests/run_tests.gd", "tests/transport/test_transport_save.gd", "tests/ui/test_dimension_migration_ui.gd", "tests/ui/test_supply_management.gd"):
|
||||
if not (Path(report["stages"]["candidate"]["path"]) / script).is_file():
|
||||
# Resolve only exact repository test filenames, never silently skip.
|
||||
raise AssertionError("missing required suite " + script)
|
||||
run("candidate", script.replace("/", "__").replace(".gd", ""), script)
|
||||
after = module.source_map()
|
||||
changes = [p for p in sorted(set(source) | set(after)) if source.get(p) != after.get(p)]
|
||||
write(OUT / "source-end.json", after)
|
||||
report.update(focused_acceptance_passed=True, source_changes=changes, full_merged_regression_pending=True, original_full_acceptance=False)
|
||||
write(OUT / "execution.json", report)
|
||||
assert not changes
|
||||
print("Save preservation focused acceptance passed; production merge and full regression remain pending.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Accept the retained one-file transit guard against the 1396 source baseline.
|
||||
|
||||
Only disposable stages are changed. Whole-state/event oracle comparisons and
|
||||
transport regressions are new runs; full regression follows the merged changes.
|
||||
"""
|
||||
from datetime import datetime
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "artifacts/original-completion-2026-10-02/p03-transit-guard"
|
||||
PACKET = ROOT / "artifacts/path-transit-snapshot-partial-success-guard-candidate-2026-10-01"
|
||||
PATH_TARGET = "scripts/world/path_service.gd"
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def write(path, data):
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
global OUT
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--output", type=Path, default=OUT)
|
||||
args = parser.parse_args()
|
||||
OUT = args.output.resolve()
|
||||
OUT.mkdir(parents=True, exist_ok=False)
|
||||
baseline_map = json.loads((ROOT / "artifacts/facility-need-pricing-production-scale-adoption-2026-10-01-r01/after.json").read_text())
|
||||
pins = json.loads((PACKET / "source-pins.json").read_text())
|
||||
assert baseline_map[PATH_TARGET] == pins["target_base"][PATH_TARGET]
|
||||
assert sha(PACKET / "candidate" / PATH_TARGET) == pins["target_candidate"][PATH_TARGET]
|
||||
assert sha(ROOT / "scripts/world/facility_infrastructure_projector.gd") == pins["unchanged_projector_sha256"]
|
||||
run_id = uuid.uuid4().hex[:12]
|
||||
stage_root = Path("/private/tmp") / ("thetree-original-guard-" + run_id)
|
||||
stage_root.mkdir()
|
||||
assert shutil.disk_usage(stage_root).free > 3 * 1024**3
|
||||
info = {"source_head": subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip(),
|
||||
"started_at_jst": datetime.now(ZoneInfo("Asia/Tokyo")).isoformat(),
|
||||
"baseline_files": len(baseline_map), "stages": {}, "rows": [], "production_applied": False}
|
||||
write(OUT / "source-baseline.json", baseline_map)
|
||||
for variant in ("reference", "candidate"):
|
||||
stage = stage_root / variant
|
||||
stage.mkdir()
|
||||
for name, expected in baseline_map.items():
|
||||
source = ROOT / name
|
||||
destination = stage / name
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if source.is_file() and sha(source) == expected:
|
||||
shutil.copyfile(source, destination)
|
||||
else:
|
||||
# A parallel save fix may already change production; recover
|
||||
# only the exact committed baseline, never mix generations.
|
||||
payload = subprocess.check_output(["git", "show", "HEAD:" + name], cwd=ROOT)
|
||||
assert hashlib.sha256(payload).hexdigest() == expected, name
|
||||
destination.write_bytes(payload)
|
||||
profile_name = "TheTreeGoalGuard-" + run_id + "-" + variant
|
||||
profile = Path.home() / "Library/Application Support" / profile_name
|
||||
project = stage / "project.godot"
|
||||
config = project.read_text()
|
||||
project.write_text(config.replace("[application]", '[application]\nconfig/use_custom_user_dir=true\nconfig/custom_user_dir_name="' + profile_name + '"', 1))
|
||||
qa = stage / "tests/diagnostic/transit_snapshot_guard"
|
||||
qa.mkdir(parents=True)
|
||||
for name in ("frozen_full_snapshot_path.gd", "transit_snapshot_guard_boundary.gd"):
|
||||
shutil.copyfile(PACKET / "qa" / name, qa / name)
|
||||
shutil.copyfile(ROOT / "artifacts/path-snapshot-lazy-default-storage-budget-prep-2026-09-30/drivers/profile_current_whole.gd", qa / "whole.gd")
|
||||
if variant == "candidate":
|
||||
shutil.copyfile(PACKET / "candidate" / PATH_TARGET, stage / PATH_TARGET)
|
||||
info["stages"][variant] = {"path": str(stage), "profile": str(profile), "path_sha256": sha(stage / PATH_TARGET)}
|
||||
write(OUT / "execution.json", info)
|
||||
|
||||
def run(variant, label, script=None, extra=None):
|
||||
stage = Path(info["stages"][variant]["path"])
|
||||
env = dict(os.environ)
|
||||
env.update(THE_TREE_TRANSIT_SNAPSHOT_GUARD_STAGE=str(stage),
|
||||
THE_TREE_TRANSIT_SNAPSHOT_GUARD_PROFILE=info["stages"][variant]["profile"])
|
||||
if extra:
|
||||
env.update(extra)
|
||||
command = ["godot", "--headless", "--path", str(stage)]
|
||||
command += ["--script", "res://" + script] if script else ["--editor", "--import"]
|
||||
start = time.monotonic()
|
||||
log = OUT / (label + ".log")
|
||||
with log.open("w") as stream:
|
||||
try:
|
||||
code = subprocess.run(command, cwd=stage, env=env, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1200).returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
code = 124
|
||||
content = log.read_text()
|
||||
unicode_lines = [line for line in content.splitlines() if "Unicode parsing error" in line]
|
||||
passed = code == 0 and "Godot Engine v4.7.2" in content and not any(t in content for t in ("SCRIPT ERROR:", "\nERROR:"))
|
||||
# The historical importer emits two known NUL diagnostics. Retain and
|
||||
# expose them, while requiring clean script execution for this fix.
|
||||
if script and unicode_lines:
|
||||
passed = False
|
||||
row = {"variant": variant, "label": label, "script": script, "exit_code": code,
|
||||
"seconds": round(time.monotonic() - start, 3), "passed": passed,
|
||||
"unicode_diagnostics": unicode_lines, "diagnostics_clear": not unicode_lines,
|
||||
"log": log.name, "log_sha256": sha(log)}
|
||||
info["rows"].append(row)
|
||||
write(OUT / "execution.json", info)
|
||||
print(json.dumps(row), flush=True)
|
||||
assert passed, label
|
||||
return content
|
||||
|
||||
run("reference", "reference-import")
|
||||
run("candidate", "candidate-import")
|
||||
boundary = run("candidate", "boundary", "tests/diagnostic/transit_snapshot_guard/transit_snapshot_guard_boundary.gd")
|
||||
assert "failures=0" in boundary and boundary.count("TRANSIT_SNAPSHOT_GUARD checks=") == 1
|
||||
oracle = str(stage_root / "whole-oracle.var")
|
||||
for variant, mode in (("reference", "oracle"), ("candidate", "check")):
|
||||
report = OUT / (variant + "-whole.json")
|
||||
run(variant, variant + "-whole", "tests/diagnostic/transit_snapshot_guard/whole.gd",
|
||||
{"THE_TREE_WHOLE_MODE": mode, "THE_TREE_WHOLE_REPORT": str(report),
|
||||
"THE_TREE_WHOLE_ORACLE": oracle, "THE_TREE_PROFILE_VARIANT": variant,
|
||||
"THE_TREE_PROFILE_SOURCE_SHA": hashlib.sha256(json.dumps(baseline_map, sort_keys=True).encode()).hexdigest()})
|
||||
data = json.loads(report.read_text())
|
||||
assert data["failures"] == 0 and data["checked_ticks_including_midpoint_replay"] == 1200
|
||||
assert data["uncompared_delivered_event_count"] == 0
|
||||
for test in ("tests/s1/test_path_service.gd", "tests/s1/test_path_congestion.gd",
|
||||
"tests/transport/test_transport_save.gd", "tests/s1/test_runtime_infrastructure_integration.gd",
|
||||
"tests/s6/test_runtime_save_lifecycle.gd"):
|
||||
run("candidate", test.replace("/", "__"), test)
|
||||
info.update(finished_at_jst=datetime.now(ZoneInfo("Asia/Tokyo")).isoformat(),
|
||||
focused_acceptance_passed=True, full_merged_regression_pending=True,
|
||||
original_full_acceptance=False, performance_acceptance=False,
|
||||
unresolved_import_diagnostics=any(not r["diagnostics_clear"] for r in info["rows"]))
|
||||
write(OUT / "execution.json", info)
|
||||
print("Transit guard focused acceptance passed; merged full regression remains required.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"status": "PASS",
|
||||
"scope": "static audit consistency only; not game/UI/save acceptance",
|
||||
"audit_sha256": "447388ba03a29bfac25f2bcd068665aedbb07ba7641ce86f2db93d44f4c9f326",
|
||||
"source_pins_checked": 49,
|
||||
"original_facilities_checked": 30,
|
||||
"parameter_records_checked": 780,
|
||||
"level_records_checked": 90,
|
||||
"errors": [],
|
||||
"negative_mutants": [
|
||||
{
|
||||
"name": "drop_original",
|
||||
"rejected": true
|
||||
},
|
||||
{
|
||||
"name": "duplicate_id",
|
||||
"rejected": true
|
||||
},
|
||||
{
|
||||
"name": "invent_cannon_size",
|
||||
"rejected": true
|
||||
},
|
||||
{
|
||||
"name": "infer_approval_from_capacity",
|
||||
"rejected": true
|
||||
},
|
||||
{
|
||||
"name": "invent_lv2_value",
|
||||
"rejected": true
|
||||
},
|
||||
{
|
||||
"name": "D03_mandatory_missing",
|
||||
"rejected": true
|
||||
},
|
||||
{
|
||||
"name": "invent_threshold",
|
||||
"rejected": true
|
||||
},
|
||||
{
|
||||
"name": "runtime_pass",
|
||||
"rejected": true
|
||||
},
|
||||
{
|
||||
"name": "final_catalog",
|
||||
"rejected": true
|
||||
},
|
||||
{
|
||||
"name": "playable_undefined",
|
||||
"rejected": true
|
||||
}
|
||||
],
|
||||
"runtime_runs": 0
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"scope": "P28-01 read-only current Runtime mixed-load fixture audit and unexecuted driver proposal",
|
||||
"observed_at_jst": "2026-10-02T01:25:59.616501+09:00",
|
||||
"production_root": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部",
|
||||
"source_pins": {
|
||||
"autoload/simulation_runtime.gd": "d3ad4aab6ded19eb137c5494481cc7333e846746a25b18dbad3c8b663e9f6b72",
|
||||
"autoload/game_clock.gd": "39400634a57ccbf8c584b54a15a259048a95da90b1528330151e45667d65f7f7",
|
||||
"scripts/operations/operations_coordinator.gd": "1c0408d12a659d7f11676765e64ec8850454609290519471f5e8d30da28d540e",
|
||||
"scripts/progression/progression_service.gd": "4f4e00fbeab4a4cec49b7912d8e75381b8354791e0a9efdb8414d984e88ca46f",
|
||||
"scripts/core/performance_budget.gd": "79d18e8f0dcc3932801cd02ea8a8ba037af02ef57665387cba8dbe3b3e9af917",
|
||||
"scripts/core/save_service.gd": "7c107a509b3afccb478ba89d051fbaa9a8c93165326fac8e4cef2249c05879d0",
|
||||
"tests/performance/perf_mvp_001_harness.gd": "63519cf3723d084fa9c30425e49eb210a54a23fd1987df910621f8d12df9e413",
|
||||
"tests/performance/profile_production_scene.gd": "83f288a10963f0331db3d2c71e9f8387a8940c03a1f710276217490f88b8adf5",
|
||||
"tests/s3/test_runtime_actor_cycle.gd": "cbad1505c735f3dab4596f7ad823d6501faf0cea4b9b91cc2b6ff0422296a2de",
|
||||
"tests/s6/test_save_preflight.gd": "49e24247649839b6f50d19b2284368efb7f8f8b189c5010491ad428e97c1c8b9",
|
||||
"scripts/ui/pixel_tower_demo.gd": "d66a245818d77d879b3046a4daea372d8b36d33c7fcce8c7ad3c170206e847ae",
|
||||
"docs/plans/original-completion-2026-10-02/execution/IMPLEMENTATION_PLAN.initial.md": "f9cb3f61e4cb020e37f792c615dfbd4794763884a8ec528de14c3469cf4e5248"
|
||||
},
|
||||
"source_mutated_by_this_audit": false,
|
||||
"performance_driver_executed": false,
|
||||
"full_runtime_mixed_load_accepted": false,
|
||||
"windows_rendering_accepted": false,
|
||||
"requested_target": {
|
||||
"open_cells": 20000,
|
||||
"guests": 200,
|
||||
"staff": 50,
|
||||
"facilities": 150,
|
||||
"queues": "present and counted",
|
||||
"logistics": "present and counted",
|
||||
"events": "present and counted",
|
||||
"origin": "IMPLEMENTATION_PLAN.initial.md:395,450"
|
||||
},
|
||||
"existing_fixtures": [
|
||||
{
|
||||
"path": "tests/performance/perf_mvp_001_harness.gd",
|
||||
"kind": "standalone real-service model composition",
|
||||
"full_target": {
|
||||
"open_cells": 20000,
|
||||
"guests": 200,
|
||||
"staff": 50,
|
||||
"facilities": 150,
|
||||
"queued_requests": 500,
|
||||
"unfinished_logistics": 200,
|
||||
"event_candidates": 50,
|
||||
"simulation_minutes": 1440,
|
||||
"speed": 3
|
||||
},
|
||||
"anchors": {
|
||||
"target": 22,
|
||||
"independent_services": 179,
|
||||
"world_population": 203,
|
||||
"manual_tick": 511,
|
||||
"individual_service_roundtrips": 827
|
||||
},
|
||||
"limitations": [
|
||||
"Does not instantiate SimulationRuntime or GameClock.",
|
||||
"Coordinates services in its own minute loop, not the registered Runtime kernel.",
|
||||
"Service roundtrips are not Runtime SaveService package acceptance.",
|
||||
"Its 200x50x2 map does not fit the canonical Rank 7 bounds.",
|
||||
"No main scene subscribers, frame drawing, GPU or Windows measurement."
|
||||
],
|
||||
"reuse": "service seeding ideas, explicit target counts and invariant assertions only"
|
||||
},
|
||||
{
|
||||
"path": "tests/performance/profile_production_scene.gd",
|
||||
"kind": "actual Runtime small main-scene diagnostic",
|
||||
"anchors": {
|
||||
"runtime_entry": 27,
|
||||
"ui_entry": 34,
|
||||
"clock_measurement": 43,
|
||||
"snapshot_measurement": 66,
|
||||
"limitations": 129
|
||||
},
|
||||
"limitations": [
|
||||
"PixelTowerDemo supplies seven service facilities and four staff, not full target.",
|
||||
"Autosave disabled; synchronous UI refresh is included but rendered frames/GPU are excluded.",
|
||||
"No full Runtime package save/load timing, and no native Windows evidence."
|
||||
],
|
||||
"reuse": "phase signals, x1/x3 sampling, deferred dashboard flush and component breakdown"
|
||||
},
|
||||
{
|
||||
"path": "tests/s3/test_runtime_actor_cycle.gd",
|
||||
"kind": "actual Runtime command-driven actors, service/task Tick and snapshot replay",
|
||||
"anchors": {
|
||||
"initialize": 37,
|
||||
"build": 57,
|
||||
"staff": 79,
|
||||
"spawn": 163,
|
||||
"snapshot": 196,
|
||||
"replay": 210,
|
||||
"day_replay": 220
|
||||
},
|
||||
"limitations": [
|
||||
"One cafeteria, two staff and one guest; correctness proof does not establish 200-guest scale."
|
||||
],
|
||||
"reuse": "normal public commands, actor linkage, synchronized GameClock and digest replay"
|
||||
},
|
||||
{
|
||||
"path": "tests/s6/test_save_preflight.gd",
|
||||
"kind": "standalone large world save-size fixture",
|
||||
"anchors": {
|
||||
"fixture_sizes": 137
|
||||
},
|
||||
"limitations": [
|
||||
"Explicit full_runtime_20000_cell_acceptance=false; cannot certify mixed Runtime load."
|
||||
]
|
||||
}
|
||||
],
|
||||
"production_operations_capacity": {
|
||||
"population_cap": 100,
|
||||
"default_anchor": "scripts/operations/operations_coordinator.gd:83",
|
||||
"normal_enable_anchor": "autoload/simulation_runtime.gd:1446",
|
||||
"spawn_guard_anchor": "autoload/simulation_runtime.gd:5892",
|
||||
"restore_wrapper_anchor": "autoload/simulation_runtime.gd:3160",
|
||||
"restore_canonical_anchor": "autoload/simulation_runtime.gd:3828",
|
||||
"config_locked_after_commit_anchor": "scripts/operations/operations_coordinator.gd:183",
|
||||
"normal_runtime_enable_accepts_cap_override": false,
|
||||
"canonical_restore_accepts_raw_cap_200": false,
|
||||
"conclusion": "200 guests cannot be accepted as current production Operations-enabled normal mixed load by editing the cap.",
|
||||
"resolution_required": "Scope/normal production capacity must be resolved separately; no cap or save baseline changes adopted."
|
||||
},
|
||||
"operations_disabled_200_guest_feasibility": {
|
||||
"assessment": "statically plausible public Runtime diagnostic; unexecuted and unaccepted",
|
||||
"public_spawn_guest_guard_applies_only_when_operations_enabled": true,
|
||||
"disabled_save_wrapper_has_population_cap": false,
|
||||
"public_tick": "GameClock.advance_one_minute / advance_minutes run registered Runtime phases",
|
||||
"public_package_save": "SimulationRuntime.save_to_path (3991)",
|
||||
"public_package_load": "SimulationRuntime.load_from_path (4019)",
|
||||
"guest_actor_and_wallet_callbacks": "public spawn_guest creates GuestRuntime, path actor, wallet and progression lifetime state",
|
||||
"evidence_limit": "The existing actor-cycle proves small Runtime snapshot replay, not 200-guest package save/load.",
|
||||
"admission_statement": "production Operations enabled mixed load未受入"
|
||||
},
|
||||
"world_constraints": {
|
||||
"canonical_rank7_bounds": {
|
||||
"min_x": -60,
|
||||
"max_x": 60,
|
||||
"min_y": -40,
|
||||
"max_y": 56
|
||||
},
|
||||
"canonical_rank7_max_coordinate_slots_two_layers": 23474,
|
||||
"rank6_max_coordinate_slots_two_layers": 13774,
|
||||
"restore_requires_world_bounds_match_progression_and_game_state": "autoload/simulation_runtime.gd:924-954",
|
||||
"rank_promotion_entry": "set_world_tree_rank public command; executes synchronized live metrics and request_rank_up (7636)",
|
||||
"rank7_fixture_currently_confirmed": false,
|
||||
"avoid": [
|
||||
"Do not copy the model 200x50 bounds into Runtime.",
|
||||
"Do not set GameState Rank alone or fabricate a promotion snapshot.",
|
||||
"Do not bypass SpatialSubslot/RouteAdapter/InfrastructureProjector crosslinks."
|
||||
]
|
||||
},
|
||||
"minimal_driver_proposal": {
|
||||
"status": "proposal only; no GDScript added or run during root full regression",
|
||||
"driver_location_if_approved": "tests/performance/profile_runtime_mixed_load.gd in an isolated accepted-source stage",
|
||||
"runtime_path": "real GameClock/GameState/SimulationRuntime; keep Operations disabled for 200-guest diagnostic",
|
||||
"fixture_source": "a legal command-built or already accepted rank-7 Runtime save; existence not yet confirmed",
|
||||
"steps": [
|
||||
"Pin the root adopted source map, Godot binary, machine, seed, driver hash and separate temporary save directory.",
|
||||
"Initialize actual Runtime with clock paused and a fixed seed; disable background autosave only for explicitly labelled Tick measurements.",
|
||||
"Prepare canonical rank-7 state through progression commands and recorded legitimate prerequisites, or load a reviewed compatible save.",
|
||||
"Populate exactly 20000 nonempty/open world cells inside canonical bounds through public world/route APIs; synchronize route projection and reject any failed invariant.",
|
||||
"Build 150 facilities with public build commands and real completion; register 50 staff via public commands, use real shifts, assignments and dispatchable tasks.",
|
||||
"Spawn 200 stable perf-guest IDs via public spawn_guest with real wallets/current cells and exit horizon; verify exact counts and crosslinks.",
|
||||
"Stock real inventories; create real logistics requests, service queues and event candidates with public commands; record actual active and unfinished counts at measurement start/end.",
|
||||
"Create a checkpoint and first require Runtime save_to_path plus fresh Runtime load_from_path digest equality; any failure stops timing and records unsupported fixture.",
|
||||
"Restore identical checkpoint for x1 and x3; measure whole clock.advance_minutes(speed), per-phase cost, path queries, snapshot/digest, synchronous UI refresh and maximum stall.",
|
||||
"Measure explicit Runtime package save/load separately with disk I/O and validation; preserve immutable measurement logs and save hashes.",
|
||||
"Repeat from identical initial digest without concurrent regression work; report average, P95, maximum, sample count and load attrition rather than silently reducing targets.",
|
||||
"Run a separate <=100 guest Operations-enabled canonical diagnostic; report it separately and leave the 200-guest enabled target unaccepted.",
|
||||
"Perform rendered/native Windows measurement only on the same accepted EXE/PCK/ZIP, with CPU/GPU/RAM/resolution/scaling and target FPS recorded after D10 is fixed."
|
||||
],
|
||||
"bootstrap_blockers": [
|
||||
"No currently verified canonical rank-7 mixed Runtime save fixture.",
|
||||
"Normal Operations cap 100 conflicts with requested 200 guest mixed target.",
|
||||
"Pending per-facility required staffing, capacities, prices and other production values remain separate from diagnostic fixture values.",
|
||||
"Full simultaneous queues/logistics/events sustained-load topology has not been validated.",
|
||||
"Windows rendered frame target and native device evidence remain unverified."
|
||||
],
|
||||
"acceptance_exclusions": [
|
||||
"Operations-disabled diagnostic cannot certify production arrivals/daily closing/wages/debt/pauses.",
|
||||
"Headless Tick speed cannot certify rendered frames or native Windows FPS.",
|
||||
"Artificial stock/resource setup must be labelled diagnostic and never presented as normal-play evidence."
|
||||
]
|
||||
},
|
||||
"budgets_current_code_us": {
|
||||
"simulation_x1_average": 4000,
|
||||
"simulation_x3_p95": 12000,
|
||||
"path_query_p95": 2000,
|
||||
"main_thread_stall_maximum": 50000,
|
||||
"save_maximum": 5000000,
|
||||
"load_maximum": 8000000
|
||||
},
|
||||
"measurement_gate": "Root full regression session 43345 currently owns CPU; no extra Godot profile execution in this audit."
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
{
|
||||
"plan_date_jst": "2026-10-02",
|
||||
"scope": "planning consistency and static baseline reconciliation; not game acceptance",
|
||||
"passed": true,
|
||||
"checks": [
|
||||
{
|
||||
"name": "immutable_initial_planning_data_pins",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all30_unique_package_ids",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "unique_delivery_ids",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "initial88_delivery_ids",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "delivery_scope_and_acceptance",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "valid_delivery_states",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "completed_delivery_requires_evidence",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "linked_delivery_evidence_exists",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "initial_remaining_delivery_not_claimed_done",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "known_dependency_ids",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "dependency_graph_acyclic",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all75_original_groups_once",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "feature71_schedule4",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "coverage_known_packages",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "coverage_known_delivery_ids",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "coverage_exact_package_deliveries",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "package_and_requirement_mapping_agree",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "source_lines_preserved",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "old_status_not_promoted",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all10_facility_supplements",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "P09_owns10_supplements",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all10_pending_decision_groups",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "known_decision_references",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all5_confirmed_decisions",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "confirmed_decision_references_exist",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "valid_package_states",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "new_completed_packages_require_evidence",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "acceptance_conditions_not_claimed_results",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "initial_revision_history_preserved",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "initial_revised_state_counts",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "initial_no_new_completed_packages",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "initial_eight_partial_state_promotions",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "initial_atomic_denominator_not_invented",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "progress_contract8_views",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "progress_no_partial_credit",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "unfrozen_denominator_not_assigned",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "progress_schedule_excluded",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "baseline_materials_exist",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "retained_regression155_12310",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "new_game_tests_not_claimed",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all_package_headings",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all_group_rows",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all_delivery_ids_in_document",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "encyclopedia_and_hero_rival_scope",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "current_native_and_longplay_not_claimed",
|
||||
"passed": true
|
||||
}
|
||||
],
|
||||
"failed_checks": [],
|
||||
"packages": 30,
|
||||
"remaining_delivery_units": 88,
|
||||
"current_package_state_counts": {
|
||||
"completed": 1,
|
||||
"in_progress": 25,
|
||||
"planned": 4
|
||||
},
|
||||
"initial_snapshot_checked": true,
|
||||
"historical_source_bytes_compared": false,
|
||||
"covered_original_groups": 75,
|
||||
"missing_original_group_ids": [],
|
||||
"facility_supplements": 10,
|
||||
"atomic_denominator": null,
|
||||
"known_condition_denominator": null,
|
||||
"final_denominator_established": false,
|
||||
"total_game_implementation_percent": null,
|
||||
"total_game_acceptance_percent": null,
|
||||
"new_game_execution_count": 0,
|
||||
"document_sha256": {
|
||||
"ASSETS_PLATFORM_AUDIT.md": "a3a4b8ca0fb39a9abf88692285d26aa879815e092352ba304ef2758e71da50bd",
|
||||
"ATOMIC_REGISTER.md": "6070143b85b7c3b9b73d7c9768d88d2dae4d20407ad54a563cb6ffd74f2373ae",
|
||||
"ATOMIC_SEMANTIC_REVIEW.md": "c156b1fb57c2b4a900f27a19049510f3e9616e723d5edb3417f50d733de5be48",
|
||||
"IMPLEMENTATION_PLAN.md": "7079c74c1f10ee61b8444bcd57e823a9a75f0f15a6acf4df3878ab14cb281eb7",
|
||||
"IMPORT_UNICODE_AUDIT.md": "88b2382bfbb3411fece8be7e6004ba21d8bf6a47e9d1f3cf573ea6638f79c12f",
|
||||
"P09_VALUE_CONTRACTS.md": "c5abdc4543e54033b1fcfd0943cba266f664d8c10968965fd36ade40da488a25",
|
||||
"PERFORMANCE_AUDIT.md": "c75e806acf825fc4a61f773a2cce7e71be335a1e25dbb15d98091097e8dc47c1",
|
||||
"PROGRESS_INTEGRATION.md": "9972bae1ccd5fd6f7d74612edefa5111614e65d0887edc9fe66ff288649214cb",
|
||||
"REVIEW.md": "6665259dfd53be86e113aee502818104dbdb5497da9c817790a24338a4689316",
|
||||
"RUNTIME_SAVE_AUDIT.md": "e88ae014c6154c7aaade5835afc2113f79ee4a4ca5a015c28a4976f8ead68883",
|
||||
"assets_platform_audit.json": "a9fabd989d03071c6af6dc828fff4f021e1ece9638857c54115997923af68d77",
|
||||
"atomic_denominator_freeze.json": "467e960eb0fe47241edb217f5fa2044f2de304441daf9e51a7b0cb46ffa9194d",
|
||||
"atomic_requirements.json": "82f4b4c0fbf32bd296a83e6c565dd2b64d2449336c9ada7be11742761d4a5cfb",
|
||||
"atomic_requirements_validation.json": "8dc300b099cfd90fda862f138b6a528d01e9c488a2c67fe07a00a92a70f99175",
|
||||
"atomic_semantic_review.json": "e300b57f22ab86aa8029ce97231fcff0d7530fd4a9134988f113a46498ada7ad",
|
||||
"baseline.json": "750bb3d313cc5e2ce0d4c79e563a995abd7a78f991dee1dd75c627877b02cd7b",
|
||||
"build_atomic_requirements.py": "6d6ea9e4216372dbd0f9fed3f885a4481b21a3f539a26f64cf98a6830af984c5",
|
||||
"build_facility_value_contracts.py": "35027b23ce75ce9e4e0cdfc5132fd2eb771ba9f598d97750d7ecf9ecb257f769",
|
||||
"current_evidence_links.json": "cd0bf4ef00679bc61936754a2186ab040270e3d1a288b00a6446339b79ae85be",
|
||||
"decisions.json": "ad0673051b0fa601e525f05128e0b11e2dddd9d4bae6a3e0170bbf47da35efe9",
|
||||
"facility_value_contracts.json": "447388ba03a29bfac25f2bcd068665aedbb07ba7641ce86f2db93d44f4c9f326",
|
||||
"facility_value_contracts_validation.json": "bf490a9ca585c48fc318e76c4a069ef244827d828d46e6415399f7710590dd16",
|
||||
"performance_audit.json": "0b630b15a392c3e0366f1362bfe4ddacb968f915f54549f82e4d6fa315384762",
|
||||
"progress_contract.json": "c42a678f16a548ecd93900d4ef6466db0becfb45a6bef76c8278113e14ed9abe",
|
||||
"progress_snapshot.json": "edf988429b67c96e33fdc80332f7ef54f35484c16a9f7a05a16305c0e2804f11",
|
||||
"progress_validator_guard_checks.json": "72ee513fcf1cad85c24bfd0a4b472f425f6acf117928ece0f6b41e29018da627",
|
||||
"progress_validator_guard_checks.r01.json": "14c26adffa5a1938241db4912e50ea54b660641ee6fee2e412d467dce14d0519",
|
||||
"requirement_coverage.json": "8b5788b22ae8f49ea7e5cf048a5f0d82b54f6f193623465e0b70496b5aadd09d",
|
||||
"runtime_save_audit.json": "0c0c3724371c99bc9a2a80378b30d51fec7d3d9499da02e0d6dfeaf99f01de00",
|
||||
"source_atomic_coverage.json": "2ab7b9e6a2536ef248d4f7ccd695bf75a082b47018aca7a5a1b6b706fe1907bc",
|
||||
"validate_atomic_requirements.py": "5b2d0b7d72cb124830fbb0a12d1434e6db698148cc5263d33fd5bd7c2953a1e0",
|
||||
"validate_facility_value_contracts.py": "91640e2a76cd6cc9186f7b85c0bf379b24b6cac0d48962815235ee628e7ae43e",
|
||||
"validate_plan.py": "4cbc339c8cbec8d230b87d7d6124bc05c5996d8e62f8b16afc224b39d82f5183",
|
||||
"validator_update_checks.json": "f997d0f3de2cea29550840b1d5cb23647d64f81329de46758551e2b6c8fecc27",
|
||||
"work_packages.json": "ccd76549097fe7b64cc54ee151ba5a05ea8c572a82576e6545e5c5590b9d484f"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
{
|
||||
"plan_date_jst": "2026-10-02",
|
||||
"scope": "planning consistency and static baseline reconciliation; not game acceptance",
|
||||
"passed": true,
|
||||
"checks": [
|
||||
{
|
||||
"name": "all30_unique_package_ids",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "unique_delivery_ids",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "delivery_scope_and_acceptance",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "valid_delivery_states",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "completed_delivery_requires_evidence",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "linked_delivery_evidence_exists",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "known_dependency_ids",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "dependency_graph_acyclic",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all75_original_groups_once",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "feature71_schedule4",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "coverage_known_packages",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "coverage_known_delivery_ids",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "coverage_exact_package_deliveries",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "package_and_requirement_mapping_agree",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "source_lines_preserved",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "old_status_not_promoted",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all10_facility_supplements",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "P09_owns10_supplements",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all10_pending_decision_groups",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "known_decision_references",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all5_confirmed_decisions",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "confirmed_decision_references_exist",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "valid_package_states",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "new_completed_packages_require_evidence",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "acceptance_conditions_not_claimed_results",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "initial_revision_history_preserved",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "progress_contract8_views",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "progress_no_partial_credit",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "frozen_denominator_has_register",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "known_denominator_matches_frozen_register",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "progress_register_pin_matches",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "evidence_index_register_pin_matches",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "progress_index_pin_matches",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "known_counts_match_freeze",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "unresolved_final_denominator_not_reported_as_total",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "static_leads_not_whole_atom_acceptance",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "declared_limited_views_whole_atoms_unreviewed",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "regression_evidence_pin_matches",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "regression_source_time_scope_preserved",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "P01_P03_not_whole_package_completed",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "progress_schedule_excluded",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "baseline_materials_exist",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "retained_regression155_12310",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "new_game_tests_not_claimed",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all_package_headings",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all_group_rows",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "all_delivery_ids_in_document",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "encyclopedia_and_hero_rival_scope",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"name": "current_native_and_longplay_not_claimed",
|
||||
"passed": true
|
||||
}
|
||||
],
|
||||
"failed_checks": [],
|
||||
"packages": 30,
|
||||
"remaining_delivery_units": 88,
|
||||
"current_package_state_counts": {
|
||||
"completed": 1,
|
||||
"in_progress": 25,
|
||||
"planned": 4
|
||||
},
|
||||
"initial_snapshot_checked": false,
|
||||
"historical_source_bytes_compared": false,
|
||||
"covered_original_groups": 75,
|
||||
"missing_original_group_ids": [],
|
||||
"facility_supplements": 10,
|
||||
"atomic_denominator": null,
|
||||
"known_condition_denominator": 1291,
|
||||
"final_denominator_established": false,
|
||||
"total_game_implementation_percent": null,
|
||||
"total_game_acceptance_percent": null,
|
||||
"new_game_execution_count": 0,
|
||||
"document_sha256": {
|
||||
"ASSETS_PLATFORM_AUDIT.md": "a3a4b8ca0fb39a9abf88692285d26aa879815e092352ba304ef2758e71da50bd",
|
||||
"ATOMIC_REGISTER.md": "6070143b85b7c3b9b73d7c9768d88d2dae4d20407ad54a563cb6ffd74f2373ae",
|
||||
"ATOMIC_SEMANTIC_REVIEW.md": "c156b1fb57c2b4a900f27a19049510f3e9616e723d5edb3417f50d733de5be48",
|
||||
"IMPLEMENTATION_PLAN.md": "e641a5c26957d00945f62e79510c647450298b45266dd6cb299bb7c2179044c6",
|
||||
"IMPORT_UNICODE_AUDIT.md": "88b2382bfbb3411fece8be7e6004ba21d8bf6a47e9d1f3cf573ea6638f79c12f",
|
||||
"P09_VALUE_CONTRACTS.md": "c5abdc4543e54033b1fcfd0943cba266f664d8c10968965fd36ade40da488a25",
|
||||
"PERFORMANCE_AUDIT.md": "c75e806acf825fc4a61f773a2cce7e71be335a1e25dbb15d98091097e8dc47c1",
|
||||
"PROGRESS_INTEGRATION.md": "df1b8fc316b83c8b7c01bd46afcbbce463cb51db15708e5b6b0003888c16d646",
|
||||
"REVIEW.md": "6665259dfd53be86e113aee502818104dbdb5497da9c817790a24338a4689316",
|
||||
"RUNTIME_SAVE_AUDIT.md": "e88ae014c6154c7aaade5835afc2113f79ee4a4ca5a015c28a4976f8ead68883",
|
||||
"assets_platform_audit.json": "a9fabd989d03071c6af6dc828fff4f021e1ece9638857c54115997923af68d77",
|
||||
"atomic_denominator_freeze.json": "467e960eb0fe47241edb217f5fa2044f2de304441daf9e51a7b0cb46ffa9194d",
|
||||
"atomic_requirements.json": "44530bb929fcc8c68c06d0d94da66dd04b8db7680af731b5037cd44677502938",
|
||||
"atomic_requirements_validation.json": "8dc300b099cfd90fda862f138b6a528d01e9c488a2c67fe07a00a92a70f99175",
|
||||
"atomic_semantic_review.json": "e300b57f22ab86aa8029ce97231fcff0d7530fd4a9134988f113a46498ada7ad",
|
||||
"baseline.json": "750bb3d313cc5e2ce0d4c79e563a995abd7a78f991dee1dd75c627877b02cd7b",
|
||||
"build_atomic_requirements.py": "6d6ea9e4216372dbd0f9fed3f885a4481b21a3f539a26f64cf98a6830af984c5",
|
||||
"build_facility_value_contracts.py": "35027b23ce75ce9e4e0cdfc5132fd2eb771ba9f598d97750d7ecf9ecb257f769",
|
||||
"current_evidence_links.json": "db1837bcc8497201134297e312f4b266a297102285307ec3075d4e23632e26c2",
|
||||
"decisions.json": "ad0673051b0fa601e525f05128e0b11e2dddd9d4bae6a3e0170bbf47da35efe9",
|
||||
"facility_value_contracts.json": "447388ba03a29bfac25f2bcd068665aedbb07ba7641ce86f2db93d44f4c9f326",
|
||||
"facility_value_contracts_validation.json": "bf490a9ca585c48fc318e76c4a069ef244827d828d46e6415399f7710590dd16",
|
||||
"performance_audit.json": "0b630b15a392c3e0366f1362bfe4ddacb968f915f54549f82e4d6fa315384762",
|
||||
"progress_contract.json": "ccbdfce8c8365d5c13f59531955708834cce45acd0e136561c95295dfdb5df77",
|
||||
"progress_snapshot.json": "b88b2e754f6e5078cc7d6b97cfd5ec513c9ff065adaae7375e14ad4feb79a8f4",
|
||||
"progress_validator_guard_checks.json": "72ee513fcf1cad85c24bfd0a4b472f425f6acf117928ece0f6b41e29018da627",
|
||||
"progress_validator_guard_checks.r01.json": "14c26adffa5a1938241db4912e50ea54b660641ee6fee2e412d467dce14d0519",
|
||||
"requirement_coverage.json": "8b5788b22ae8f49ea7e5cf048a5f0d82b54f6f193623465e0b70496b5aadd09d",
|
||||
"runtime_save_audit.json": "0c0c3724371c99bc9a2a80378b30d51fec7d3d9499da02e0d6dfeaf99f01de00",
|
||||
"source_atomic_coverage.json": "2ab7b9e6a2536ef248d4f7ccd695bf75a082b47018aca7a5a1b6b706fe1907bc",
|
||||
"validate_atomic_requirements.py": "5b2d0b7d72cb124830fbb0a12d1434e6db698148cc5263d33fd5bd7c2953a1e0",
|
||||
"validate_facility_value_contracts.py": "91640e2a76cd6cc9186f7b85c0bf379b24b6cac0d48962815235ee628e7ae43e",
|
||||
"validate_plan.py": "23678227d9b3c556f1bd903f4b5b3147c4a82ce5085e10055481026803451b80",
|
||||
"validator_update_checks.json": "f997d0f3de2cea29550840b1d5cb23647d64f81329de46758551e2b6c8fecc27",
|
||||
"work_packages.json": "f61c4623d0cdfd05ae37ea4ea86a33cf986976f362f7a51771cccfc378fddeab"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"scope": "Known-condition v1 counting contract linked to the register; final game denominator and acceptance remain unresolved",
|
||||
"denominator_frozen": true,
|
||||
"atomic_register": "atomic_requirements.json",
|
||||
"excluded_from_function_denominator": [
|
||||
"B-14.1",
|
||||
"B-14.2",
|
||||
"B-14.3",
|
||||
"B-14.4"
|
||||
],
|
||||
"counting_unit": "one unique independently judgeable specification condition",
|
||||
"completion_views": [
|
||||
"source_condition",
|
||||
"definition",
|
||||
"runtime",
|
||||
"normal_ui",
|
||||
"save_migration",
|
||||
"boundary_verification",
|
||||
"assets_platform",
|
||||
"evidence"
|
||||
],
|
||||
"implementation_required_views": [
|
||||
"source_condition",
|
||||
"definition",
|
||||
"runtime",
|
||||
"normal_ui",
|
||||
"save_migration"
|
||||
],
|
||||
"acceptance_required_views": "all applicable completion views",
|
||||
"view_states": [
|
||||
"unverified",
|
||||
"not_started",
|
||||
"partial",
|
||||
"blocked",
|
||||
"pass",
|
||||
"fail",
|
||||
"not_applicable"
|
||||
],
|
||||
"not_applicable_requires": [
|
||||
"reason",
|
||||
"source_or_explicit_user_decision",
|
||||
"reviewer"
|
||||
],
|
||||
"partial_credit": false,
|
||||
"equal_count_weight": 1,
|
||||
"implementation_percent": "100 * fully_implemented_atomic_count / applicable frozen denominator; known-only ratio is not a total-game percentage",
|
||||
"acceptance_percent": "100 * fully_accepted_atomic_count / applicable frozen denominator; final total-game percentage remains null until final targets and applicability are established",
|
||||
"unknown_before_freeze": null,
|
||||
"rounding": "Calculate from integer counts, display to two decimal places, retain counts",
|
||||
"atomic_record_required_fields": [
|
||||
"id",
|
||||
"source_anchors",
|
||||
"source_sha256",
|
||||
"user_amendment_refs",
|
||||
"condition",
|
||||
"normal_entry",
|
||||
"implementation_paths",
|
||||
"save_contract",
|
||||
"applicable_completion_views",
|
||||
"view_results",
|
||||
"evidence_refs",
|
||||
"source_head",
|
||||
"primary_package",
|
||||
"responsible_role",
|
||||
"reviewer",
|
||||
"status",
|
||||
"updated_at"
|
||||
],
|
||||
"published_snapshot_required_fields": [
|
||||
"source_head",
|
||||
"denominator_version",
|
||||
"denominator_ids",
|
||||
"implemented_ids",
|
||||
"accepted_ids",
|
||||
"blocked_ids",
|
||||
"partial_ids",
|
||||
"missing_evidence_ids",
|
||||
"difference_ids",
|
||||
"calculated_at"
|
||||
],
|
||||
"denominator_change_requires": [
|
||||
"added_ids",
|
||||
"removed_ids",
|
||||
"merged_ids",
|
||||
"reason",
|
||||
"previous_version",
|
||||
"new_version"
|
||||
],
|
||||
"must_remain_in_denominator": [
|
||||
"mandatory_unimplemented",
|
||||
"mandatory_unverified",
|
||||
"mandatory_undefined_values",
|
||||
"mandatory_unproduced_content"
|
||||
],
|
||||
"must_not_count_as_current_acceptance": [
|
||||
"test_count",
|
||||
"file_count",
|
||||
"package_count",
|
||||
"unadopted_candidate",
|
||||
"old_source_evidence_without_current_applicability",
|
||||
"fallback_asset",
|
||||
"accelerated_replay_as_human_playtime"
|
||||
],
|
||||
"effort_weighted_progress": "Separate future estimate; never relabel atomic count as effort percentage",
|
||||
"denominator_scope": "known-condition v1 only; never final content-target denominator",
|
||||
"denominator_version": "atomic-v1-2026-10-02",
|
||||
"atomic_denominator_freeze": "atomic_denominator_freeze.json",
|
||||
"atomic_register_sha256_at_integration": "44530bb929fcc8c68c06d0d94da66dd04b8db7680af731b5037cd44677502938",
|
||||
"current_evidence_index": "current_evidence_links.json",
|
||||
"current_evidence_index_sha256_at_integration": "db1837bcc8497201134297e312f4b266a297102285307ec3075d4e23632e26c2",
|
||||
"known_condition_denominator": 1291,
|
||||
"final_denominator_established": false,
|
||||
"final_atomic_denominator": null,
|
||||
"overall_atomic_implementation_percent": null,
|
||||
"overall_atomic_acceptance_percent": null,
|
||||
"total_game_implementation_percent": null,
|
||||
"total_game_acceptance_percent": null,
|
||||
"current_whole_atom_implementation_count": 0,
|
||||
"current_whole_atom_acceptance_count": 0,
|
||||
"known_condition_implementation_percent": 0.0,
|
||||
"known_condition_acceptance_percent": 0.0,
|
||||
"known_percent_note": "認定済み全観点原子は0。既存コード不存在やゲーム実装率0の意味ではない。最終全体率はnull。",
|
||||
"atomic_implementation_state": "not_reaudited",
|
||||
"atomic_completion_view_state": "limited_declared_observations_whole_atoms_unreviewed",
|
||||
"progress_snapshot": "progress_snapshot.json",
|
||||
"updated_at_jst": "2026-10-02T01:44:58.035263+09:00",
|
||||
"limited_view_review_manifest": {
|
||||
"manifest": {
|
||||
"path": "../../../artifacts/original-completion-2026-10-02/kitchen-p20-atomic-registry-view-update-candidate-r02/limited-view-manifest.json",
|
||||
"sha256": "1a156219fe74cbbe04c61b486d8819f02817f8dcc06bb86d6e3977dc060ec25c"
|
||||
},
|
||||
"atom_count": 7,
|
||||
"view_count": 45,
|
||||
"state_counts": {
|
||||
"pass": 9,
|
||||
"partial": 36
|
||||
},
|
||||
"whole_atom_implementation_count": 0,
|
||||
"whole_atom_acceptance_count": 0
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"scope": "plan-validator negative guards; isolated document copies, no game execution",
|
||||
"checked_at_jst": "2026-10-02T01:48:01.998279+09:00",
|
||||
"passed": true,
|
||||
"rows": [
|
||||
{
|
||||
"case": "unmodified_control",
|
||||
"exit_code": 0,
|
||||
"expected_guard": null,
|
||||
"expected_result": true,
|
||||
"failed_checks": []
|
||||
},
|
||||
{
|
||||
"case": "unresolved_final_percentage",
|
||||
"exit_code": 1,
|
||||
"expected_guard": "unresolved_final_denominator_not_reported_as_total",
|
||||
"expected_result": true,
|
||||
"failed_checks": [
|
||||
"unresolved_final_denominator_not_reported_as_total"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case": "stale_static_index_register",
|
||||
"exit_code": 1,
|
||||
"expected_guard": "evidence_index_register_pin_matches",
|
||||
"expected_result": true,
|
||||
"failed_checks": [
|
||||
"evidence_index_register_pin_matches",
|
||||
"progress_index_pin_matches"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case": "unreviewed_atom_promoted_to_pass",
|
||||
"exit_code": 1,
|
||||
"expected_guard": "unreviewed_atomic_views_remain_unverified",
|
||||
"expected_result": true,
|
||||
"failed_checks": [
|
||||
"progress_register_pin_matches",
|
||||
"evidence_index_register_pin_matches",
|
||||
"unreviewed_atomic_views_remain_unverified"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case": "P03_whole_package_completed",
|
||||
"exit_code": 1,
|
||||
"expected_guard": "P01_P03_not_whole_package_completed",
|
||||
"expected_result": true,
|
||||
"failed_checks": [
|
||||
"new_completed_packages_require_evidence",
|
||||
"P01_P03_not_whole_package_completed"
|
||||
]
|
||||
}
|
||||
],
|
||||
"production_game_source_edits": 0,
|
||||
"new_engine_runs": 0
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"scope": "plan-validator negative guards; isolated document copies, no game execution",
|
||||
"checked_at_jst": "2026-10-02T01:47:13.255293+09:00",
|
||||
"passed": true,
|
||||
"rows": [
|
||||
{
|
||||
"case": "unresolved_final_percentage",
|
||||
"exit_code": 1,
|
||||
"expected_guard": "unresolved_final_denominator_not_reported_as_total",
|
||||
"rejected_by_expected_guard": true,
|
||||
"failed_checks": [
|
||||
"confirmed_decision_references_exist",
|
||||
"unresolved_final_denominator_not_reported_as_total"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case": "stale_static_index_register",
|
||||
"exit_code": 1,
|
||||
"expected_guard": "evidence_index_register_pin_matches",
|
||||
"rejected_by_expected_guard": true,
|
||||
"failed_checks": [
|
||||
"confirmed_decision_references_exist",
|
||||
"evidence_index_register_pin_matches",
|
||||
"progress_index_pin_matches"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case": "unreviewed_atom_promoted_to_pass",
|
||||
"exit_code": 1,
|
||||
"expected_guard": "unreviewed_atomic_views_remain_unverified",
|
||||
"rejected_by_expected_guard": true,
|
||||
"failed_checks": [
|
||||
"confirmed_decision_references_exist",
|
||||
"progress_register_pin_matches",
|
||||
"evidence_index_register_pin_matches",
|
||||
"unreviewed_atomic_views_remain_unverified"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case": "P03_whole_package_completed",
|
||||
"exit_code": 1,
|
||||
"expected_guard": "P01_P03_not_whole_package_completed",
|
||||
"rejected_by_expected_guard": true,
|
||||
"failed_checks": [
|
||||
"confirmed_decision_references_exist",
|
||||
"new_completed_packages_require_evidence",
|
||||
"P01_P03_not_whole_package_completed"
|
||||
]
|
||||
}
|
||||
],
|
||||
"production_game_source_edits": 0,
|
||||
"new_engine_runs": 0
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,511 @@
|
||||
{
|
||||
"audit_version": 1,
|
||||
"audit_date_jst": "2026-10-02",
|
||||
"recorded_at_jst": "2026-10-02T01:12:01.348281+09:00",
|
||||
"production_root": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部",
|
||||
"baseline_head": "285528b7a3b1b46a2a67af5aa90d9085539b42f4",
|
||||
"working_head": "285528b7a3b1b46a2a67af5aa90d9085539b42f4",
|
||||
"scope": [
|
||||
"P01-02",
|
||||
"P02-01",
|
||||
"P02-02",
|
||||
"P03-01",
|
||||
"P03-02",
|
||||
"P03-03",
|
||||
"P04-01"
|
||||
],
|
||||
"status": "partial_implementation_and_evidence_audit",
|
||||
"p03_complete": false,
|
||||
"original_completion_claim": false,
|
||||
"initial_baseline_source_reconciliation": {
|
||||
"map": "artifacts/facility-need-pricing-production-scale-adoption-2026-10-01-r01/after.json",
|
||||
"checked_paths": 1396,
|
||||
"changed_paths_before_this_turn_edits": [],
|
||||
"note": "Verified at the start of this audit before P03 and Path changes. Receipt reports 155 suites / 12310 checks; no full-engine rerun is claimed by this subtask."
|
||||
},
|
||||
"code_changes": {
|
||||
"status": "working_tree_implemented_pending_root_integration",
|
||||
"sha256": {
|
||||
"autoload/simulation_runtime.gd": "d3ad4aab6ded19eb137c5494481cc7333e846746a25b18dbad3c8b663e9f6b72",
|
||||
"scripts/core/save_service.gd": "7c107a509b3afccb478ba89d051fbaa9a8c93165326fac8e4cef2249c05879d0",
|
||||
"scripts/core/simulation_snapshot.gd": "fc528a3d1db1f439c10f70d6cba2f72bd65abc0d3866eb8b582df60918af0adf",
|
||||
"tests/s6/test_save_preflight.gd": "49e24247649839b6f50d19b2284368efb7f8f8b189c5010491ad428e97c1c8b9"
|
||||
},
|
||||
"new_test": "tests/s6/test_save_preflight.gd",
|
||||
"root_owns_full_regression": true
|
||||
},
|
||||
"contracts": [
|
||||
{
|
||||
"id": "SAVE-PACKAGE",
|
||||
"scope": "formal package",
|
||||
"current_version": 1,
|
||||
"refs": [
|
||||
{
|
||||
"path": "scripts/core/save_service.gd",
|
||||
"line": 4,
|
||||
"sha256": "7c107a509b3afccb478ba89d051fbaa9a8c93165326fac8e4cef2249c05879d0"
|
||||
},
|
||||
{
|
||||
"path": "scripts/core/save_service.gd",
|
||||
"line": 823,
|
||||
"sha256": "7c107a509b3afccb478ba89d051fbaa9a8c93165326fac8e4cef2249c05879d0"
|
||||
}
|
||||
],
|
||||
"stored": [
|
||||
"manifest.json",
|
||||
"world.bin",
|
||||
"thumbnail.png",
|
||||
"journal.log"
|
||||
],
|
||||
"rejection": "Path/symlink, manifest numeric/type, binary header, checksum, advertised size, envelope schema are checked before accepting."
|
||||
},
|
||||
{
|
||||
"id": "SAVE-ENVELOPE",
|
||||
"scope": "generic simulation envelope",
|
||||
"current_version": 1,
|
||||
"refs": [
|
||||
{
|
||||
"path": "scripts/core/simulation_snapshot.gd",
|
||||
"line": 16,
|
||||
"sha256": "fc528a3d1db1f439c10f70d6cba2f72bd65abc0d3866eb8b582df60918af0adf"
|
||||
}
|
||||
],
|
||||
"stored": [
|
||||
"schema_version",
|
||||
"clock",
|
||||
"game_state",
|
||||
"systems"
|
||||
],
|
||||
"rejection": "Exact integral numeric schema/minute/speed, optional typed pause and nonnegative snapshot_version; malformed values refuse before clock/state restoration. Absent legacy pause/version keep defaults; valid old x4 resumes x3."
|
||||
},
|
||||
{
|
||||
"id": "SAVE-RUNTIME",
|
||||
"scope": "live full Runtime restore",
|
||||
"current_version": 13,
|
||||
"supported_runtime_versions": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13
|
||||
],
|
||||
"refs": [
|
||||
{
|
||||
"path": "autoload/simulation_runtime.gd",
|
||||
"line": 569,
|
||||
"sha256": "d3ad4aab6ded19eb137c5494481cc7333e846746a25b18dbad3c8b663e9f6b72"
|
||||
}
|
||||
],
|
||||
"stored": [
|
||||
"command_queue",
|
||||
"ID counters",
|
||||
"domain_events",
|
||||
"RNG streams",
|
||||
"world",
|
||||
"path",
|
||||
"movement credits",
|
||||
"mana",
|
||||
"weather",
|
||||
"economy",
|
||||
"upkeep accrual/settlement",
|
||||
"inventory",
|
||||
"production",
|
||||
"elevators",
|
||||
"stairs",
|
||||
"logistics",
|
||||
"facility operations/cleanliness",
|
||||
"guest/staff",
|
||||
"catalog/progression",
|
||||
"relationships/events",
|
||||
"authority/defense",
|
||||
"construction/excavation",
|
||||
"subslots/route/infrastructure"
|
||||
],
|
||||
"rejection": "Temporary services and crosslinks are validated before swapping live services; v5+ exact canonical outer/runtime keys. Supported legacy aggregates initialize or migrate only through explicit existing version branches."
|
||||
},
|
||||
{
|
||||
"id": "SAVE-PREFLIGHT",
|
||||
"scope": "read-only dimension migration preview",
|
||||
"refs": [
|
||||
{
|
||||
"path": "scripts/core/save_service.gd",
|
||||
"line": 58,
|
||||
"sha256": "7c107a509b3afccb478ba89d051fbaa9a8c93165326fac8e4cef2249c05879d0"
|
||||
},
|
||||
{
|
||||
"path": "autoload/simulation_runtime.gd",
|
||||
"line": 12967,
|
||||
"sha256": "d3ad4aab6ded19eb137c5494481cc7333e846746a25b18dbad3c8b663e9f6b72"
|
||||
}
|
||||
],
|
||||
"mutation_contract": "Named formal package only; no reconciliation/promotion/deletion, no fallback selection and no automatic migration. Missing formal stays missing. Isolated Runtime preview preserves disk and live snapshot."
|
||||
},
|
||||
{
|
||||
"id": "SAVE-COMMIT",
|
||||
"scope": "explicit migrated snapshot promotion",
|
||||
"refs": [
|
||||
{
|
||||
"path": "scripts/core/save_service.gd",
|
||||
"line": 390,
|
||||
"sha256": "7c107a509b3afccb478ba89d051fbaa9a8c93165326fac8e4cef2249c05879d0"
|
||||
},
|
||||
{
|
||||
"path": "autoload/simulation_runtime.gd",
|
||||
"line": 12893,
|
||||
"sha256": "d3ad4aab6ded19eb137c5494481cc7333e846746a25b18dbad3c8b663e9f6b72"
|
||||
}
|
||||
],
|
||||
"mutation_contract": "Expected source snapshot/digest, verified candidate, exact pre_migration backup, atomic promotion and interrupted promotion recovery. Live Runtime unaffected until UI explicitly loads migrated formal."
|
||||
},
|
||||
{
|
||||
"id": "SAVE-RECOVERY",
|
||||
"scope": "ordinary load and candidate recovery",
|
||||
"refs": [
|
||||
{
|
||||
"path": "scripts/core/save_service.gd",
|
||||
"line": 932,
|
||||
"sha256": "7c107a509b3afccb478ba89d051fbaa9a8c93165326fac8e4cef2249c05879d0"
|
||||
},
|
||||
{
|
||||
"path": "autoload/simulation_runtime.gd",
|
||||
"line": 4050,
|
||||
"sha256": "d3ad4aab6ded19eb137c5494481cc7333e846746a25b18dbad3c8b663e9f6b72"
|
||||
}
|
||||
],
|
||||
"mutation_contract": "Ordinary load/recovery inventory may reconcile durable transaction artifacts; cannot be labeled globally read-only. Runtime whitelist repair/fallback are distinct from strict restore and read-only preview."
|
||||
}
|
||||
],
|
||||
"technical_resource_limits": {
|
||||
"full_runtime_20000_cell_acceptance": false,
|
||||
"manifest_limit_bytes": 4194304,
|
||||
"starter_runtime_compressed_bytes": 7511,
|
||||
"starter_runtime_raw_bytes": 61040,
|
||||
"world_cells": 20000,
|
||||
"world_limit_bytes": 268435456,
|
||||
"world_only_compressed_bytes": 18873,
|
||||
"world_only_raw_bytes": 640488
|
||||
},
|
||||
"before_reproductions": [
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p03-save-preflight/before-repro.log",
|
||||
"sha256": "17de92bc9323e13f5c16905e4a05ee0602821aea5560ef9d0d0e9d05573567ec",
|
||||
"facts": [
|
||||
"dimension preview deleted stale formal-save transaction marker",
|
||||
"format_version true / string 1 / fractional 1.5 each loaded successfully"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p03-save-preflight/envelope-before.log",
|
||||
"sha256": "c917d3b772ad97b32cbcbc823590e01873b00c1768bc566381c8c9c93769615b",
|
||||
"facts": [
|
||||
"schema string/bool/fraction, minute string, speed bool/fraction, pause string and negative snapshot_version passed generic validation"
|
||||
]
|
||||
}
|
||||
],
|
||||
"current_final_targeted_evidence": {
|
||||
"phase": "r02",
|
||||
"results": [
|
||||
{
|
||||
"test": "tests/s6/test_save_preflight.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 128,
|
||||
"seconds": 3.15,
|
||||
"log": "tests__s6__test_save_preflight.gd-r02.log"
|
||||
},
|
||||
{
|
||||
"test": "tests/s6/test_save_operations.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 127,
|
||||
"seconds": 2.79,
|
||||
"log": "tests__s6__test_save_operations.gd-r02.log"
|
||||
},
|
||||
{
|
||||
"test": "tests/s6/test_runtime_save_lifecycle.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 96,
|
||||
"seconds": 6.68,
|
||||
"log": "tests__s6__test_runtime_save_lifecycle.gd-r02.log"
|
||||
},
|
||||
{
|
||||
"test": "tests/run_tests.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 73,
|
||||
"seconds": 1.13,
|
||||
"log": "tests__run_tests.gd-r02.log"
|
||||
},
|
||||
{
|
||||
"test": "tests/transport/test_transport_save.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 29,
|
||||
"seconds": 1.7,
|
||||
"log": "tests__transport__test_transport_save.gd-r02.log"
|
||||
},
|
||||
{
|
||||
"test": "tests/ui/test_dimension_migration_ui.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 12,
|
||||
"seconds": 2.33,
|
||||
"log": "tests__ui__test_dimension_migration_ui.gd-r02.log"
|
||||
},
|
||||
{
|
||||
"test": "tests/supply/test_supply_save_migration.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 20,
|
||||
"seconds": 1.18,
|
||||
"log": "tests__supply__test_supply_save_migration.gd-r02.log"
|
||||
}
|
||||
],
|
||||
"suites": 7,
|
||||
"checks": 485,
|
||||
"failed_suites": 0,
|
||||
"source_manifest": "artifacts/original-completion-2026-10-02/p03-save-preflight/source-start.json",
|
||||
"source_change": [],
|
||||
"classification": "Controlled domain / save API tests plus headless actual migration-menu signal-handler test; no native pointer or Windows claim."
|
||||
},
|
||||
"prior_related_evidence": {
|
||||
"phase": "r01_before_integral_float_fix",
|
||||
"results": [
|
||||
{
|
||||
"test": "tests/s1/test_dimension_batch_save.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 23,
|
||||
"seconds": 2.31,
|
||||
"log": "tests__s1__test_dimension_batch_save.gd.log"
|
||||
},
|
||||
{
|
||||
"test": "tests/s1/test_weighted_resize_runtime.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 63,
|
||||
"seconds": 2.14,
|
||||
"log": "tests__s1__test_weighted_resize_runtime.gd.log"
|
||||
},
|
||||
{
|
||||
"test": "tests/staff/test_dormitory_dimension_migration.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 25,
|
||||
"seconds": 2.37,
|
||||
"log": "tests__staff__test_dormitory_dimension_migration.gd.log"
|
||||
},
|
||||
{
|
||||
"test": "tests/ui/test_selection_autosave.gd",
|
||||
"exit_code": 0,
|
||||
"passed": true,
|
||||
"checks": 75,
|
||||
"seconds": 27.26,
|
||||
"log": "tests__ui__test_selection_autosave.gd.log"
|
||||
}
|
||||
],
|
||||
"checks": 186,
|
||||
"note": "Passed before final envelope comparison correction. These logs are historical; do not aggregate as final exact-source acceptance."
|
||||
},
|
||||
"retained_failure_history": [
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p03-save-preflight/preflight-r01.log",
|
||||
"reason": "85 logical checks passed but malformed JSON emitted engine ERROR; phase is not accepted. Loader now uses JSON.parse with returned error."
|
||||
},
|
||||
{
|
||||
"path": "artifacts/original-completion-2026-10-02/p03-save-preflight/tests__s6__test_save_preflight.gd.log",
|
||||
"reason": "128 checks / 1 failure: Array membership rejected integral float speed. Fixed by converting only after numeric integer validation."
|
||||
}
|
||||
],
|
||||
"old_evidence_reconciliation": [
|
||||
{
|
||||
"source_map": "artifacts/snapshot-opening-restart-2026-09-29/source-start.json",
|
||||
"source_map_sha256": "7d8f9fbd35579d4a784b93f18af5178e9302cc3b32bb385d118cb8b4891cbabc",
|
||||
"checked_paths": 283,
|
||||
"changed_paths": [
|
||||
"autoload/simulation_runtime.gd",
|
||||
"data/facilities/expansion/premium_room.tres",
|
||||
"data/facilities/expansion/staff_dormitory.tres",
|
||||
"data/facilities/standard_room.tres",
|
||||
"scenes/main/main.gd",
|
||||
"scenes/main/world_preview.gd",
|
||||
"scripts/core/command_queue.gd",
|
||||
"scripts/core/save_service.gd",
|
||||
"scripts/core/simulation_snapshot.gd",
|
||||
"scripts/data/facility_catalog.gd",
|
||||
"scripts/data/mvp_content_catalog.gd",
|
||||
"scripts/defense/defense_service.gd",
|
||||
"scripts/events/event_action.gd",
|
||||
"scripts/facility/facility_pricing.gd",
|
||||
"scripts/facility/facility_service.gd",
|
||||
"scripts/guest/guest_service.gd",
|
||||
"scripts/operations/operations_coordinator.gd",
|
||||
"scripts/staff/staff_service.gd",
|
||||
"scripts/ui/business_management_panel.gd",
|
||||
"scripts/ui/guest_observation_panel.gd",
|
||||
"scripts/ui/inn_opening_guide.gd",
|
||||
"scripts/ui/layer_interface_skin.gd",
|
||||
"scripts/ui/pixel_tower_art.gd",
|
||||
"scripts/ui/staff_management_panel.gd",
|
||||
"scripts/world/facility_infrastructure_projector.gd",
|
||||
"scripts/world/path_service.gd",
|
||||
"scripts/world/world_service.gd",
|
||||
"tests/environment/test_runtime_weather.gd",
|
||||
"tests/export/test_exported_opening.gd",
|
||||
"tests/fixtures/pre_upkeep_runtime.gd",
|
||||
"tests/mvp/test_operations_coordinator.gd",
|
||||
"tests/mvp/test_runtime_content_progression.gd",
|
||||
"tests/mvp/test_runtime_operations_acceptance.gd",
|
||||
"tests/mvp/test_runtime_operations_integration.gd",
|
||||
"tests/s0/test_definition_registry.gd",
|
||||
"tests/s0/test_runtime_integration.gd",
|
||||
"tests/s1/test_runtime_infrastructure_integration.gd",
|
||||
"tests/s3/test_runtime_path_traffic.gd",
|
||||
"tests/s5/test_defense_service.gd",
|
||||
"tests/s5/test_runtime_defense_cycle.gd",
|
||||
"tests/supply/test_supply_save_migration.gd",
|
||||
"tests/transport/test_runtime_stairs.gd",
|
||||
"tests/transport/test_runtime_transport.gd",
|
||||
"tests/transport/test_transport_density_migration.gd",
|
||||
"tests/transport/test_transport_save.gd",
|
||||
"tests/ui/fixtures/opening_restart_flow.gd",
|
||||
"tests/ui/test_business_management.gd",
|
||||
"tests/ui/test_chapel_gameplay.gd",
|
||||
"tests/ui/test_guest_observation.gd",
|
||||
"tests/ui/test_main_dashboard.gd",
|
||||
"tests/ui/test_staff_management.gd"
|
||||
],
|
||||
"changed_count": 51,
|
||||
"current_exact_source_match": false
|
||||
},
|
||||
{
|
||||
"source_map": "artifacts/original-completion-2026-10-02/p02-opening-restart/source-start.json",
|
||||
"source_map_sha256": "8793934bc0130b5f45610fc597eccd5bc0c87cc06e9fb20769cd93c7183c9b37",
|
||||
"checked_paths": 316,
|
||||
"changed_paths": [
|
||||
"autoload/simulation_runtime.gd",
|
||||
"scripts/core/save_service.gd",
|
||||
"scripts/core/simulation_snapshot.gd",
|
||||
"scripts/world/path_service.gd"
|
||||
],
|
||||
"changed_count": 4,
|
||||
"current_exact_source_match": false
|
||||
}
|
||||
],
|
||||
"normal_ui_entries": [
|
||||
{
|
||||
"path": "scenes/main/main.gd",
|
||||
"line": 2120,
|
||||
"sha256": "032f370394973876bd1c220aae92efa48035bc3788a8c55472513cda8a3a657f"
|
||||
},
|
||||
{
|
||||
"path": "scenes/main/main.gd",
|
||||
"line": 2135,
|
||||
"sha256": "032f370394973876bd1c220aae92efa48035bc3788a8c55472513cda8a3a657f"
|
||||
},
|
||||
{
|
||||
"path": "scenes/main/main.gd",
|
||||
"line": 2762,
|
||||
"sha256": "032f370394973876bd1c220aae92efa48035bc3788a8c55472513cda8a3a657f"
|
||||
},
|
||||
{
|
||||
"path": "scenes/main/main.gd",
|
||||
"line": 2786,
|
||||
"sha256": "032f370394973876bd1c220aae92efa48035bc3788a8c55472513cda8a3a657f"
|
||||
}
|
||||
],
|
||||
"current_update_order": {
|
||||
"source_refs": [
|
||||
{
|
||||
"path": "scripts/core/simulation_kernel.gd",
|
||||
"line": 122,
|
||||
"sha256": "341c0c878c4894adbac628d0db6362ff69fc3347bdd9e61e50936e15dc2d1edb"
|
||||
},
|
||||
{
|
||||
"path": "autoload/simulation_runtime.gd",
|
||||
"line": 1720,
|
||||
"sha256": "d3ad4aab6ded19eb137c5494481cc7333e846746a25b18dbad3c8b663e9f6b72"
|
||||
},
|
||||
{
|
||||
"path": "autoload/simulation_runtime.gd",
|
||||
"line": 4360,
|
||||
"sha256": "d3ad4aab6ded19eb137c5494481cc7333e846746a25b18dbad3c8b663e9f6b72"
|
||||
}
|
||||
],
|
||||
"order": [
|
||||
"APPLY_COMMANDS (can abort before clock advance)",
|
||||
"clock minute advance / midnight signals",
|
||||
"TIME_BOUNDARIES: weather, upkeep, operations (lexical handler order)",
|
||||
"MANA",
|
||||
"CONSTRUCTION",
|
||||
"LOGISTICS",
|
||||
"FACILITY_COMPLETION",
|
||||
"VITALS_AND_ENVIRONMENT",
|
||||
"INCIDENTS_AND_EVENTS: authority, defense, relationships",
|
||||
"FACILITY_QUEUES",
|
||||
"AI_DECISIONS",
|
||||
"PUBLISH_SNAPSHOT then queued checkpoints",
|
||||
"clock snapshot_version increment / tick_completed / time_changed"
|
||||
],
|
||||
"daily_autosave": "06:00 deduplicated by saved last_daily_checkpoint_day; approved maintenance settlement is separate at 00:00",
|
||||
"all_boundary_semantics_accepted": false
|
||||
},
|
||||
"remaining_gaps": [
|
||||
{
|
||||
"id": "P03-G01",
|
||||
"package": "P03-01",
|
||||
"state": "partial",
|
||||
"issue": "Per-feature authoritative save contracts for not-yet-implemented needs charging, all original facilities/people, story/endings and encyclopedia are not complete. Existing aggregate serialization is a foundation, not full original acceptance."
|
||||
},
|
||||
{
|
||||
"id": "P03-G02",
|
||||
"package": "P03-02",
|
||||
"state": "unverified",
|
||||
"issue": "Dimension migration accepted legacy premium/dormitory batch and partial premium construction. Full simultaneous layouts, actors on removed cells, coordinated task destination changes, in-flight elevator/rescue/logistics migration still lack accepted cases."
|
||||
},
|
||||
{
|
||||
"id": "P03-G03",
|
||||
"package": "P03-02",
|
||||
"state": "unset",
|
||||
"decision": "D07",
|
||||
"issue": "RelationshipState retains trust/respect/affection/fear/dependence. Old five metrics to original three metrics/route meaning is undecided; no guessed conversion is applied."
|
||||
},
|
||||
{
|
||||
"id": "P03-G04",
|
||||
"package": "P03-03",
|
||||
"state": "unverified",
|
||||
"issue": "Fresh-process normal opening covers the available inn over three closes with explicit timed-domain-event exclusion. Paired cross-process midway delivery, cleaning, defense, all unique progression and every original facility are still missing."
|
||||
},
|
||||
{
|
||||
"id": "P03-G05",
|
||||
"package": "P03-01",
|
||||
"state": "unverified",
|
||||
"issue": "256 MiB world / 4 MiB manifest limits are technical resource policies. Largest full Runtime world with all actors/services/content remains unmeasured; legitimate oversized legacy saves now refuse with explicit size-limit code."
|
||||
},
|
||||
{
|
||||
"id": "P04-G01",
|
||||
"package": "P04-01",
|
||||
"state": "partial",
|
||||
"decision": "D05",
|
||||
"issue": "Current phase order can be listed, but all simultaneous closing/power/destruction/exit/charge/shift/00:00 cases have not been accepted. Undecided cost/EV/boundary semantics remain unset."
|
||||
},
|
||||
{
|
||||
"id": "P02-G01",
|
||||
"package": "P02-02",
|
||||
"state": "unverified",
|
||||
"issue": "Headless/rendered UI signal-handler fixtures are distinct from OS pointer input and native Windows. Current candidate must rerun the same process-restart evidence after source mutations."
|
||||
}
|
||||
],
|
||||
"overall_atomic_acceptance_percent": null,
|
||||
"overall_atomic_implementation_percent": null,
|
||||
"denominator_fixed_by_this_subtask": false
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Independently validate P01 register/coverage/frozen IDs, never game completion.
|
||||
Usage: python3 validate_atomic_requirements.py [--self-test]
|
||||
Negative checks mutate in-memory documents only and must be rejected.
|
||||
"""
|
||||
import argparse, copy, hashlib, json, re
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone, timedelta
|
||||
ROOT=Path(__file__).resolve().parents[3]; HERE=Path(__file__).resolve().parent
|
||||
BASE='docs/traceability/original/BASELINE.txt'
|
||||
VIEWS=['source_condition','definition','runtime','normal_ui','save_migration','boundary_verification','assets_platform','evidence']
|
||||
STATES={'unverified','not_started','partial','blocked','pass','fail','not_applicable'}
|
||||
FIELDS=['id','source_anchors','source_sha256','user_amendment_refs','condition','normal_entry','implementation_paths','save_contract','applicable_completion_views','view_results','evidence_refs','source_head','primary_package','responsible_role','reviewer','status','updated_at','acceptance','implementation_state','acceptance_state']
|
||||
sha=lambda p:hashlib.sha256((ROOT/p).read_bytes()).hexdigest()
|
||||
|
||||
def validate(reg,cov,freeze):
|
||||
errors=[]
|
||||
def require(ok,msg):
|
||||
if not ok:errors.append(msg)
|
||||
source_lines=(ROOT/BASE).read_text().splitlines()
|
||||
source_groups=json.loads((ROOT/'docs/traceability/original/requirements.json').read_text())
|
||||
reqs=reg.get('requirements',[]); byid={r.get('id'):r for r in reqs}
|
||||
require(len(byid)==len(reqs),'duplicate requirement ID')
|
||||
require(reg.get('source_pins',{}).get(BASE)==sha(BASE),'baseline pin mismatch')
|
||||
for p,digest in reg.get('source_pins',{}).items():require((ROOT/p).is_file() and sha(p)==digest,'source pin mismatch: '+p)
|
||||
require(reg.get('final_denominator_established') is False,'final denominator claimed despite unresolved catalogs')
|
||||
require(reg.get('final_catalog_coverage_claimed') is False,'all catalog entries claimed despite unresolved catalogs')
|
||||
require(any(x.get('scope')=='additional_named_facilities_and_infrastructure_applicability' and len(x.get('source_examples',[]))>=9 for x in reg.get('open_expansion_register',[])), 'other named facilities/infrastructure applicability expansion missing')
|
||||
require(set(reg.get('completion_views',[]))==set(VIEWS),'eight views missing')
|
||||
inverse={n:set() for n in range(1,len(source_lines)+1)}
|
||||
packages={p['id'] for p in json.loads((HERE/'work_packages.json').read_text())}
|
||||
feature=set(); schedule=set()
|
||||
for r in reqs:
|
||||
rid=r.get('id','<missing>')
|
||||
require(bool(re.fullmatch(r'AR\.[A-Za-z0-9_.-]+',rid)),rid+': invalid semantic ID')
|
||||
for field in FIELDS:require(field in r,rid+': missing '+field)
|
||||
require(bool(r.get('condition')),rid+': no condition')
|
||||
require(r.get('mandatory') is True and r.get('equal_count_weight')==1,rid+': changed mandatory weight')
|
||||
require(r.get('primary_package') in packages,rid+': invalid primary package')
|
||||
require(set(r.get('view_results',{}))==set(VIEWS),rid+': eight result views missing')
|
||||
require(set(r.get('applicable_completion_views',[]))<=set(VIEWS),rid+': unknown applicability view')
|
||||
require(bool(r.get('source_anchors')),rid+': no source anchor')
|
||||
require(r.get('source_sha256')==sha(BASE),rid+': original SHA mismatch')
|
||||
for a in r.get('source_anchors',[]):
|
||||
p=a.get('path'); nums=a.get('lines',[])
|
||||
require(p in reg.get('source_pins',{}),rid+': unpinned anchor')
|
||||
require(a.get('sha256')==reg.get('source_pins',{}).get(p),rid+': anchor SHA mismatch')
|
||||
require(bool(nums) and all(isinstance(n,int) and 1<=n<=len((ROOT/p).read_text().splitlines()) for n in nums),rid+': invalid source lines')
|
||||
if p==BASE:
|
||||
for n in nums:
|
||||
if n in inverse:inverse[n].add(rid)
|
||||
for v,result in r.get('view_results',{}).items():
|
||||
state=result.get('state');require(state in STATES,rid+': unknown view state '+str(state))
|
||||
if state=='pass':
|
||||
require(bool(result.get('evidence_refs')) and bool(result.get('reviewer')),rid+': unsupported PASS '+v)
|
||||
if state=='not_applicable':
|
||||
require(bool(result.get('not_applicable_reason')) and bool(result.get('reviewer')) and bool(result.get('source_or_explicit_user_decision')),rid+': unsupported N/A '+v)
|
||||
if r.get('implementation_state')=='implemented':
|
||||
require(all(r['view_results'].get(v,{}).get('state') in ['pass','not_applicable'] for v in VIEWS[:5]),rid+': incomplete implementation counted')
|
||||
if r.get('acceptance_state')=='accepted':
|
||||
require(all(r['view_results'].get(v,{}).get('state') in ['pass','not_applicable'] for v in r['applicable_completion_views']),rid+': incomplete acceptance counted')
|
||||
scope=r.get('scope')
|
||||
require(scope in ['feature_or_content','historical_schedule'],rid+': invalid scope')
|
||||
(schedule if scope=='historical_schedule' else feature).add(rid)
|
||||
if scope=='historical_schedule':
|
||||
require(all(g.startswith('B-14.') for g in r.get('source_group_ids',[])),rid+': functional group in schedule')
|
||||
for dep in r.get('decision_dependencies',[]):
|
||||
if dep['decision_id']=='D03':require(dep['kind']=='informational_explicitly_unset_optional_extension',rid+': explicit unset option treated as required decision')
|
||||
require(set(reg.get('denominator_ids',[]))==feature and len(reg.get('denominator_ids',[]))==len(feature),'register denominator changed/duplicated')
|
||||
require(set(reg.get('historical_schedule_ids',[]))==schedule,'schedule IDs mismatch')
|
||||
require(freeze.get('denominator_version')==reg.get('denominator_version'),'freeze version mismatch')
|
||||
require(freeze.get('source_head')==reg.get('source_head'),'freeze source HEAD mismatch')
|
||||
require(freeze.get('source_pins')==reg.get('source_pins'),'freeze source pins mismatch')
|
||||
require(freeze.get('denominator')==len(feature) and set(freeze.get('denominator_ids',[]))==feature,'frozen denominator mismatch')
|
||||
require(set(freeze.get('historical_schedule_ids',[]))==schedule and freeze.get('historical_schedule_count')==len(schedule),'frozen schedule mismatch')
|
||||
require(freeze.get('final_denominator_established') is False,'freeze falsely establishes final denominator')
|
||||
implemented={r['id'] for r in reqs if r.get('scope')=='feature_or_content' and r.get('implementation_state')=='implemented'}
|
||||
accepted={r['id'] for r in reqs if r.get('scope')=='feature_or_content' and r.get('acceptance_state')=='accepted'}
|
||||
require(set(freeze.get('implemented_ids',[]))==implemented,'implemented numerator IDs mismatch')
|
||||
require(set(freeze.get('accepted_ids',[]))==accepted,'accepted numerator IDs mismatch')
|
||||
require(freeze.get('implementation_percent')==100*len(implemented)/len(feature),'implementation percentage not recomputable')
|
||||
require(freeze.get('acceptance_percent')==100*len(accepted)/len(feature),'acceptance percentage not recomputable')
|
||||
clines=cov.get('source_lines',[])
|
||||
require(cov.get('source_sha256')==sha(BASE) and cov.get('line_count')==len(source_lines),'coverage baseline mismatch')
|
||||
require([x.get('line') for x in clines]==list(range(1,len(source_lines)+1)),'coverage has missing/duplicate source lines')
|
||||
for row in clines:
|
||||
n=row.get('line');ids=set(row.get('atomic_ids',[]))
|
||||
require(bool(ids) and ids<=set(byid),f'line {n}: absent/unknown IDs')
|
||||
if isinstance(n,int) and 1<=n<=len(source_lines):
|
||||
require(row.get('line_sha256')==hashlib.sha256(source_lines[n-1].encode()).hexdigest(),f'line {n}: content hash mismatch')
|
||||
if row.get('kind')=='condition':require(ids==inverse[n],f'line {n}: condition anchor correspondence mismatch')
|
||||
cgroups={g.get('group_id'):g for g in cov.get('groups',[])}
|
||||
require(len(cgroups)==75 and set(cgroups)=={g['id'] for g in source_groups},'75 groups missing/duplicated')
|
||||
for g in source_groups:
|
||||
row=cgroups.get(g['id'],{});expected={r['id'] for r in reqs if g['id'] in r.get('source_group_ids',[])}
|
||||
require(row.get('source_lines')==g['source_lines'] and set(row.get('atomic_ids',[]))==expected and bool(expected),g['id']+': group coverage mismatch')
|
||||
facilities=cov.get('facility_ids',[]);heroes=cov.get('heroine_ids',[])
|
||||
require(len(facilities)==30 and len({f.get('key') for f in facilities})==30,'original 30 facilities missing')
|
||||
require(len(heroes)==7 and len({h.get('key') for h in heroes})==7,'original 7 heroines missing')
|
||||
# Known facility fields and 360 finite, explicitly specified visual slots must exist.
|
||||
fields=['exists','footprint','construction_gold','upkeep_gold','upkeep_mana','service_price','role','operation_state','customer_capacity','staff_min','staff_max','related_facility','related_operating','service_attribute']
|
||||
for f in facilities:
|
||||
key=f['key']
|
||||
for field in fields:require('AR.facility.'+key+'.'+field in byid,key+': missing facility condition '+field)
|
||||
for lv in range(1,4):
|
||||
for view in ['front','back']:
|
||||
for part in ['exterior','interior']:require(f'AR.asset.facility.{key}.lv{lv}.{view}.{part}' in byid,key+': missing finished asset slot')
|
||||
for h in heroes:
|
||||
for route in ['love','ntr','corruption']:require(f'AR.heroine.{h["key"]}.route.{route}' in byid,h['key']+': route missing')
|
||||
for d in ['A01','A02','A03','A04','A05']:
|
||||
expected={r['id'] for r in reqs if d in r.get('user_amendment_refs',[])}
|
||||
got=[row for row in cov.get('approved_decisions',[]) if row.get('decision_id')==d]
|
||||
require(len(got)==1 and bool(expected) and set(got[0]['atomic_ids'])==expected,d+': approval coverage mismatch')
|
||||
for n in range(1,11):
|
||||
fid=f'F{n:02}';expected={r['id'] for r in reqs if fid in r.get('supplement_ids',[])}
|
||||
got=[row for row in cov.get('facility_supplements',[]) if row.get('supplement_id')==fid]
|
||||
require(len(got)==1 and bool(expected) and set(got[0]['atomic_ids'])==expected,fid+': supplement coverage mismatch')
|
||||
required_contracts=['AR.progression.all_endings','AR.progression.encyclopedia_complete','AR.platform.windows','AR.progression.main_duration','AR.progression.full_duration']
|
||||
for rid in required_contracts:require(rid in feature,'missing release condition: '+rid)
|
||||
return errors
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser();parser.add_argument('--self-test',action='store_true');args=parser.parse_args()
|
||||
reg=json.loads((HERE/'atomic_requirements.json').read_text());cov=json.loads((HERE/'source_atomic_coverage.json').read_text());freeze=json.loads((HERE/'atomic_denominator_freeze.json').read_text())
|
||||
errors=validate(reg,cov,freeze);negative=[]
|
||||
if args.self_test:
|
||||
def mutate(label,callback):
|
||||
r,c,f=copy.deepcopy(reg),copy.deepcopy(cov),copy.deepcopy(freeze);callback(r,c,f)
|
||||
found=validate(r,c,f);negative.append({'case':label,'rejected':bool(found),'first_error':found[0] if found else None})
|
||||
mutate('duplicate_id',lambda r,c,f:r['requirements'].append(copy.deepcopy(r['requirements'][0])))
|
||||
mutate('source_line_deleted',lambda r,c,f:c['source_lines'].pop())
|
||||
mutate('source_hash_changed',lambda r,c,f:r['source_pins'].__setitem__(BASE,'0'*64))
|
||||
mutate('frozen_denominator_changed',lambda r,c,f:f['denominator_ids'].pop())
|
||||
mutate('unsupported_pass',lambda r,c,f:r['requirements'][0]['view_results']['runtime'].__setitem__('state','pass'))
|
||||
mutate('unsupported_not_applicable',lambda r,c,f:r['requirements'][0]['view_results']['runtime'].__setitem__('state','not_applicable'))
|
||||
mutate('partial_implementation_counted',lambda r,c,f:r['requirements'][0].__setitem__('implementation_state','implemented'))
|
||||
mutate('unverified_acceptance_counted',lambda r,c,f:r['requirements'][0].__setitem__('acceptance_state','accepted'))
|
||||
mutate('all_content_final_claim',lambda r,c,f:r.__setitem__('final_denominator_established',True))
|
||||
mutate('facility_removed',lambda r,c,f:r['requirements'].__setitem__(slice(None),[x for x in r['requirements'] if x['id']!='AR.facility.chapel.exists']))
|
||||
mutate('heroine_route_removed',lambda r,c,f:r['requirements'].__setitem__(slice(None),[x for x in r['requirements'] if x['id']!='AR.heroine.anri.route.love']))
|
||||
mutate('schedule_counted_as_function',lambda r,c,f:next(x for x in r['requirements'] if x['scope']=='historical_schedule').__setitem__('scope','feature_or_content'))
|
||||
mutate('altered_percentage',lambda r,c,f:f.__setitem__('implementation_percent',50.0))
|
||||
ok=not errors and all(x['rejected'] for x in negative)
|
||||
result={'validator':'P01 register structure and source coverage, not game acceptance','passed':ok,
|
||||
'checked_at_jst':datetime.now(timezone(timedelta(hours=9))).isoformat(timespec='seconds'),
|
||||
'source_head':reg['source_head'],'known_condition_v1_denominator':len(reg['denominator_ids']),
|
||||
'historical_schedule_atoms':len(reg['historical_schedule_ids']),'original_lines':cov['line_count'],'original_groups':75,
|
||||
'original_facilities':len(cov['facility_ids']),'heroines':len(cov['heroine_ids']),'heroine_routes':21,
|
||||
'implemented_whole_atoms':len(freeze['implemented_ids']),'accepted_whole_atoms':len(freeze['accepted_ids']),
|
||||
'all_evidence_unverified':all(x['acceptance_state']=='unverified' for x in reg['requirements']),
|
||||
'final_denominator_established':False,'errors':errors,'negative_checks':negative}
|
||||
(HERE/'atomic_requirements_validation.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n')
|
||||
print(json.dumps(result,ensure_ascii=False,indent=2))
|
||||
raise SystemExit(0 if ok else 1)
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Independent static contract consistency checks, not Runtime acceptance."""
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
OUT = Path(__file__).resolve().parent
|
||||
ROOT = OUT.parents[2]
|
||||
TARGET = OUT / "facility_value_contracts.json"
|
||||
EXPECTED_NAMES = ["一般客室", "上級客室", "VIPスイート", "ドミトリー", "大衆食堂", "レストラン", "酒場", "屋台", "大浴場", "カジノ", "闘技場", "礼拝堂", "風俗個室", "高級ソープ", "SM拷問室", "闇市場", "厨房", "倉庫", "待機室", "従業員寮", "警備員詰所", "清掃員室", "医務室", "覗き部屋", "地下牢", "マナ炉", "蔓の昇降機", "業務用EV", "ワープ", "魔法砲台"]
|
||||
EXPECTED_SIZES = [[2,1], [4,1], [6,2], [4,1], [4,2], [6,2], [4,2], [2,1], [6,2], [6,3], [8,4], [4,3], [2,1], [4,2], [4,2], [4,2], [4,2], [4,2], [4,2], [2,1], [2,2], [2,1], [4,2], [2,1], [4,2], [6,3], [2,2], [3,2], [2,2], None]
|
||||
EXPECTED_CUSTOMER = {"chapel": 8, "cleaning_room": 0}
|
||||
MANDATORY_PARAMETER_KEYS = {"construction_cost", "build_minutes", "maintenance_gold", "maintenance_mana", "base_price", "footprint", "layer", "zone", "required_roles", "primary_skill", "staff_min", "service_types", "default_service_type", "service_profiles", "base_service_minutes", "customer_capacity", "capacity", "queue_capacity", "staff_max", "service_attribute", "max_level", "upgrade_costs", "unlock_rank", "research_requirements", "policy_schema", "faith_per_service"}
|
||||
|
||||
|
||||
def validate(doc, check_files=True):
|
||||
errors = []
|
||||
def require(test, label):
|
||||
if not test:
|
||||
errors.append(label)
|
||||
rows = doc.get("original_facilities", [])
|
||||
require(len(rows) == 30, "original30count")
|
||||
require([r.get("name") for r in rows] == EXPECTED_NAMES, "original30identity_order")
|
||||
require(len({r.get("id") for r in rows}) == len(rows), "unique_contract_ids")
|
||||
require(sum(r.get("definition_exists", False) for r in rows) == 17, "defined17")
|
||||
require(sum(r.get("normal_registered_static", False) for r in rows) == 16, "normal16")
|
||||
require([r.get("definition_id") for r in rows if r.get("registration_state") == "definition_only"] == ["black_market"], "black_market_definition_only")
|
||||
require(len(doc.get("missing_definition_blueprints", [])) == 13, "blueprints13")
|
||||
require(len(doc.get("supplemental_current_definitions", [])) == 20, "other20definitions")
|
||||
customer_states = Counter()
|
||||
for i, r in enumerate(rows):
|
||||
parms = r.get("parameters", {})
|
||||
require(set(parms) == MANDATORY_PARAMETER_KEYS, f"{r.get('id')}:parameter_keys")
|
||||
footprint = parms.get("footprint", {}).get("original")
|
||||
require((footprint or {}).get("value") == EXPECTED_SIZES[i], f"{r.get('id')}:source_size")
|
||||
require(set(r.get("major_items", {})) == {f"F{i:02d}" for i in range(1, 11)}, f"{r.get('id')}:major10")
|
||||
require([lv.get("level") for lv in r.get("level_contracts", [])] == [1,2,3], f"{r.get('id')}:level123")
|
||||
for lv in r.get("level_contracts", []):
|
||||
for k in ["authoritative_level_specific_price", "authoritative_level_specific_customer_capacity", "authoritative_level_specific_staff_min", "authoritative_level_specific_staff_max", "unlock_condition", "policy_unlock_assignment"]:
|
||||
require(lv.get(k) is None, f"{r.get('id')}:no_invented_level_value:{k}")
|
||||
require(lv.get("base_approval_has_explicit_level_scope") is False, f"{r.get('id')}:no_invented_approval_scope")
|
||||
cp = parms.get("customer_capacity", {})
|
||||
customer_states[cp.get("value_status")] += 1
|
||||
fid = r.get("definition_id")
|
||||
approval = cp.get("individual_or_formula_approval")
|
||||
if fid in EXPECTED_CUSTOMER:
|
||||
require((approval or {}).get("value") == EXPECTED_CUSTOMER[fid], f"{fid}:approved_customer")
|
||||
else:
|
||||
require(approval is None, f"{fid}:no_customer_approval_inferred")
|
||||
require(r.get("generic_capacity_contract", {}).get("is_customer_capacity") is False, f"{fid}:capacity_distinct")
|
||||
require(r.get("runtime_acceptance_state") == "unverified_no_engine_run_in_this_audit", f"{fid}:no_runtime_pass")
|
||||
match_inputs = r.get("need_match_inputs", {})
|
||||
require(match_inputs.get("deficit_threshold", {}).get("value") is None, f"{fid}:threshold_unset")
|
||||
require(match_inputs.get("surcharge_amount_or_rate", {}).get("value") is None, f"{fid}:surcharge_unset")
|
||||
if check_files and fid:
|
||||
path = ROOT / r["definition_path"]
|
||||
raw_fields = {m[1]: m[2] for line in path.read_text().splitlines() if (m := re.match(r"(\w+) = (.+)$", line))}
|
||||
for k, parm in parms.items():
|
||||
observed = parm.get("current", {})
|
||||
if observed.get("origin") == "explicit_tres":
|
||||
require(raw_fields.get(k) == observed.get("raw"), f"{fid}:{k}:raw_current_match")
|
||||
require(customer_states == Counter({"approved_initial_trial_value":2, "current_value_without_individual_approval":2, "explicitly_unset_required_contract":13, "not_set_no_definition":13}), "customer_state_counts")
|
||||
for bp in doc.get("missing_definition_blueprints", []):
|
||||
require(bp.get("production_id") is None and bp.get("register_and_allow_build_now") is False, "blueprint_not_playable")
|
||||
common = doc.get("common_contracts", {})
|
||||
require(common.get("operation_state", {}).get("authoritative_states") == ["INACTIVE", "OPERATING", "SUSPENDED"], "three_operation_states")
|
||||
for item in [common.get("operation_state", {}).get("resumption_conditions", {}), common.get("related_facility", {}).get("multi_relation_AND_OR", {})]:
|
||||
require(item.get("value") is None and item.get("decision_id") == "D03" and item.get("is_mandatory_missing") is False, "D03_not_mandatory_gap")
|
||||
require(common.get("need_match_surcharge", {}).get("multiple_attributes_and_duplicate_surcharge", {}).get("is_mandatory_missing") is False, "optional_multiple_attributes_not_gap")
|
||||
require(common.get("need_match_surcharge", {}).get("guest_need_effects_are_attribute_assignment") is False, "effect_not_attribute")
|
||||
require(common.get("need_match_surcharge", {}).get("guest_need_0_100_is_deficit_threshold") is False, "range_not_threshold")
|
||||
summary = doc.get("summary", {})
|
||||
require(summary.get("is_final_all_facility_catalog") is False and summary.get("approved_runtime_acceptances_counted") == 0, "no_final_acceptance_claim")
|
||||
require(summary.get("optional_unset_D03_counted_as_mandatory_missing") == 0, "no_optional_gap_count")
|
||||
require(doc.get("acceptance", {}).get("completed") is False and doc.get("acceptance", {}).get("runtime_runs_in_this_audit") == 0, "audit_not_complete_runtime")
|
||||
require(set(doc.get("approvals", {})) == {f"A{i:02d}" for i in range(1,6)}, "A01_A05_preserved")
|
||||
pins = doc.get("source_pins", {})
|
||||
if check_files:
|
||||
for path, sha in pins.items():
|
||||
require(hashlib.sha256((ROOT / path).read_bytes()).hexdigest() == sha, f"source_sha:{path}")
|
||||
def walk(value):
|
||||
if isinstance(value, dict):
|
||||
if all(k in value for k in ["path", "lines", "sha256", "authority"]):
|
||||
require(pins.get(value["path"]) == value["sha256"], f"anchor_pin:{value['path']}")
|
||||
if check_files:
|
||||
count = len((ROOT / value["path"]).read_text().splitlines())
|
||||
require(bool(value["lines"]) and all(1 <= n <= count for n in value["lines"]), f"anchor_lines:{value['path']}")
|
||||
for v in value.values():
|
||||
walk(v)
|
||||
elif isinstance(value, list):
|
||||
for v in value:
|
||||
walk(v)
|
||||
walk(doc)
|
||||
return errors
|
||||
|
||||
|
||||
doc = json.loads(TARGET.read_text())
|
||||
errors = validate(doc)
|
||||
mutations = {
|
||||
"drop_original": lambda d: d["original_facilities"].pop(),
|
||||
"duplicate_id": lambda d: d["original_facilities"][1].update(id=d["original_facilities"][0]["id"]),
|
||||
"invent_cannon_size": lambda d: d["original_facilities"][-1]["parameters"]["footprint"].update(original={"value":[2,2]}),
|
||||
"infer_approval_from_capacity": lambda d: d["original_facilities"][0]["parameters"]["customer_capacity"].update(individual_or_formula_approval={"value":2}),
|
||||
"invent_lv2_value": lambda d: d["original_facilities"][0]["level_contracts"][1].update(authoritative_level_specific_price=150),
|
||||
"D03_mandatory_missing": lambda d: d["common_contracts"]["operation_state"]["resumption_conditions"].update(is_mandatory_missing=True),
|
||||
"invent_threshold": lambda d: d["original_facilities"][0]["need_match_inputs"]["deficit_threshold"].update(value=75),
|
||||
"runtime_pass": lambda d: d["original_facilities"][0].update(runtime_acceptance_state="PASS"),
|
||||
"final_catalog": lambda d: d["summary"].update(is_final_all_facility_catalog=True),
|
||||
"playable_undefined": lambda d: d["missing_definition_blueprints"][0].update(register_and_allow_build_now=True),
|
||||
}
|
||||
mutant_results = []
|
||||
for name, mutate in mutations.items():
|
||||
changed = copy.deepcopy(doc)
|
||||
mutate(changed)
|
||||
mutant_results.append({"name":name, "rejected":bool(validate(changed, check_files=False))})
|
||||
receipt = {"status": "PASS" if not errors and all(m["rejected"] for m in mutant_results) else "FAIL",
|
||||
"scope": "static audit consistency only; not game/UI/save acceptance",
|
||||
"audit_sha256": hashlib.sha256(TARGET.read_bytes()).hexdigest(),
|
||||
"source_pins_checked": len(doc["source_pins"]), "original_facilities_checked":30,
|
||||
"parameter_records_checked":780, "level_records_checked":90,
|
||||
"errors":errors, "negative_mutants":mutant_results, "runtime_runs":0}
|
||||
(OUT / "facility_value_contracts_validation.json").write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + "\n")
|
||||
print(json.dumps(receipt, ensure_ascii=False, indent=2))
|
||||
raise SystemExit(0 if receipt["status"] == "PASS" else 1)
|
||||
@@ -0,0 +1,276 @@
|
||||
"""Validate planning data only; this does not execute or accept the game."""
|
||||
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import collections
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
|
||||
PLAN = Path(__file__).resolve().parent
|
||||
ROOT = PLAN.parents[2]
|
||||
|
||||
|
||||
def read(name):
|
||||
return json.loads((PLAN / name).read_text())
|
||||
|
||||
|
||||
def digest(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def limited_view_registry_errors(atomic, declaration):
|
||||
"""Validate declared view payloads against an immutable prior register."""
|
||||
errors = []
|
||||
|
||||
def require(ok, message):
|
||||
if not ok:
|
||||
raise ValueError(message)
|
||||
|
||||
try:
|
||||
ref = declaration["manifest"]
|
||||
path = (PLAN / ref["path"]).resolve()
|
||||
require(digest(path) == ref["sha256"], "limited view manifest SHA")
|
||||
patch = json.loads(path.read_text())
|
||||
require(type(patch["schema_version"]) is int and patch["schema_version"] == 1
|
||||
and type(patch["whole_atom_status_changes"]) is int
|
||||
and patch["whole_atom_status_changes"] == 0, "limited view manifest scope")
|
||||
baseline_ref = patch["baseline_registry"]
|
||||
baseline_path = (PLAN / baseline_ref["path"]).resolve()
|
||||
require(digest(baseline_path) == baseline_ref["sha256"], "limited view baseline SHA")
|
||||
baseline = json.loads(baseline_path.read_text())
|
||||
expected = json.loads(json.dumps(baseline))
|
||||
by_id = {row["id"]: row for row in expected["requirements"]}
|
||||
matrix = patch["view_results_patch"]
|
||||
require(type(matrix) is dict and bool(matrix), "limited view nonempty ID matrix")
|
||||
require(set(matrix) <= set(by_id), "limited view unknown requirement ID")
|
||||
allowed_views = set(baseline["completion_views"])
|
||||
allowed_states = set(read("progress_contract.json")["view_states"])
|
||||
declared_counts = collections.Counter()
|
||||
reference_pins = patch["reference_pins"]
|
||||
for rid, views in matrix.items():
|
||||
require(type(views) is dict and bool(views) and set(views) <= allowed_views,
|
||||
"limited view empty/unknown view: " + rid)
|
||||
for view, result in views.items():
|
||||
require(type(result) is dict and result.get("state") in allowed_states
|
||||
and result.get("state") != "unverified", "limited view state: " + rid)
|
||||
require(type(result.get("evidence_refs")) is list
|
||||
and bool(result["evidence_refs"]) and bool(result.get("reviewer")),
|
||||
"limited view missing evidence/reviewer: " + rid)
|
||||
if result["state"] == "not_applicable":
|
||||
require(bool(result.get("not_applicable_reason"))
|
||||
and bool(result.get("source_or_explicit_user_decision")),
|
||||
"limited view unsupported not-applicable: " + rid)
|
||||
declared_counts[result["state"]] += 1
|
||||
for evidence in result["evidence_refs"] + [result["current_adoption_evidence"]]:
|
||||
require(type(evidence) is str, "limited view reference type: " + rid)
|
||||
filename, separator, fragment = evidence.partition("#")
|
||||
require(separator == "#" and bool(fragment) and evidence in reference_pins,
|
||||
"limited view resolved reference: " + rid)
|
||||
evidence_path = (PLAN / filename).resolve()
|
||||
require(digest(evidence_path) == reference_pins[evidence],
|
||||
"limited view evidence SHA: " + rid)
|
||||
selected = json.loads(evidence_path.read_text())
|
||||
for key in fragment.split("/"):
|
||||
selected = selected[key]
|
||||
require(bool(selected), "limited view empty referenced fragment: " + rid)
|
||||
by_id[rid]["view_results"][view] = result
|
||||
for key in ["atom_count", "view_count"]:
|
||||
require(type(declaration[key]) is int and type(patch[key]) is int,
|
||||
"limited view count type: " + key)
|
||||
require(len(matrix) == declaration["atom_count"] == patch["atom_count"],
|
||||
"limited view atom count")
|
||||
require(sum(declared_counts.values()) == declaration["view_count"] == patch["view_count"],
|
||||
"limited view observation count")
|
||||
require(type(declaration["state_counts"]) is dict and type(patch["state_counts"]) is dict
|
||||
and all(type(value) is int for value in declaration["state_counts"].values())
|
||||
and dict(declared_counts) == declaration["state_counts"] == patch["state_counts"],
|
||||
"limited view state counts")
|
||||
require(json.dumps(atomic, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
||||
== json.dumps(expected, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False),
|
||||
"limited view type-exact payload / untouched registry mismatch")
|
||||
for row in atomic["requirements"]:
|
||||
require(row["implementation_state"] == "not_reaudited"
|
||||
and row["acceptance_state"] == row["status"] == "unverified",
|
||||
"limited evidence changed whole-atom status: " + row["id"])
|
||||
except (OSError, ValueError, KeyError, TypeError, AttributeError) as error:
|
||||
errors.append(type(error).__name__ + ": " + str(error))
|
||||
return errors
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--initial-snapshot", action="store_true", help="Also verify the immutable initial planning state")
|
||||
parser.add_argument("--verify-baseline", action="store_true", help="Also compare current source bytes to the historical baseline")
|
||||
args = parser.parse_args()
|
||||
packages = read("execution/work_packages.initial.json" if args.initial_snapshot else "work_packages.json")
|
||||
coverage = read("requirement_coverage.json")
|
||||
baseline = read("baseline.json")
|
||||
decisions = read("decisions.json")
|
||||
contract = read("execution/progress_contract.initial.json" if args.initial_snapshot else "progress_contract.json")
|
||||
checks = []
|
||||
|
||||
def check(name, condition):
|
||||
checks.append({"name": name, "passed": bool(condition)})
|
||||
|
||||
if args.initial_snapshot:
|
||||
initial = read("execution/PLANNING_DATA_INITIAL_SNAPSHOT.json")
|
||||
check("immutable_initial_planning_data_pins", all(
|
||||
(PLAN / name).is_file() and digest(PLAN / name) == value
|
||||
for name, value in initial["sha256"].items()
|
||||
))
|
||||
|
||||
ids = [row["id"] for row in packages]
|
||||
expected = {f"P{i:02d}" for i in range(30)}
|
||||
check("all30_unique_package_ids", len(ids) == 30 and set(ids) == expected)
|
||||
tasks = [task for row in packages for task in row["remaining_deliverables"]]
|
||||
task_ids = {task["id"] for task in tasks}
|
||||
check("unique_delivery_ids", len(tasks) == len(task_ids) and all(p["remaining_deliverables"] for p in packages))
|
||||
if args.initial_snapshot:
|
||||
check("initial88_delivery_ids", len(tasks) == 88)
|
||||
check("delivery_scope_and_acceptance", all(t["work"] and t["acceptance"] for t in tasks))
|
||||
states = {"planned", "in_progress", "blocked", "completed"}
|
||||
check("valid_delivery_states", all(t["state"] in states for t in tasks))
|
||||
check("completed_delivery_requires_evidence", all(t["state"] != "completed" or t["evidence"] for t in tasks))
|
||||
check("linked_delivery_evidence_exists", all(
|
||||
isinstance(ref, dict) and (ROOT / ref["path"]).is_file()
|
||||
for task in tasks for ref in task["evidence"]
|
||||
))
|
||||
if args.initial_snapshot:
|
||||
check("initial_remaining_delivery_not_claimed_done", all(t["state"] == "planned" and not t["evidence"] for t in tasks))
|
||||
check("known_dependency_ids", all(set(row["depends_on"]) <= expected for row in packages))
|
||||
by_id = {row["id"]: row for row in packages}
|
||||
visited, active = set(), set()
|
||||
|
||||
def acyclic(node):
|
||||
if node in active:
|
||||
return False
|
||||
if node in visited:
|
||||
return True
|
||||
active.add(node)
|
||||
if not all(acyclic(dep) for dep in by_id[node]["depends_on"] if dep in by_id):
|
||||
return False
|
||||
active.remove(node)
|
||||
visited.add(node)
|
||||
return True
|
||||
|
||||
check("dependency_graph_acyclic", all(acyclic(node) for node in ids))
|
||||
old_requirements = json.loads((ROOT / "docs/traceability/original/requirements.json").read_text())
|
||||
original_ids = {r["id"] for r in old_requirements}
|
||||
mapped_ids = [r["requirement_id"] for r in coverage]
|
||||
check("all75_original_groups_once", len(mapped_ids) == 75 and set(mapped_ids) == original_ids)
|
||||
check("feature71_schedule4", sum(r["scope"] == "feature_or_content" for r in coverage) == 71 and sum(r["scope"] == "schedule" for r in coverage) == 4)
|
||||
check("coverage_known_packages", all(set(r["work_packages"]) <= expected for r in coverage))
|
||||
check("coverage_known_delivery_ids", all(set(r["remaining_deliverable_ids"]) <= task_ids for r in coverage))
|
||||
check("coverage_exact_package_deliveries", all(set(r["remaining_deliverable_ids"]) == {d["id"] for p in packages if p["id"] in r["work_packages"] for d in p["remaining_deliverables"]} for r in coverage))
|
||||
check("package_and_requirement_mapping_agree", all({p["id"] for p in packages if r["requirement_id"] in p["requirement_ids"]} == set(r["work_packages"]) for r in coverage))
|
||||
check("source_lines_preserved", all(r["source_lines"] == next(x["source_lines"] for x in old_requirements if x["id"] == r["requirement_id"]) for r in coverage))
|
||||
check("old_status_not_promoted", all("current_status" not in r and r["current_atomic_acceptance"] == "not_reaudited" for r in coverage))
|
||||
supplements = {s for p in packages for s in p["supplement_ids"]}
|
||||
check("all10_facility_supplements", supplements == {f"F-{i:02d}" for i in range(1, 11)})
|
||||
check("P09_owns10_supplements", set(by_id["P09"]["supplement_ids"]) == supplements)
|
||||
pending = {d["id"] for d in decisions["pending"]}
|
||||
check("all10_pending_decision_groups", pending == {f"D{i:02d}" for i in range(1, 11)})
|
||||
check("known_decision_references", all(set(p["decisions"]) <= pending for p in packages))
|
||||
check("all5_confirmed_decisions", {d["id"] for d in decisions["confirmed"]} == {f"A{i:02d}" for i in range(1, 6)})
|
||||
check("confirmed_decision_references_exist", all((PLAN / d["reference"]).is_file() for d in decisions["confirmed"] if "reference" in d))
|
||||
state_counts = dict(collections.Counter(p["state"] for p in packages))
|
||||
check("valid_package_states", all(p["state"] in states for p in packages))
|
||||
check("new_completed_packages_require_evidence", all(p["state"] != "completed" or p["legacy_state"] == "completed" or p.get("completion_evidence") for p in packages))
|
||||
check("acceptance_conditions_not_claimed_results", all("cross_cutting_done" not in p and p["cross_cutting_acceptance_scope"] for p in packages))
|
||||
check("initial_revision_history_preserved", dict(collections.Counter(p["initial_revision_state"] for p in packages)) == baseline["revised_package_state_counts"])
|
||||
if args.initial_snapshot:
|
||||
check("initial_revised_state_counts", state_counts == baseline["revised_package_state_counts"] == {"completed": 1, "in_progress": 25, "planned": 4})
|
||||
check("initial_no_new_completed_packages", all(p["state"] != "completed" or p["legacy_state"] == "completed" for p in packages))
|
||||
check("initial_eight_partial_state_promotions", {p["id"] for p in packages if p["state"] != p["legacy_state"]} == {"P09", "P14", "P15", "P16", "P20", "P23", "P25", "P28"})
|
||||
check("initial_atomic_denominator_not_invented", baseline["atomic_denominator"] is None and baseline["overall_atomic_implementation_percent"] is None and baseline["overall_atomic_acceptance_percent"] is None)
|
||||
check("progress_contract8_views", len(contract["completion_views"]) == 8)
|
||||
check("progress_no_partial_credit", contract["partial_credit"] is False)
|
||||
known_denominator = None
|
||||
final_denominator = None
|
||||
if contract["denominator_frozen"]:
|
||||
check("frozen_denominator_has_register", bool(contract["atomic_register"]) and (PLAN / contract["atomic_register"]).is_file())
|
||||
atomic = read(contract["atomic_register"])
|
||||
freeze = read(contract["atomic_denominator_freeze"])
|
||||
index = read(contract["current_evidence_index"])
|
||||
known_denominator = len(atomic["denominator_ids"])
|
||||
final_denominator = contract["final_atomic_denominator"]
|
||||
check("known_denominator_matches_frozen_register", contract["known_condition_denominator"] == freeze["denominator"] == known_denominator and freeze["denominator_ids"] == atomic["denominator_ids"])
|
||||
check("progress_register_pin_matches", contract["atomic_register_sha256_at_integration"] == digest(PLAN / contract["atomic_register"]))
|
||||
check("evidence_index_register_pin_matches", index["atomic_register_sha256"] == digest(PLAN / contract["atomic_register"]) and index["known_condition_denominator"] == known_denominator)
|
||||
check("progress_index_pin_matches", contract["current_evidence_index_sha256_at_integration"] == digest(PLAN / contract["current_evidence_index"]))
|
||||
check("known_counts_match_freeze", contract["current_whole_atom_implementation_count"] == len(freeze["implemented_ids"]) and contract["current_whole_atom_acceptance_count"] == len(freeze["accepted_ids"]))
|
||||
check("unresolved_final_denominator_not_reported_as_total", contract["final_denominator_established"] or (
|
||||
final_denominator is None and all(contract[key] is None for key in [
|
||||
"overall_atomic_implementation_percent", "overall_atomic_acceptance_percent",
|
||||
"total_game_implementation_percent", "total_game_acceptance_percent"
|
||||
]) and atomic["final_denominator_established"] is False and freeze["final_denominator_established"] is False
|
||||
))
|
||||
check("static_leads_not_whole_atom_acceptance", index["whole_atom_acceptance_claimed"] is False and index["complete_runtime_ui_save_review"] is False)
|
||||
if contract["atomic_implementation_state"] == "not_reaudited":
|
||||
if contract.get("limited_view_review_manifest"):
|
||||
check("declared_limited_views_whole_atoms_unreviewed", not limited_view_registry_errors(
|
||||
atomic, contract["limited_view_review_manifest"]
|
||||
) and index.get("limited_view_review_manifest") == contract["limited_view_review_manifest"])
|
||||
else:
|
||||
check("unreviewed_atomic_views_remain_unverified", all(
|
||||
view["state"] == "unverified" and view["implementation_state"] == "not_reaudited"
|
||||
for row in atomic["requirements"] for view in row["view_results"].values()
|
||||
))
|
||||
snapshot = read(contract["progress_snapshot"])
|
||||
regression = snapshot["latest_regression_at_evidence_time"]
|
||||
check("regression_evidence_pin_matches", digest(ROOT / regression["path"]) == regression["sha256"] and digest(ROOT / regression["source_map_path"]) == regression["source_map_sha256"])
|
||||
check("regression_source_time_scope_preserved", regression["future_source_changes_auto_accepted"] is False and regression["original_full_acceptance"] is False and regression["windows_acceptance"] is False)
|
||||
check("P01_P03_not_whole_package_completed", by_id["P01"]["state"] != "completed" and by_id["P03"]["state"] != "completed")
|
||||
else:
|
||||
check("unfrozen_denominator_not_assigned", contract["atomic_register"] is None)
|
||||
check("progress_schedule_excluded", contract["excluded_from_function_denominator"] == ["B-14.1", "B-14.2", "B-14.3", "B-14.4"])
|
||||
check("baseline_materials_exist", all((ROOT / path).is_file() for path in baseline["source_pins"]))
|
||||
pins = baseline["facility_audit_current_pins_verified"]
|
||||
if args.verify_baseline:
|
||||
check("plan_source_pins_unchanged", all((ROOT / path).is_file() and digest(ROOT / path) == value for path, value in baseline["source_pins"].items()))
|
||||
check("current_facility40_pins_unchanged", len(pins) == 40 and all(digest(ROOT / path) == value for path, value in pins.items()))
|
||||
accepted = json.loads((ROOT / "artifacts/facility-need-pricing-production-scale-adoption-2026-10-01-r01/after.json").read_text())
|
||||
if args.verify_baseline:
|
||||
check("accepted1396_files_unchanged", len(accepted) == 1396 and all((ROOT / path).is_file() and digest(ROOT / path) == value for path, value in accepted.items()))
|
||||
receipt = json.loads((ROOT / "artifacts/facility-need-pricing-production-scale-adoption-2026-10-01-r01/receipt.json").read_text())
|
||||
check("retained_regression155_12310", baseline["full_regression"]["suites"] == receipt["accepted_proof"]["actual_full_suites"] == 155 and baseline["full_regression"]["checks"] == receipt["accepted_proof"]["actual_full_checks"] == 12310)
|
||||
check("new_game_tests_not_claimed", baseline["full_regression"]["new_engine_runs_in_plan_creation"] == 0)
|
||||
text = (PLAN / ("execution/IMPLEMENTATION_PLAN.initial.md" if args.initial_snapshot else "IMPLEMENTATION_PLAN.md")).read_text()
|
||||
check("all_package_headings", all(f"### {identifier} " in text for identifier in ids))
|
||||
check("all_group_rows", all(f"| {identifier} |" in text for identifier in original_ids))
|
||||
check("all_delivery_ids_in_document", all(identifier in text for identifier in task_ids))
|
||||
check("encyclopedia_and_hero_rival_scope", "図鑑の完成" in text and "ライバルは別集合" in text)
|
||||
check("current_native_and_longplay_not_claimed", baseline["current_head_windows_native_verified"] is False and baseline["long_play_acceptance_verified"] is False)
|
||||
failures = [c["name"] for c in checks if not c["passed"]]
|
||||
result = {
|
||||
"plan_date_jst": baseline["plan_date_jst"],
|
||||
"scope": "planning consistency and static baseline reconciliation; not game acceptance",
|
||||
"passed": not failures,
|
||||
"checks": checks,
|
||||
"failed_checks": failures,
|
||||
"packages": len(packages),
|
||||
"remaining_delivery_units": len(tasks),
|
||||
"current_package_state_counts": state_counts,
|
||||
"initial_snapshot_checked": args.initial_snapshot,
|
||||
"historical_source_bytes_compared": args.verify_baseline,
|
||||
"covered_original_groups": len(mapped_ids),
|
||||
"missing_original_group_ids": sorted(original_ids - set(mapped_ids)),
|
||||
"facility_supplements": len(supplements),
|
||||
"atomic_denominator": final_denominator,
|
||||
"known_condition_denominator": known_denominator,
|
||||
"final_denominator_established": contract.get("final_denominator_established", False),
|
||||
"total_game_implementation_percent": None,
|
||||
"total_game_acceptance_percent": None,
|
||||
"new_game_execution_count": 0,
|
||||
"document_sha256": {p.name: digest(p) for p in sorted(PLAN.iterdir()) if p.is_file() and p.name not in ["plan_validation.json", "plan_validation.initial.json"]},
|
||||
}
|
||||
output = "plan_validation.initial.json" if args.initial_snapshot else "plan_validation.json"
|
||||
(PLAN / output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|
||||
print(json.dumps({k: v for k, v in result.items() if k not in ["checks", "document_sha256"]}, ensure_ascii=False))
|
||||
if failures:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"scope": "in-memory simulated planning progress; no game execution or production mutation",
|
||||
"cases": [
|
||||
{
|
||||
"case": "progress_to_in_progress",
|
||||
"expected_plan_valid": true,
|
||||
"observed_plan_valid": true,
|
||||
"correct": true,
|
||||
"failures": []
|
||||
},
|
||||
{
|
||||
"case": "reject_done_without_task_evidence",
|
||||
"expected_plan_valid": false,
|
||||
"observed_plan_valid": false,
|
||||
"correct": true,
|
||||
"failures": [
|
||||
"completed_delivery_requires_evidence"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case": "reject_done_without_package_evidence",
|
||||
"expected_plan_valid": false,
|
||||
"observed_plan_valid": false,
|
||||
"correct": true,
|
||||
"failures": [
|
||||
"new_completed_packages_require_evidence"
|
||||
]
|
||||
}
|
||||
],
|
||||
"passed": true
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -39,6 +39,10 @@ window/stretch/mode="canvas_items"
|
||||
window/stretch/aspect="expand"
|
||||
window/stretch/scale_mode="fractional"
|
||||
|
||||
[editor]
|
||||
|
||||
import/use_multiple_threads=false
|
||||
|
||||
[rendering]
|
||||
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
|
||||
+254
-28
@@ -1,6 +1,19 @@
|
||||
extends Control
|
||||
|
||||
const UIAudioFeedbackScript := preload("res://scripts/ui/ui_audio_feedback.gd")
|
||||
const UIAudioSettingsDialogScript := preload("res://scripts/ui/ui_audio_settings_dialog.gd")
|
||||
const UIAudioOwnerShutdownScript := preload("res://scripts/ui/ui_audio_owner_shutdown.gd")
|
||||
const PairedBGMScript := preload("res://scripts/ui/paired_bgm.gd")
|
||||
var _paired_bgm: Variant
|
||||
signal ui_audio_window_close_drained(report: Dictionary)
|
||||
var audio_settings_path := "user://settings/audio.cfg"
|
||||
var _audio_feedback: Variant
|
||||
var _audio_settings_dialog: Variant
|
||||
var _window_close_in_progress := false
|
||||
|
||||
const RankDialogScript := preload("res://scripts/ui/world_tree_rank_dialog.gd")
|
||||
const EventLogDialogScript := preload("res://scripts/ui/event_log_dialog.gd")
|
||||
const ManualSaveSlotsDialogScript := preload("res://scripts/ui/manual_save_slots_dialog.gd")
|
||||
|
||||
const OpeningGuideScript := preload("res://scripts/ui/inn_opening_guide.gd")
|
||||
const OperatingLedgerPanelScript := preload("res://scripts/ui/operating_ledger_panel.gd")
|
||||
@@ -113,11 +126,13 @@ const STAFF_SKILL_NAMES := {
|
||||
|
||||
@export var visual_theme: VisualThemeProfile = DefaultVisualTheme
|
||||
var manual_save_path := MANUAL_SAVE_PATH
|
||||
var manual_save_slots_dialog: ConfirmationDialog
|
||||
|
||||
var time_label: Label
|
||||
var phase_label: Label
|
||||
var rank_button: Button
|
||||
var rank_dialog: ConfirmationDialog
|
||||
var event_log_dialog: AcceptDialog
|
||||
var weather_label: Label
|
||||
var guest_count_label: Button
|
||||
var dimension_migration_dialog: ConfirmationDialog
|
||||
@@ -205,6 +220,9 @@ func _ready() -> void:
|
||||
if not bool(current_operations.get(&"enabled", false)):
|
||||
_operations_start_report = _runtime.call(&"enable_production_operations")
|
||||
_synchronize_ui_command_state()
|
||||
_setup_ui_audio()
|
||||
UIAudioOwnerShutdownScript.register_owner(get_tree(), self)
|
||||
get_tree().root.close_requested.connect(_on_window_close_requested)
|
||||
_build_interface()
|
||||
_connect_signals()
|
||||
_populate_build_palette()
|
||||
@@ -225,6 +243,43 @@ func _ready() -> void:
|
||||
_focus_initial_view.call_deferred()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
var tree := get_tree()
|
||||
if tree != null:
|
||||
if tree.root.close_requested.is_connected(_on_window_close_requested):
|
||||
tree.root.close_requested.disconnect(_on_window_close_requested)
|
||||
UIAudioOwnerShutdownScript.unregister_owner(tree, self)
|
||||
|
||||
|
||||
func _on_window_close_requested() -> void:
|
||||
# The shared coordinator drains every registered Main before one quit.
|
||||
# Main stays alive while its audio child is freed and the backend releases.
|
||||
var tree := get_tree()
|
||||
UIAudioOwnerShutdownScript.close_owners(tree)
|
||||
|
||||
|
||||
func _report_audio_close_drained(report: Dictionary) -> void:
|
||||
ui_audio_window_close_drained.emit(report)
|
||||
|
||||
|
||||
func _prepare_owned_audio_close() -> Array[Dictionary]:
|
||||
_window_close_in_progress = true
|
||||
set_process(false)
|
||||
set_process_input(false)
|
||||
set_process_unhandled_input(false)
|
||||
set_process_unhandled_key_input(false)
|
||||
hide()
|
||||
var audio: Node = _audio_feedback as Node
|
||||
_audio_feedback = null
|
||||
var targets := UIAudioOwnerShutdownScript.capture_and_release(audio)
|
||||
if is_instance_valid(_paired_bgm):
|
||||
targets.append_array(_paired_bgm.capture_owned_targets())
|
||||
_paired_bgm.prepare_owner_shutdown()
|
||||
_paired_bgm.queue_free()
|
||||
_paired_bgm = null
|
||||
return targets
|
||||
|
||||
|
||||
func _focus_initial_view() -> void:
|
||||
await get_tree().process_frame
|
||||
world_preview.focus_operating_area()
|
||||
@@ -281,6 +336,8 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
||||
_:
|
||||
handled = false
|
||||
if handled:
|
||||
if _audio_feedback != null:
|
||||
_audio_feedback.request_click()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
@@ -377,6 +434,9 @@ func _create_top_bar() -> Control:
|
||||
title_box.add_child(rank_button)
|
||||
rank_dialog=RankDialogScript.new()
|
||||
rank_dialog.configure(_runtime)
|
||||
rank_dialog.configure_audio(Callable(_audio_feedback, "submit_command"))
|
||||
_audio_feedback.bind_button(rank_dialog.get_ok_button())
|
||||
_audio_feedback.bind_button(rank_dialog.get_cancel_button())
|
||||
rank_dialog.status_requested.connect(func(message: String, success: bool) -> void:
|
||||
_set_status(message,success)
|
||||
_refresh_dashboard()
|
||||
@@ -561,6 +621,7 @@ func _create_side_panel() -> Control:
|
||||
column.add_child(candidate_heading_row)
|
||||
staff_management_panel = StaffManagementPanelScript.new()
|
||||
staff_management_panel.configure(_runtime, _apply_button_style)
|
||||
staff_management_panel.configure_audio(Callable(_audio_feedback, "submit_command"))
|
||||
staff_management_panel.status_requested.connect(_on_staff_management_status)
|
||||
staff_management_panel.visible = false
|
||||
column.add_child(staff_management_panel)
|
||||
@@ -624,15 +685,19 @@ func _create_side_panel() -> Control:
|
||||
operating_ledger_panel = OperatingLedgerPanelScript.new()
|
||||
root_column.add_child(operating_ledger_panel)
|
||||
operating_ledger_panel.setup(_runtime, GameClock, _apply_button_style, GameState.active_layer)
|
||||
operating_ledger_panel.configure_audio(Callable(_audio_feedback, "submit_command"))
|
||||
operating_ledger_panel.status_requested.connect(_on_staff_management_status)
|
||||
operating_ledger_panel.visible = false
|
||||
supply_management_panel = SupplyManagementPanelScript.new()
|
||||
supply_management_panel.configure(_runtime, _apply_button_style)
|
||||
supply_management_panel.configure_audio(Callable(_audio_feedback, "submit_command"))
|
||||
supply_management_panel.status_requested.connect(_on_staff_management_status)
|
||||
supply_management_panel.action_requested.connect(_activate_management_mode)
|
||||
supply_management_panel.visible = false
|
||||
root_column.add_child(supply_management_panel)
|
||||
business_management_panel = BusinessManagementPanelScript.new()
|
||||
business_management_panel.configure(_runtime, _apply_button_style)
|
||||
business_management_panel.configure_audio(Callable(_audio_feedback, "submit_command"))
|
||||
business_management_panel.status_requested.connect(_on_staff_management_status)
|
||||
business_management_panel.action_requested.connect(_activate_management_mode)
|
||||
business_management_panel.visible = false
|
||||
@@ -676,7 +741,7 @@ func _create_bottom_bar() -> Control:
|
||||
pan_button = _button("画面移動", _toggle_pan_mode, 90)
|
||||
pan_button.name = "PanModeButton"
|
||||
pan_button.toggle_mode = true
|
||||
pan_button.tooltip_text = "左ドラッグで画面を移動。もう一度押すか Esc で解除。ホイールで拡大縮小、Homeで全体表示。"
|
||||
pan_button.tooltip_text = "左ドラッグとホイールで画面を上下移動。Ctrl+ホイールで拡大縮小。もう一度押すか Esc で解除、Homeで全体表示。"
|
||||
row.add_child(pan_button)
|
||||
var focus_button := _button("営業階", func() -> void: world_preview.focus_operating_area(), 64)
|
||||
focus_button.name = "FocusOperatingAreaButton"
|
||||
@@ -689,9 +754,20 @@ func _create_bottom_bar() -> Control:
|
||||
management_toggle_button.name = "ManagementToggleButton"
|
||||
management_toggle_button.toggle_mode = true
|
||||
row.add_child(management_toggle_button)
|
||||
event_log_dialog = EventLogDialogScript.new()
|
||||
event_log_dialog.configure(_runtime)
|
||||
add_child(event_log_dialog)
|
||||
var event_log_button := _button("記録", event_log_dialog.show_events, 62)
|
||||
event_log_button.name = "EventLogButton"
|
||||
event_log_button.tooltip_text = "最近のイベントを確認"
|
||||
row.add_child(event_log_button)
|
||||
var save_button := _button("保存", _save_manual, 62)
|
||||
save_button.name = "ManualSaveButton"
|
||||
_apply_button_icon(save_button, visual_theme.control_icon(&"save") if visual_theme != null else null, 16)
|
||||
row.add_child(save_button)
|
||||
var audio_button := _button("音設定", _show_audio_settings, 62)
|
||||
audio_button.name = "AudioSettingsButton"
|
||||
row.add_child(audio_button)
|
||||
var load_menu := MenuButton.new()
|
||||
load_menu.name = "SaveLoadMenu"
|
||||
load_menu.text = "読込 ▾"
|
||||
@@ -700,9 +776,11 @@ func _create_bottom_bar() -> Control:
|
||||
_apply_button_style(load_menu)
|
||||
load_menu.get_popup().add_item("手動セーブを読み込む",0)
|
||||
load_menu.get_popup().add_item("旧寸法を修正して読み込む",1)
|
||||
load_menu.get_popup().add_item("保存スロットを選んで読込",2)
|
||||
load_menu.get_popup().id_pressed.connect(func(id: int):
|
||||
if id == 0: _load_manual()
|
||||
elif id == 1: _preview_dimension_migration())
|
||||
elif id == 1: _preview_dimension_migration()
|
||||
elif id == 2: _show_manual_load_slots())
|
||||
row.add_child(load_menu)
|
||||
|
||||
palette_scroll = ScrollContainer.new()
|
||||
@@ -865,11 +943,14 @@ func _connect_signals() -> void:
|
||||
GameClock.phase_changed.connect(_on_phase_changed)
|
||||
GameState.resource_changed.connect(_on_resource_changed)
|
||||
world_preview.facility_selected.connect(_on_facility_selected)
|
||||
world_preview.facility_selected.connect(_on_audio_world_selection)
|
||||
world_preview.staff_assignment_dropped.connect(func(data: Dictionary, facility_id: StringName) -> void:
|
||||
staff_management_panel.assign_from_drop(data, facility_id)
|
||||
)
|
||||
world_preview.legacy_stair_selected.connect(_on_legacy_stair_selected)
|
||||
world_preview.legacy_stair_selected.connect(_on_audio_world_selection)
|
||||
world_preview.construction_selected.connect(_on_construction_selected)
|
||||
world_preview.construction_selected.connect(_on_audio_world_selection)
|
||||
world_preview.cell_clicked.connect(_on_map_cell_clicked)
|
||||
world_preview.build_preview_changed.connect(_on_build_preview_changed)
|
||||
world_preview.excavation_preview_changed.connect(_on_excavation_preview_changed)
|
||||
@@ -1085,9 +1166,7 @@ func _hire_staff_candidate(candidate_id: StringName) -> void:
|
||||
var idempotency_key := StringName(
|
||||
"ui.hire.%s.%08d" % [String(candidate_id), sequence]
|
||||
)
|
||||
var result: Variant = _runtime.call(
|
||||
&"submit_command",
|
||||
&"hire_staff_candidate",
|
||||
var result: Variant = _submit_ui_command(&"hire_staff_candidate",
|
||||
{&"candidate_id": candidate_id},
|
||||
&"player",
|
||||
candidate_id,
|
||||
@@ -1215,7 +1294,7 @@ func _toggle_excavation_mode() -> void:
|
||||
|
||||
func _toggle_pan_mode() -> void:
|
||||
_set_pan_mode(not world_preview.pan_mode)
|
||||
_set_status("画面移動中:左ドラッグで移動、ホイールで拡大縮小。Escで解除。" if world_preview.pan_mode else "画面移動を解除しました。施設をクリックすると情報を表示します。", true, true)
|
||||
_set_status("画面移動中:左ドラッグで移動、ホイールで上下移動、Ctrl+ホイールで拡大縮小。Escで解除。" if world_preview.pan_mode else "画面移動を解除しました。施設をクリックすると情報を表示します。", true, true)
|
||||
|
||||
|
||||
func _set_pan_mode(enabled: bool) -> void:
|
||||
@@ -1273,6 +1352,8 @@ func _rotate_build_preview() -> void:
|
||||
|
||||
|
||||
func _on_map_cell_clicked(cell: Vector3i) -> void:
|
||||
if _audio_feedback != null:
|
||||
_audio_feedback.request_click()
|
||||
if _excavation_mode:
|
||||
_start_excavation(cell)
|
||||
return
|
||||
@@ -1287,6 +1368,8 @@ func _on_map_cell_clicked(cell: Vector3i) -> void:
|
||||
return
|
||||
var preview := world_preview.get_build_preview_report(cell)
|
||||
if not bool(preview.get(&"can_build", false)):
|
||||
if _audio_feedback != null:
|
||||
_audio_feedback.reject_local(&"BUILD_PREVIEW_REJECTED")
|
||||
_set_status(_preview_failure_message(preview), false)
|
||||
return
|
||||
if _runtime == null or not _runtime.has_method(&"submit_command"):
|
||||
@@ -1294,9 +1377,7 @@ func _on_map_cell_clicked(cell: Vector3i) -> void:
|
||||
return
|
||||
var idempotency_key := StringName("ui.build.%08d" % _build_sequence)
|
||||
_build_sequence += 1
|
||||
var result: Variant = _runtime.call(
|
||||
&"submit_command",
|
||||
&"build_facility",
|
||||
var result: Variant = _submit_ui_command(&"build_facility",
|
||||
{&"definition_id": _selected_definition_id, &"origin": cell, &"rotation": _build_rotation},
|
||||
&"player",
|
||||
&"",
|
||||
@@ -1324,7 +1405,9 @@ func _on_build_preview_changed(report: Dictionary) -> void:
|
||||
return
|
||||
if bool(report.get(&"can_build", false)):
|
||||
var cell: Vector3i = world_preview.get_hovered_cell()
|
||||
_set_status("配置可能 (%d, %d)・%d° — クリックで建設" % [cell.x, cell.y, _build_rotation], true, true)
|
||||
var connection: Dictionary = report.get(&"connection", {})
|
||||
var warning := " · 入口への経路未接続" if connection.get(&"status", &"") == &"DISCONNECTED" else " · 経路確認不可" if connection.get(&"status", &"") == &"UNAVAILABLE" else ""
|
||||
_set_status("配置可能 (%d, %d)・%d°%s — クリックで建設" % [cell.x, cell.y, _build_rotation, warning], true, true)
|
||||
else:
|
||||
_set_status(_preview_failure_message(report), false, true)
|
||||
|
||||
@@ -1369,9 +1452,7 @@ func _start_excavation(cell: Vector3i) -> void:
|
||||
_set_status("このマスの掘削指示はすでに受理されています。", false)
|
||||
return
|
||||
var idempotency_key := _excavation_idempotency_key(cell)
|
||||
var result: Variant = _runtime.call(
|
||||
&"submit_command",
|
||||
&"start_excavation",
|
||||
var result: Variant = _submit_ui_command(&"start_excavation",
|
||||
{&"cell": Vector3i(cell.x, cell.y, world_preview.layer_mode)},
|
||||
&"player",
|
||||
&"",
|
||||
@@ -1436,6 +1517,8 @@ func _on_construction_selected(job_id: StringName) -> void:
|
||||
|
||||
|
||||
func _refresh_dashboard() -> void:
|
||||
if event_log_dialog != null and event_log_dialog.visible:
|
||||
event_log_dialog.refresh()
|
||||
if rank_button != null and _service(&"progression_service") != null:
|
||||
rank_button.text="世界樹ランク %d" % _service(&"progression_service").current_rank
|
||||
if rank_dialog != null and rank_dialog.visible: rank_dialog.refresh()
|
||||
@@ -1544,6 +1627,8 @@ func _refresh_foreground_event(event_service: Variant) -> void:
|
||||
foreground_event_panel.visible = false
|
||||
return
|
||||
foreground_event_panel.visible = true
|
||||
if _audio_feedback != null:
|
||||
_audio_feedback.observe_foreground(foreground)
|
||||
_set_ledger_panel_open(false)
|
||||
_set_supply_panel_open(false)
|
||||
_set_management_panel_open(true)
|
||||
@@ -1611,9 +1696,7 @@ func _submit_foreground_event_choice(
|
||||
var idempotency_key := StringName(
|
||||
"ui.event.choice.%s.%s" % [String(choice_instance_id), String(choice_id)]
|
||||
)
|
||||
var result: Variant = _runtime.call(
|
||||
&"submit_command",
|
||||
&"submit_event_choice",
|
||||
var result: Variant = _submit_ui_command(&"submit_event_choice",
|
||||
{
|
||||
&"event_instance_id": event_instance_id,
|
||||
&"choice_instance_id": choice_instance_id,
|
||||
@@ -1774,7 +1857,7 @@ func _on_related_facility_selected(index: int) -> void:
|
||||
if _runtime == null or _selected_entity_id.is_empty():
|
||||
return
|
||||
var related_id := StringName(related_facility_picker.get_item_metadata(index))
|
||||
var result: Variant = _runtime.call(&"submit_command", &"set_facility_conditions", {&"facility_id": _selected_entity_id, &"conditions": {&"related_facility_id": related_id}})
|
||||
var result: Variant = _submit_ui_command(&"set_facility_conditions", {&"facility_id": _selected_entity_id, &"conditions": {&"related_facility_id": related_id}})
|
||||
if result.is_success() and not result.details.has(&"instance_id"):
|
||||
_pending_related_commands[int(result.command_id)] = true
|
||||
_set_status("関連施設の変更を受け付けました。" if result.is_success() else result.message, result.is_success(), true)
|
||||
@@ -1908,7 +1991,7 @@ func _on_elevator_operation_pressed() -> void:
|
||||
var key := StringName("ui.elevator.%08d" % _elevator_sequence)
|
||||
_elevator_sequence += 1
|
||||
var open_value := not bool(shaft.get(&"operator_open", true))
|
||||
var result: Variant = _runtime.call(&"submit_command", &"set_elevator_operation", {&"shaft_id": shaft_id, &"operator_open": open_value}, &"player", &"", 50, -1, key)
|
||||
var result: Variant = _submit_ui_command(&"set_elevator_operation", {&"shaft_id": shaft_id, &"operator_open": open_value}, &"player", &"", 50, -1, key)
|
||||
if result == null or not result.is_success():
|
||||
_set_status("昇降機の指示を反映できませんでした。", false)
|
||||
return
|
||||
@@ -2118,21 +2201,55 @@ func _command_failure_message(result: Variant) -> String:
|
||||
|
||||
|
||||
func _save_manual() -> void:
|
||||
_save_manual_at_path(manual_save_path, "手動保存")
|
||||
|
||||
|
||||
func _save_manual_at_path(save_path: String, display_name: String) -> bool:
|
||||
if _runtime == null or not _runtime.has_method(&"save_to_path"):
|
||||
_set_status("保存サービスへ接続できません。", false)
|
||||
return
|
||||
var directory_error := DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(manual_save_path).get_base_dir())
|
||||
return false
|
||||
var directory_error := DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(save_path).get_base_dir())
|
||||
if directory_error != OK and directory_error != ERR_ALREADY_EXISTS:
|
||||
_set_status("保存フォルダを作成できませんでした。", false)
|
||||
return
|
||||
var result: Variant = _runtime.call(&"save_to_path", manual_save_path, {&"display_name": "手動保存", &"interface_selection": _capture_interface_selection()})
|
||||
return false
|
||||
var metadata := {&"display_name": display_name, &"interface_selection": _capture_interface_selection()}
|
||||
var thumbnail := _capture_manual_thumbnail_png()
|
||||
if not thumbnail.is_empty(): metadata[&"thumbnail_png"] = thumbnail
|
||||
var result: Variant = _runtime.call(&"save_to_path", save_path, metadata)
|
||||
if result is Dictionary and bool(result.get(&"ok", false)):
|
||||
manual_save_path = save_path
|
||||
_set_status("手動セーブを保存しました。", true)
|
||||
return true
|
||||
if result is Dictionary and StringName(result.get(&"code", &"")) == &"FORMAT_MIGRATION_REQUIRED":
|
||||
_set_status("元の保存を残すため、別のスロットへ保存してください。", false)
|
||||
_ensure_manual_save_slots().show_new_save(save_path)
|
||||
else:
|
||||
_set_status("保存に失敗しました: %s" % _result_message(result), false)
|
||||
return false
|
||||
|
||||
|
||||
func _capture_manual_thumbnail_png() -> PackedByteArray:
|
||||
# Capture the existing rendered game; no simulation update or new art source.
|
||||
# Dummy/headless rendering retains the existing SaveService fallback.
|
||||
if DisplayServer.get_name() == "headless": return PackedByteArray()
|
||||
var viewport := get_viewport()
|
||||
if viewport == null: return PackedByteArray()
|
||||
var texture := viewport.get_texture()
|
||||
if texture == null: return PackedByteArray()
|
||||
var image := texture.get_image()
|
||||
if image == null or image.is_empty(): return PackedByteArray()
|
||||
var scale := minf(320.0 / float(image.get_width()), 180.0 / float(image.get_height()))
|
||||
var width := maxi(1, roundi(float(image.get_width()) * scale))
|
||||
var height := maxi(1, roundi(float(image.get_height()) * scale))
|
||||
image.resize(width, height, Image.INTERPOLATE_LANCZOS)
|
||||
return image.save_png_to_buffer()
|
||||
|
||||
|
||||
func _load_manual(strict: bool = false) -> void:
|
||||
_load_manual_at_path(manual_save_path, strict)
|
||||
|
||||
|
||||
func _load_manual_at_path(save_path: String, strict: bool = false) -> void:
|
||||
if _runtime == null or (
|
||||
not _runtime.has_method(&"load_with_recovery")
|
||||
and not _runtime.has_method(&"load_from_path")
|
||||
@@ -2140,11 +2257,12 @@ func _load_manual(strict: bool = false) -> void:
|
||||
_set_status("読込サービスへ接続できません。", false)
|
||||
return
|
||||
var result: Variant = (
|
||||
_runtime.call(&"load_with_recovery", manual_save_path)
|
||||
_runtime.call(&"load_with_recovery", save_path)
|
||||
if not strict and _runtime.has_method(&"load_with_recovery")
|
||||
else _runtime.call(&"load_from_path", manual_save_path)
|
||||
else _runtime.call(&"load_from_path", save_path)
|
||||
)
|
||||
if result is Dictionary and bool(result.get(&"ok", false)):
|
||||
manual_save_path = save_path
|
||||
# Real Runtime already emitted its common completion notification.
|
||||
if not _runtime.has_signal(&"save_operation_completed"):
|
||||
_apply_loaded_interface(result)
|
||||
@@ -2153,8 +2271,37 @@ func _load_manual(strict: bool = false) -> void:
|
||||
_set_status("読み込みに失敗しました: %s" % _result_message(result), false)
|
||||
|
||||
|
||||
func _ensure_manual_save_slots() -> Variant:
|
||||
if manual_save_slots_dialog == null:
|
||||
manual_save_slots_dialog = ManualSaveSlotsDialogScript.new()
|
||||
manual_save_slots_dialog.connect(&"new_slot_requested", _save_new_manual_slot)
|
||||
manual_save_slots_dialog.connect(&"load_slot_requested", _load_selected_manual_slot)
|
||||
add_child(manual_save_slots_dialog)
|
||||
return manual_save_slots_dialog
|
||||
|
||||
|
||||
func _save_new_manual_slot(path: String, display_name: String) -> void:
|
||||
if not ManualSaveSlotsDialogScript.is_fresh_slot(path) or path.get_base_dir() != manual_save_path.get_base_dir():
|
||||
_set_status("新しい保存先を用意できませんでした。元の保存は変更していません。", false)
|
||||
return
|
||||
_save_manual_at_path(path, display_name)
|
||||
|
||||
|
||||
func _show_manual_load_slots() -> void:
|
||||
_ensure_manual_save_slots().show_load_slots(manual_save_path)
|
||||
|
||||
|
||||
func _load_selected_manual_slot(path: String) -> void:
|
||||
if path.get_base_dir() != manual_save_path.get_base_dir() or not ManualSaveSlotsDialogScript.is_owned_slot_name(path.get_file()):
|
||||
_set_status("この保存先を読み込めません。", false)
|
||||
return
|
||||
_load_manual_at_path(path)
|
||||
|
||||
func _capture_interface_selection() -> Dictionary:
|
||||
var selection := {&"version": 1, &"kind": String(_selected_entity_kind), &"id": String(_selected_entity_id), &"staff_open": _staff_management_panel_open, &"guest_open": false}
|
||||
selection[&"product_open"] = _supply_panel_open
|
||||
if supply_management_panel != null:
|
||||
selection[&"product_tab"] = String(supply_management_panel.selected_tab)
|
||||
if staff_management_panel != null:
|
||||
selection[&"staff_id"] = String(staff_management_panel.get("selected_staff_id"))
|
||||
if guest_observer != null:
|
||||
@@ -2181,6 +2328,8 @@ func _on_save_operation_completed(result: Dictionary) -> void:
|
||||
|
||||
|
||||
func _apply_loaded_interface(result: Dictionary) -> void:
|
||||
if _audio_feedback != null:
|
||||
_audio_feedback.begin_loaded_epoch(_current_audio_foreground())
|
||||
_pending_build_commands.clear()
|
||||
_pending_excavation_commands.clear()
|
||||
_pending_hire_commands.clear()
|
||||
@@ -2200,6 +2349,7 @@ func _apply_loaded_interface(result: Dictionary) -> void:
|
||||
_restore_interface_selection(result.get(&"manifest", {}))
|
||||
_restore_actor_interface_selection(result.get(&"manifest", {}))
|
||||
_restore_defense_interface_selection(result.get(&"manifest", {}))
|
||||
_restore_product_interface_selection(result.get(&"manifest", {}))
|
||||
_refresh_dashboard()
|
||||
_update_pause_and_speed()
|
||||
|
||||
@@ -2244,13 +2394,12 @@ func _restore_actor_interface_selection(manifest: Variant) -> void:
|
||||
# then restore each optional inspector independently of the map selection.
|
||||
_set_staff_management_panel_open(false)
|
||||
if staff_management_panel != null:
|
||||
staff_management_panel.call("select_staff", &"")
|
||||
staff_management_panel.call("clear_selection")
|
||||
if guest_observer != null:
|
||||
guest_observer.hide()
|
||||
var old_panel := guest_observer.find_child("GuestObservationPanel", true, false) as GuestObservationPanel
|
||||
if old_panel != null:
|
||||
old_panel.selected_guest_id = &""
|
||||
old_panel.refresh()
|
||||
old_panel.clear_selection()
|
||||
if not manifest is Dictionary:
|
||||
return
|
||||
var selection: Variant = manifest.get("interface_selection", {})
|
||||
@@ -2270,6 +2419,34 @@ func _restore_actor_interface_selection(manifest: Variant) -> void:
|
||||
_show_guest_observer()
|
||||
|
||||
|
||||
func _restore_product_interface_selection(manifest: Variant) -> void:
|
||||
# Additive optional presentation: old packages keep their existing behavior.
|
||||
# No purchase draft, quantity, policy, assignment or command is restored here.
|
||||
if supply_management_panel == null or not manifest is Dictionary:
|
||||
return
|
||||
var selection: Variant = manifest.get("interface_selection", {})
|
||||
if not selection is Dictionary:
|
||||
return
|
||||
var version: Variant = selection.get("version", 0)
|
||||
if not (version is int or version is float) or version != 1:
|
||||
return
|
||||
var open_value: Variant = selection.get("product_open", null)
|
||||
var tab_value: Variant = selection.get("product_tab", null)
|
||||
if not open_value is bool or not tab_value is String:
|
||||
return
|
||||
if tab_value not in ["purchase", "kitchen", "delivery"]:
|
||||
return
|
||||
# A captured main panel cannot be both staff and product. Ignore a
|
||||
# contradictory optional product claim without overriding restored staff.
|
||||
if open_value and selection.get("staff_open", false) is bool and selection.get("staff_open", false):
|
||||
return
|
||||
supply_management_panel.select_tab(StringName(tab_value))
|
||||
if open_value:
|
||||
_activate_management_mode(&"product")
|
||||
else:
|
||||
_set_supply_panel_open(false)
|
||||
|
||||
|
||||
func _result_message(result: Variant) -> String:
|
||||
if result is Dictionary:
|
||||
var message := String(result.get(&"message", result.get(&"code", "保存データがありません")))
|
||||
@@ -2434,6 +2611,8 @@ func _on_command_completed(result: Variant) -> void:
|
||||
|
||||
func _set_layer(value: int) -> void:
|
||||
var normalized := clampi(value, 0, 1)
|
||||
if is_instance_valid(_paired_bgm):
|
||||
_paired_bgm.set_layer(normalized)
|
||||
if world_preview != null:
|
||||
world_preview.layer_mode = normalized
|
||||
GameState.active_layer = normalized
|
||||
@@ -2448,6 +2627,8 @@ func _set_layer(value: int) -> void:
|
||||
if _interface_layer >= 0 and _interface_layer != normalized:
|
||||
LayerSkin.apply_tree(self, normalized)
|
||||
if rank_dialog != null: rank_dialog.apply_layer_skin(normalized)
|
||||
if event_log_dialog != null: event_log_dialog.apply_layer_skin(normalized)
|
||||
if manual_save_slots_dialog != null: manual_save_slots_dialog.apply_layer_skin(normalized)
|
||||
_interface_layer = normalized
|
||||
if foreground_event_label != null:
|
||||
foreground_event_label.add_theme_color_override("font_color", Color("ffb498") if normalized == 1 else Color("8f3f29"))
|
||||
@@ -2627,6 +2808,8 @@ func _apply_button_icon(button: Button, icon_texture: Texture2D, maximum_width:
|
||||
|
||||
|
||||
func _apply_button_style(button: Button) -> void:
|
||||
if _audio_feedback != null:
|
||||
_audio_feedback.bind_button(button)
|
||||
button.add_theme_color_override("font_color", _visual_color(&"text_light", Color("fff0c8")))
|
||||
button.add_theme_color_override("font_hover_color", Color("fff8e5"))
|
||||
button.add_theme_color_override("font_pressed_color", Color("fff8e5"))
|
||||
@@ -2818,6 +3001,7 @@ func _ensure_defense_manager() -> DefenseManagementPanel:
|
||||
_apply_button_style(defense_manager.get_ok_button())
|
||||
var panel := DefenseManagementPanelScript.new()
|
||||
panel.configure(_runtime, _apply_button_style)
|
||||
panel.configure_audio(Callable(_audio_feedback, "submit_command"))
|
||||
panel.status_requested.connect(_on_staff_management_status)
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.name = "DefenseManagementScroll"
|
||||
@@ -2841,7 +3025,7 @@ func _restore_defense_interface_selection(manifest: Variant) -> void:
|
||||
if defense_manager != null:
|
||||
defense_manager.hide()
|
||||
var existing := defense_manager.find_child("DefenseManagementPanel", true, false) as DefenseManagementPanel
|
||||
if existing != null: existing.select_raid(&"")
|
||||
if existing != null: existing.clear_selection()
|
||||
if not manifest is Dictionary: return
|
||||
var selection: Variant = manifest.get("interface_selection", {})
|
||||
if not selection is Dictionary or selection.get("version", 0) != 1: return
|
||||
@@ -2852,3 +3036,45 @@ func _restore_defense_interface_selection(manifest: Variant) -> void:
|
||||
_ensure_defense_manager().select_raid(raid_id)
|
||||
if selection.get("defense_open", false) is bool and selection.get("defense_open", false):
|
||||
_show_defense_manager()
|
||||
|
||||
|
||||
func _setup_ui_audio() -> void:
|
||||
_paired_bgm = PairedBGMScript.new()
|
||||
_paired_bgm.name = "PairedBGM"
|
||||
_paired_bgm.configure(GameState.active_layer)
|
||||
add_child(_paired_bgm)
|
||||
_audio_feedback = UIAudioFeedbackScript.new()
|
||||
_audio_feedback.name = "UIAudioFeedback"
|
||||
_audio_feedback.configure(_runtime, audio_settings_path)
|
||||
add_child(_audio_feedback)
|
||||
_audio_feedback.begin_loaded_epoch(_current_audio_foreground())
|
||||
_audio_settings_dialog = UIAudioSettingsDialogScript.new()
|
||||
_audio_settings_dialog.configure(_audio_feedback, _apply_button_style)
|
||||
add_child(_audio_settings_dialog)
|
||||
|
||||
|
||||
func _current_audio_foreground() -> Variant:
|
||||
var events: Variant = _service(&"event_service")
|
||||
if events != null and events.has_method(&"get_foreground_instance"):
|
||||
return events.call(&"get_foreground_instance")
|
||||
return null
|
||||
|
||||
|
||||
func _show_audio_settings() -> void:
|
||||
if _audio_settings_dialog != null:
|
||||
_audio_settings_dialog.show_settings()
|
||||
|
||||
|
||||
func _on_audio_world_selection(_id: StringName) -> void:
|
||||
if _audio_feedback != null:
|
||||
_audio_feedback.request_click()
|
||||
|
||||
|
||||
func _submit_ui_command(
|
||||
command_type: StringName, payload: Dictionary = {}, actor_id: StringName = &"player",
|
||||
target_id: StringName = &"", priority := 0, execute_at := -1,
|
||||
idempotency_key: StringName = &""
|
||||
) -> Variant:
|
||||
if _audio_feedback != null:
|
||||
return _audio_feedback.submit_command(command_type, payload, actor_id, target_id, priority, execute_at, idempotency_key)
|
||||
return _runtime.call(&"submit_command", command_type, payload, actor_id, target_id, priority, execute_at, idempotency_key)
|
||||
|
||||
@@ -82,6 +82,9 @@ var _drag_button := MOUSE_BUTTON_NONE
|
||||
|
||||
var _runtime: Variant = null
|
||||
var _hovered_cell: Variant = null
|
||||
var _preview_topology_key: Array = []
|
||||
var _connection_cache_key: Array = []
|
||||
var _connection_cache_report: Dictionary = {}
|
||||
var _last_preview_report: Dictionary = INVALID_PREVIEW.duplicate(true)
|
||||
var _last_excavation_preview_report: Dictionary = INVALID_EXCAVATION_PREVIEW.duplicate(true)
|
||||
|
||||
@@ -112,6 +115,8 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func set_runtime(value: Variant) -> void:
|
||||
_connection_cache_key.clear()
|
||||
_connection_cache_report.clear()
|
||||
_disconnect_runtime()
|
||||
_runtime = value
|
||||
_actor_tracks.clear()
|
||||
@@ -201,7 +206,11 @@ func get_build_preview_report(cell: Variant = null) -> Dictionary:
|
||||
build_rotation
|
||||
)
|
||||
if report != null and report.has_method(&"to_dictionary"):
|
||||
return report.call(&"to_dictionary") as Dictionary
|
||||
var result: Dictionary = report.call(&"to_dictionary")
|
||||
result[&"connection"] = _build_connection_report(
|
||||
_cells_from_serialized(result.get(&"cells", [])),
|
||||
Vector3i(target.x, target.y, layer_mode))
|
||||
return result
|
||||
return {
|
||||
&"can_build": _fallback_cells_are_available(Vector3i(target.x, target.y, layer_mode)),
|
||||
&"error_codes": [],
|
||||
@@ -338,6 +347,9 @@ func _process(delta: float) -> void:
|
||||
lens_material.set_shader_parameter("extent", size)
|
||||
lens_material.set_shader_parameter("progress", _layer_transition_elapsed / LAYER_TRANSITION_SECONDS)
|
||||
_layer_lens.visible = _layer_transition_elapsed < LAYER_TRANSITION_SECONDS
|
||||
if build_definition != null and _hovered_cell is Vector3i and _current_preview_topology() != _preview_topology_key:
|
||||
_update_build_preview()
|
||||
queue_redraw()
|
||||
var clock := get_node_or_null("/root/GameClock")
|
||||
if clock != null and bool(clock.get("paused")):
|
||||
return
|
||||
@@ -421,7 +433,13 @@ func _gui_input(event: InputEvent) -> void:
|
||||
accept_event()
|
||||
return
|
||||
if event.pressed and event.button_index in [MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_WHEEL_DOWN]:
|
||||
set_zoom(zoom_level + (0.5 if event.button_index == MOUSE_BUTTON_WHEEL_UP else -0.5), event.position)
|
||||
if pan_mode and not event.ctrl_pressed:
|
||||
var direction := 1.0 if event.button_index == MOUSE_BUTTON_WHEEL_UP else -1.0
|
||||
pan_offset.y += direction * _cell_dimensions().y * maxf(event.factor, 0.0)
|
||||
_clamp_pan()
|
||||
queue_redraw()
|
||||
else:
|
||||
set_zoom(zoom_level + (0.5 if event.button_index == MOUSE_BUTTON_WHEEL_UP else -0.5), event.position)
|
||||
accept_event()
|
||||
return
|
||||
if event is InputEventMouseMotion and _dragging:
|
||||
@@ -657,6 +675,16 @@ func _draw_congestion_overlay(bounds: Dictionary) -> void:
|
||||
draw_line(Vector2(rect.end.x, rect.position.y), Vector2(rect.position.x, rect.end.y), color, 2.0)
|
||||
|
||||
|
||||
## Visual-only level comes from the existing live FacilityInstance, not the
|
||||
## construction footprint dictionary. No upgrade or simulation value is written.
|
||||
func _facility_visual_level(instance_id: StringName) -> int:
|
||||
var service: Variant = _service(&"facility_service")
|
||||
if service == null or not service.has_method(&"get_instance"):
|
||||
return 1
|
||||
var instance: Variant = service.get_instance(instance_id)
|
||||
return int(instance.level) if instance != null else 1
|
||||
|
||||
|
||||
func _completed_facilities_for_draw() -> Array:
|
||||
if _drawing and _draw_cache.has(&"completed_facilities"):
|
||||
return _draw_cache[&"completed_facilities"]
|
||||
@@ -685,7 +713,8 @@ func _other_layer_rooms() -> Array[Dictionary]:
|
||||
if cells.is_empty(): continue
|
||||
var rect := _cell_bounds_rect(cells)
|
||||
if rect.intersects(Rect2(Vector2.ZERO, size)):
|
||||
rooms.append({&"id": facility[&"instance_id"], &"rect": rect, &"definition_id": facility[&"definition_id"], &"origin_x": cells[0].x})
|
||||
rooms.append({&"id": facility[&"instance_id"], &"rect": rect, &"definition_id": facility[&"definition_id"], &"origin_x": cells[0].x,
|
||||
&"level": _facility_visual_level(StringName(facility[&"instance_id"]))})
|
||||
for job: ConstructionJob in _construction_jobs_for_draw():
|
||||
if job.layer == layer_mode or job.state in [ConstructionJob.State.COMPLETED, ConstructionJob.State.CANCELLED]: continue
|
||||
if job.reserved_cells.is_empty(): continue
|
||||
@@ -748,7 +777,7 @@ func _draw_world_entity_icons() -> void:
|
||||
if not rect.intersects(Rect2(Vector2.ZERO,size)):
|
||||
continue
|
||||
var id := StringName(facility.get(&"definition_id",&""))
|
||||
PixelArt.room(self,rect,id,p,dungeon,town,layer_mode==LayerMode.BACK,cells[0].x, _room_presentations.get(StringName(facility.get(&"instance_id", &"")), {}))
|
||||
PixelArt.room(self,rect,id,p,dungeon,town,layer_mode==LayerMode.BACK,cells[0].x, _room_presentations.get(StringName(facility.get(&"instance_id", &"")), {}), _facility_visual_level(StringName(facility.get(&"instance_id", &""))))
|
||||
var instance_id := StringName(facility.get(&"instance_id",&""))
|
||||
# Thin status lamp conveys operational state without repainting a room.
|
||||
draw_rect(Rect2(rect.end.x-4*p,rect.position.y+2*p,2*p,2*p),_facility_color(instance_id))
|
||||
@@ -1255,7 +1284,7 @@ func _draw_pixel_actor(center: Vector2, key: String, staff: bool, state: int, re
|
||||
if staff and state == StaffRuntime.State.INCAPACITATED:
|
||||
mode = &"collapsed"
|
||||
elif receiving and activity in [&"standard_room", &"premium_room"]:
|
||||
mode = &"sleep"
|
||||
mode = &"standard_room_sleep" if activity == &"standard_room" else &"sleep"
|
||||
elif moving:
|
||||
mode = &"carry_walk" if carrying else &"walk"
|
||||
elif carrying:
|
||||
@@ -1322,6 +1351,7 @@ func _draw_build_preview() -> void:
|
||||
draw_rect(rect, fill)
|
||||
draw_rect(rect, edge, false, 2.0)
|
||||
|
||||
_draw_build_connection(_last_preview_report.get(&"connection", {}))
|
||||
|
||||
func _draw_excavation_preview() -> void:
|
||||
if not excavation_mode or not _hovered_cell is Vector3i:
|
||||
@@ -1434,6 +1464,7 @@ func _set_hovered_cell(value: Variant) -> void:
|
||||
|
||||
|
||||
func _update_build_preview() -> void:
|
||||
_preview_topology_key = _current_preview_topology()
|
||||
_last_preview_report = get_build_preview_report()
|
||||
build_preview_changed.emit(_last_preview_report.duplicate(true))
|
||||
|
||||
@@ -1451,6 +1482,8 @@ func _on_mouse_exited() -> void:
|
||||
|
||||
|
||||
func _on_snapshot_published(_snapshot: Dictionary = {}, _digest: String = "") -> void:
|
||||
_connection_cache_key.clear()
|
||||
_connection_cache_report.clear()
|
||||
queue_redraw()
|
||||
_update_build_preview()
|
||||
_update_excavation_preview()
|
||||
@@ -1567,3 +1600,88 @@ func _can_drop_data(_position: Vector2, data: Variant) -> bool:
|
||||
func _drop_data(position: Vector2, data: Variant) -> void:
|
||||
if _can_drop_data(position, data):
|
||||
staff_assignment_dropped.emit(data, facility_at_canvas(position))
|
||||
|
||||
|
||||
func _current_preview_topology() -> Array:
|
||||
var world: Variant = _service(&"world_service")
|
||||
var path: Variant = _service(&"path_service")
|
||||
return [world.revision if world != null else -1,
|
||||
path.graph_revision if path != null else -1,
|
||||
path.traffic_revision if path != null else -1]
|
||||
|
||||
|
||||
## Read-only connection preview. Shares ordinary facility access candidates and
|
||||
## accessible graph edges, but does not issue live path queries or change routing.
|
||||
## The displayed path proves connectivity; it is not a cheapest-route promise.
|
||||
func _build_connection_report(cells: Array[Vector3i], origin: Vector3i) -> Dictionary:
|
||||
var path: Variant = _service(&"path_service")
|
||||
if path == null or _runtime == null or cells.is_empty():
|
||||
return {&"status": &"UNAVAILABLE", &"reachable": false, &"path": []}
|
||||
var entrance: Variant = _runtime.call(&"_operations_entrance_cell")
|
||||
var key: Array = [origin, cells, _current_preview_topology(), entrance]
|
||||
if key == _connection_cache_key:
|
||||
return _connection_cache_report.duplicate(true)
|
||||
var result := {&"status": &"UNAVAILABLE", &"reachable": false,
|
||||
&"entrance": entrance, &"origin": origin, &"path": [], &"access_points": []}
|
||||
if entrance is Vector3i:
|
||||
var future := FacilityInstance.new()
|
||||
future.origin = origin
|
||||
future.occupied_cells.assign(cells)
|
||||
var candidates: Array = _runtime.call(&"_facility_access_candidates", future)
|
||||
result[&"access_points"] = candidates
|
||||
var targets: Dictionary = {}
|
||||
for cell: Vector3i in candidates: targets[cell] = true
|
||||
var queue: Array[Vector3i] = [entrance]
|
||||
var parents: Dictionary = {}
|
||||
parents[entrance] = entrance
|
||||
var cursor := 0
|
||||
var target: Variant = null
|
||||
while cursor < queue.size():
|
||||
var current := queue[cursor]
|
||||
cursor += 1
|
||||
if targets.has(current):
|
||||
target = current
|
||||
break
|
||||
for edge: Dictionary in path.get_neighbors(current):
|
||||
var next: Vector3i = edge[&"to"]
|
||||
if parents.has(next) or path.get_edge_report(current, next).is_empty():
|
||||
continue
|
||||
parents[next] = current
|
||||
queue.append(next)
|
||||
var route: Array[Vector3i] = []
|
||||
if target is Vector3i:
|
||||
var current: Vector3i = target
|
||||
while current != entrance:
|
||||
route.push_front(current)
|
||||
current = parents[current]
|
||||
route.push_front(entrance)
|
||||
result[&"reachable"] = not route.is_empty()
|
||||
result[&"status"] = &"CONNECTED" if result[&"reachable"] else &"DISCONNECTED"
|
||||
result[&"path"] = route
|
||||
_connection_cache_key = key.duplicate(true)
|
||||
_connection_cache_report = result.duplicate(true)
|
||||
return result
|
||||
|
||||
|
||||
func _draw_build_connection(report: Dictionary) -> void:
|
||||
if report.is_empty() or not _hovered_cell is Vector3i: return
|
||||
var status: StringName = report.get(&"status", &"UNAVAILABLE")
|
||||
var color := Color("8ff0ab") if status == &"CONNECTED" else Color("ffd080")
|
||||
var target := cell_to_canvas(_hovered_cell)
|
||||
var route: Array = report.get(&"path", [])
|
||||
if status == &"CONNECTED":
|
||||
for i in range(1, route.size()):
|
||||
var from: Vector3i = route[i-1]
|
||||
var to: Vector3i = route[i]
|
||||
if from.z == layer_mode and to.z == layer_mode:
|
||||
draw_line(cell_to_canvas(from), cell_to_canvas(to), color, 2.0)
|
||||
if not route.is_empty() and (route.back() as Vector3i).z == layer_mode:
|
||||
draw_line(cell_to_canvas(route.back()), target, color, 2.0)
|
||||
elif report.get(&"entrance") is Vector3i:
|
||||
# Dashed straight guide is a warning, never a traversable-path claim.
|
||||
draw_dashed_line(cell_to_canvas(report[&"entrance"]), target, color, 2.0, 8.0)
|
||||
draw_line(target-Vector2(6,6),target+Vector2(6,6),color,2.0)
|
||||
draw_line(target+Vector2(-6,6),target+Vector2(6,-6),color,2.0)
|
||||
var message := "入口への経路接続済" if status == &"CONNECTED" else "入口への経路未接続" if status == &"DISCONNECTED" else "入口・経路を確認できません"
|
||||
draw_string(ThemeDB.fallback_font, target+Vector2(8,-12), message,
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, color)
|
||||
|
||||
@@ -76,13 +76,14 @@ func drain_ready(current_tick: int) -> Array[SimulationCommand]:
|
||||
break
|
||||
_pending.pop_front()
|
||||
_in_flight[command.command_id] = command
|
||||
ready.append(command)
|
||||
ready.append(command.copy())
|
||||
return ready
|
||||
|
||||
|
||||
func execute_ready(current_tick: int, executor: Callable) -> Array[CommandResult]:
|
||||
var results: Array[CommandResult] = []
|
||||
for command in drain_ready(current_tick):
|
||||
var accepted_command_id := command.command_id
|
||||
var result: CommandResult
|
||||
if not executor.is_valid():
|
||||
result = CommandResult.failed(
|
||||
@@ -103,10 +104,8 @@ func execute_ready(current_tick: int, executor: Callable) -> Array[CommandResult
|
||||
"Executor did not return a successful CommandResult."
|
||||
)
|
||||
|
||||
if result.command_id == 0:
|
||||
result.command_id = command.command_id
|
||||
complete(command.command_id, result)
|
||||
results.append(result.copy())
|
||||
var completed_result := complete(accepted_command_id, result)
|
||||
results.append(completed_result)
|
||||
return results
|
||||
|
||||
|
||||
@@ -164,6 +163,14 @@ func processed_count() -> int:
|
||||
return _processed_results.size()
|
||||
|
||||
|
||||
## Read only pending commands, without serializing the processed history.
|
||||
func get_pending_commands() -> Array[SimulationCommand]:
|
||||
var result: Array[SimulationCommand] = []
|
||||
for command: SimulationCommand in _pending:
|
||||
result.append(command.copy())
|
||||
return result
|
||||
|
||||
|
||||
func get_accepted_command_metadata() -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
var command_ids: Array = _accepted_metadata.keys()
|
||||
|
||||
@@ -26,6 +26,9 @@ const DEFAULT_BUDGETS := {
|
||||
var sample_capacity := DEFAULT_SAMPLE_CAPACITY
|
||||
var _budgets: Dictionary = {}
|
||||
var _samples: Dictionary = {}
|
||||
# Derived diagnostic index; never part of game Save snapshots.
|
||||
var _ordered_samples: Dictionary = {}
|
||||
var _ordered_sample_bytes: Dictionary = {}
|
||||
var _totals: Dictionary = {}
|
||||
var _sample_counts: Dictionary = {}
|
||||
|
||||
@@ -59,12 +62,22 @@ func record_sample(metric_id: StringName, duration_us: int) -> Dictionary:
|
||||
if not _samples.has(metric_id):
|
||||
_samples[metric_id] = []
|
||||
var values: Array = _samples[metric_id]
|
||||
var use_index: bool = get_script().resource_path == "res://scripts/core/performance_budget.gd" and _integer_window(values)
|
||||
var ordered: Array[int] = []
|
||||
if use_index: ordered = _validated_sample_order(metric_id, values)
|
||||
values.append(duration_us)
|
||||
if use_index: ordered.insert(ordered.bsearch(duration_us), duration_us)
|
||||
if values.size() > sample_capacity:
|
||||
values.pop_front()
|
||||
var removed: Variant = values.pop_front()
|
||||
if use_index: ordered.remove_at(ordered.bsearch(int(removed)))
|
||||
if use_index:
|
||||
_ordered_sample_bytes[metric_id] = var_to_bytes(values)
|
||||
else:
|
||||
_ordered_samples.erase(metric_id)
|
||||
_ordered_sample_bytes.erase(metric_id)
|
||||
_totals[metric_id] = int(_totals.get(metric_id, 0)) + duration_us
|
||||
_sample_counts[metric_id] = int(_sample_counts.get(metric_id, 0)) + 1
|
||||
var report := get_report(metric_id)
|
||||
var report: Dictionary = _indexed_report(metric_id, values, ordered) if use_index else get_report(metric_id)
|
||||
if bool(report.get(&"budget_exceeded", false)):
|
||||
budget_exceeded.emit(metric_id, report.duplicate(true))
|
||||
return {&"ok": true, &"report": report}
|
||||
@@ -92,15 +105,25 @@ func record_simulation_frame(duration_us: int, speed: int, ticks_run: int) -> Di
|
||||
|
||||
|
||||
func get_report(metric_id: StringName) -> Dictionary:
|
||||
var values: Array = _samples.get(metric_id, [])
|
||||
var raw: Array = _samples.get(metric_id, [])
|
||||
if get_script().resource_path != "res://scripts/core/performance_budget.gd" or not _integer_window(raw):
|
||||
return SimulationPerformanceBudget._legacy_report(self, metric_id)
|
||||
var ordered: Array[int] = []
|
||||
return _indexed_report(metric_id, raw, ordered, true)
|
||||
|
||||
|
||||
func _indexed_report(metric_id: StringName, values: Array, ordered: Array[int], validate_order: bool = false) -> Dictionary:
|
||||
var count := values.size()
|
||||
var lifetime_count := int(_sample_counts.get(metric_id, 0))
|
||||
var lifetime_total := int(_totals.get(metric_id, 0))
|
||||
var average_us := lifetime_total / lifetime_count if lifetime_count > 0 else 0
|
||||
var maximum_us := 0
|
||||
for raw_value: Variant in values:
|
||||
maximum_us = maxi(maximum_us, int(raw_value))
|
||||
var p95_us := _percentile(values, 95)
|
||||
if validate_order: ordered = _validated_sample_order(metric_id, values)
|
||||
var maximum_us := maxi(0, ordered[-1]) if not ordered.is_empty() else 0
|
||||
var p95_us := 0
|
||||
if not ordered.is_empty():
|
||||
var rank_numerator := 95 * ordered.size()
|
||||
var index := maxi((rank_numerator + 99) / 100 - 1, 0)
|
||||
p95_us = ordered[mini(index, ordered.size() - 1)]
|
||||
var budget: Dictionary = _budgets.get(metric_id, {})
|
||||
var aggregation := int(budget.get(&"aggregation", Aggregation.P95))
|
||||
var measured_us := _aggregated_value(aggregation, average_us, p95_us, maximum_us)
|
||||
@@ -139,10 +162,14 @@ func get_all_reports() -> Array[Dictionary]:
|
||||
func clear(metric_id: StringName = &"") -> void:
|
||||
if metric_id.is_empty():
|
||||
_samples.clear()
|
||||
_ordered_samples.clear()
|
||||
_ordered_sample_bytes.clear()
|
||||
_totals.clear()
|
||||
_sample_counts.clear()
|
||||
return
|
||||
_samples.erase(metric_id)
|
||||
_ordered_samples.erase(metric_id)
|
||||
_ordered_sample_bytes.erase(metric_id)
|
||||
_totals.erase(metric_id)
|
||||
_sample_counts.erase(metric_id)
|
||||
|
||||
@@ -172,3 +199,55 @@ func _percentile(raw_values: Array, percentile: int) -> int:
|
||||
var rank_numerator := clampi(percentile, 0, 100) * values.size()
|
||||
var index := maxi((rank_numerator + 99) / 100 - 1, 0)
|
||||
return values[mini(index, values.size() - 1)]
|
||||
|
||||
|
||||
static func _integer_window(values: Array) -> bool:
|
||||
for raw_value: Variant in values:
|
||||
if typeof(raw_value) != TYPE_INT: return false
|
||||
return true
|
||||
|
||||
|
||||
func _validated_sample_order(metric_id: StringName, values: Array) -> Array[int]:
|
||||
# No retained state for unknown metrics; raw dictionary removal also drops its index.
|
||||
if not _samples.has(metric_id):
|
||||
_ordered_samples.erase(metric_id)
|
||||
_ordered_sample_bytes.erase(metric_id)
|
||||
return []
|
||||
# Complete immutable raw bytes detect in-place/replaced windows, not just length.
|
||||
var current_bytes := var_to_bytes(values)
|
||||
if not _ordered_samples.has(metric_id) or _ordered_sample_bytes.get(metric_id, PackedByteArray()) != current_bytes:
|
||||
var ordered: Array[int] = []
|
||||
for raw_value: Variant in values: ordered.append(int(raw_value))
|
||||
ordered.sort()
|
||||
_ordered_samples[metric_id] = ordered
|
||||
_ordered_sample_bytes[metric_id] = current_bytes
|
||||
return _ordered_samples[metric_id]
|
||||
|
||||
|
||||
static func _legacy_report(owner: SimulationPerformanceBudget, metric_id: StringName) -> Dictionary:
|
||||
var values: Array = owner._samples.get(metric_id, [])
|
||||
var count := values.size()
|
||||
var lifetime_count := int(owner._sample_counts.get(metric_id, 0))
|
||||
var lifetime_total := int(owner._totals.get(metric_id, 0))
|
||||
var average_us := lifetime_total / lifetime_count if lifetime_count > 0 else 0
|
||||
var maximum_us := 0
|
||||
for raw_value: Variant in values:
|
||||
maximum_us = maxi(maximum_us, int(raw_value))
|
||||
var p95_us := owner._percentile(values, 95)
|
||||
var budget: Dictionary = owner._budgets.get(metric_id, {})
|
||||
var aggregation := int(budget.get(&"aggregation", Aggregation.P95))
|
||||
var measured_us := owner._aggregated_value(aggregation, average_us, p95_us, maximum_us)
|
||||
var limit_us := int(budget.get(&"limit_us", 0))
|
||||
return {
|
||||
&"metric_id": metric_id,
|
||||
&"window_sample_count": count,
|
||||
&"lifetime_sample_count": lifetime_count,
|
||||
&"average_us": average_us,
|
||||
&"p95_us": p95_us,
|
||||
&"maximum_us": maximum_us,
|
||||
&"aggregation": aggregation,
|
||||
&"measured_us": measured_us,
|
||||
&"limit_us": limit_us,
|
||||
&"budget_exceeded": limit_us > 0 and measured_us > limit_us,
|
||||
}
|
||||
|
||||
|
||||
+205
-11
@@ -2,6 +2,10 @@ class_name SaveService
|
||||
extends RefCounted
|
||||
|
||||
const FORMAT_VERSION := 1
|
||||
const MIMI_PURCHASE_FORMAT_VERSION := 2
|
||||
const SUPPORTED_CURRENT_FORMAT := -2
|
||||
const PurchaseQuoteV2 := preload("res://scripts/supply/supply_purchase_quote_v2.gd")
|
||||
const PurchaseQuotePlansV2 := preload("res://scripts/supply/supply_auto_purchase_quote_plans.gd")
|
||||
const MAGIC := "TTWB"
|
||||
const WORLD_FILE := "world.bin"
|
||||
const MANIFEST_FILE := "manifest.json"
|
||||
@@ -27,12 +31,15 @@ static func save_snapshot(save_path: String, snapshot: Dictionary, metadata := {
|
||||
var resolved := _resolve_safe_save_path(save_path)
|
||||
if not resolved[&"ok"]:
|
||||
return _complete_timed_operation(resolved, &"save", started_at_us)
|
||||
var selected_format := _snapshot_package_format(snapshot)
|
||||
if selected_format < 0:
|
||||
return _complete_timed_operation(_failure(&"INVALID_SNAPSHOT", "unsupported purchase Runtime/domain"), &"save", started_at_us)
|
||||
var result := _save_snapshot_absolute(
|
||||
resolved[&"path"],
|
||||
save_path,
|
||||
snapshot,
|
||||
metadata,
|
||||
FORMAT_VERSION,
|
||||
selected_format,
|
||||
resolved[&"path"],
|
||||
resolved[&"path"]
|
||||
)
|
||||
@@ -47,7 +54,7 @@ static func load_snapshot(save_path: String) -> Dictionary:
|
||||
var reconcile := _reconcile_interrupted_formal_save(resolved[&"path"])
|
||||
if not reconcile[&"ok"]:
|
||||
return _complete_timed_operation(reconcile, &"load", started_at_us)
|
||||
var result := _load_snapshot_absolute(resolved[&"path"], FORMAT_VERSION)
|
||||
var result := _load_snapshot_absolute(resolved[&"path"], SUPPORTED_CURRENT_FORMAT)
|
||||
result[&"interrupted_save_reconciled"] = bool(reconcile.get(&"reconciled", false))
|
||||
return _complete_timed_operation(result, &"load", started_at_us)
|
||||
|
||||
@@ -60,7 +67,7 @@ static func inspect_snapshot(save_path: String) -> Dictionary:
|
||||
var resolved := _resolve_safe_save_path(save_path)
|
||||
var result: Dictionary = resolved
|
||||
if resolved[&"ok"]:
|
||||
result = _load_snapshot_absolute(resolved[&"path"], FORMAT_VERSION)
|
||||
result = _load_snapshot_absolute(resolved[&"path"], SUPPORTED_CURRENT_FORMAT)
|
||||
result[&"read_only"] = true
|
||||
return _complete_timed_operation(result, &"load", started_at_us)
|
||||
|
||||
@@ -98,6 +105,9 @@ static func save_autosave(
|
||||
if not resolved[&"ok"]:
|
||||
return _complete_timed_operation(resolved, &"save", started_at_us)
|
||||
var target_path: String = resolved[&"path"]
|
||||
var selected_format := _snapshot_package_format(snapshot)
|
||||
if selected_format < 0:
|
||||
return _complete_timed_operation(_failure(&"INVALID_SNAPSHOT", "unsupported purchase Runtime/domain"), &"save", started_at_us)
|
||||
var pending_path := target_path + ".autosave.pending"
|
||||
var reconcile := _reconcile_autosave_rotation(target_path, pending_path, generation_count)
|
||||
if not reconcile[&"ok"]:
|
||||
@@ -137,7 +147,7 @@ static func save_autosave(
|
||||
get_autosave_path(save_path, 0),
|
||||
snapshot,
|
||||
candidate_metadata,
|
||||
FORMAT_VERSION,
|
||||
selected_format,
|
||||
target_path + AUTOSAVE_SUFFIX + "0",
|
||||
target_path,
|
||||
false
|
||||
@@ -201,7 +211,11 @@ static func inspect_recovery_candidates(
|
||||
var formal_reconcile := _reconcile_interrupted_formal_save(target_path)
|
||||
if not formal_reconcile[&"ok"]:
|
||||
return formal_reconcile
|
||||
var migration_reconcile := _reconcile_interrupted_migration(target_path)
|
||||
var migration_reconcile: Dictionary = (
|
||||
formal_reconcile[&"migration_reconciliation"]
|
||||
if formal_reconcile.has(&"migration_reconciliation")
|
||||
else _reconcile_interrupted_migration(target_path)
|
||||
)
|
||||
if not migration_reconcile[&"ok"]:
|
||||
return migration_reconcile
|
||||
var autosave_reconcile := _reconcile_autosave_rotation(
|
||||
@@ -235,7 +249,7 @@ static func inspect_recovery_candidates(
|
||||
var current_schema_valid := false
|
||||
var current_format := false
|
||||
if package_valid:
|
||||
current_format = int(loaded[&"manifest"].get("format_version", -1)) == FORMAT_VERSION
|
||||
current_format = _package_domain_matches(loaded)
|
||||
current_schema_valid = bool(SimulationSnapshot.validate(loaded[&"snapshot"])[&"ok"])
|
||||
var loadable := package_valid and current_format and current_schema_valid
|
||||
var candidate_code: StringName = StringName(loaded[&"code"])
|
||||
@@ -359,7 +373,7 @@ static func _load_recovery_candidate_impl(
|
||||
candidate_path = target_path + AUTOSAVE_SUFFIX + str(generation)
|
||||
else:
|
||||
return _failure(&"UNKNOWN_RECOVERY_CANDIDATE", str(candidate_id))
|
||||
var result := _load_snapshot_absolute(candidate_path, FORMAT_VERSION)
|
||||
var result := _load_snapshot_absolute(candidate_path, SUPPORTED_CURRENT_FORMAT)
|
||||
if not result[&"ok"]:
|
||||
return result
|
||||
result[&"candidate_id"] = candidate_id
|
||||
@@ -396,7 +410,9 @@ static func replace_snapshot_with_backup(save_path: String, expected: Dictionary
|
||||
if not validation[&"ok"]:
|
||||
return _failure(&"MIGRATION_INVALID_SNAPSHOT", ", ".join(validation[&"errors"]))
|
||||
var started := Time.get_ticks_usec()
|
||||
return _complete_timed_operation(_migrate_save_impl(save_path, null, FORMAT_VERSION, expected, candidate), &"save", started)
|
||||
var selected_format := _snapshot_package_format(candidate)
|
||||
if selected_format < 0: return _failure(&"MIGRATION_INVALID_SNAPSHOT", "unsupported purchase Runtime/domain")
|
||||
return _complete_timed_operation(_migrate_save_impl(save_path, null, selected_format, expected, candidate), &"save", started)
|
||||
|
||||
|
||||
static func _migrate_save_impl(
|
||||
@@ -664,6 +680,8 @@ static func _save_snapshot_absolute(
|
||||
identity_source_path := "",
|
||||
use_transaction_marker := true
|
||||
) -> Dictionary:
|
||||
if _snapshot_package_format(snapshot) != format_version:
|
||||
return _failure(&"INVALID_SNAPSHOT", "package/domain format differs")
|
||||
var validation := SimulationSnapshot.validate(snapshot)
|
||||
if not validation[&"ok"]:
|
||||
return _failure(&"INVALID_SNAPSHOT", ", ".join(validation[&"errors"]))
|
||||
@@ -674,6 +692,9 @@ static func _save_snapshot_absolute(
|
||||
return _failure(&"UNSAFE_PATH", "save path contains a symbolic link")
|
||||
var interrupted_reconcile := {&"ok": true, &"reconciled": false, &"action": &"none"}
|
||||
if use_transaction_marker:
|
||||
var format_guard := _guard_ordinary_save_format(target_path, format_version)
|
||||
if not format_guard[&"ok"]:
|
||||
return format_guard
|
||||
interrupted_reconcile = _reconcile_interrupted_formal_save(target_path)
|
||||
if not interrupted_reconcile[&"ok"]:
|
||||
return interrupted_reconcile
|
||||
@@ -754,6 +775,85 @@ static func _save_snapshot_absolute(
|
||||
}
|
||||
|
||||
|
||||
## Ordinary Save must not act as an implicit physical-format migration.
|
||||
## This preflight only reads the formal package and the candidate which the
|
||||
## existing normal-save reconciliation rules could promote. Explicit migration
|
||||
## APIs and autosave rotation keep their own unchanged contracts.
|
||||
static func _guard_ordinary_save_format(target_path: String, requested_format: int) -> Dictionary:
|
||||
var formal_guard := _guard_identified_formal_format(target_path)
|
||||
if not formal_guard[&"ok"]:
|
||||
return formal_guard
|
||||
var marker_path := target_path + SAVE_TRANSACTION_SUFFIX
|
||||
var marker_exists := FileAccess.file_exists(marker_path) or DirAccess.dir_exists_absolute(marker_path)
|
||||
if marker_exists and not _formal_transaction_marker_is_valid(marker_path):
|
||||
return _failure(&"TRANSACTION_MARKER_CONFLICT", marker_path)
|
||||
var identified := _inspect_identified_save_formats(target_path)
|
||||
if not identified[&"ok"]:
|
||||
return identified
|
||||
for existing_format: int in identified[&"format_versions"]:
|
||||
if existing_format != requested_format:
|
||||
return _ordinary_format_migration_required(target_path, existing_format, requested_format)
|
||||
var target_status := _package_status(target_path)
|
||||
if target_status[&"current_loadable"] or (target_status[&"exists"] and not marker_exists):
|
||||
return {&"ok": true, &"code": &"OK"}
|
||||
if target_status[&"exists"] and not _looks_like_save_package(target_path):
|
||||
return _failure(&"FORMAL_RECOVERY_TARGET_CONFLICT", target_path)
|
||||
# Reconciliation chooses a verified .tmp first, then a verified .bak. A
|
||||
# missing/invalid formal does not make a retained old-format source a new slot.
|
||||
for candidate_path: String in [target_path + ".tmp", target_path + ".bak"]:
|
||||
var status := _package_status(candidate_path)
|
||||
if not status[&"current_loadable"]:
|
||||
continue
|
||||
var existing_format := int(status[&"format_version"])
|
||||
if existing_format != requested_format:
|
||||
return _ordinary_format_migration_required(candidate_path, existing_format, requested_format)
|
||||
break
|
||||
return {&"ok": true, &"code": &"OK"}
|
||||
|
||||
|
||||
## A corrupt format1 checksum/manifest still identifies a retained format when
|
||||
## either its bounded manifest number or fixed TTWB header says so. Never use a
|
||||
## schema-looking nested string or coerce a non-integral manifest value.
|
||||
static func _inspect_identified_save_formats(target_path: String) -> Dictionary:
|
||||
var versions: Array[int] = []
|
||||
var manifest_path := target_path.path_join(MANIFEST_FILE)
|
||||
if FileAccess.file_exists(manifest_path):
|
||||
if not _package_file_is_safe_for_read(manifest_path):
|
||||
return _failure(&"UNSAFE_PATH", manifest_path)
|
||||
var manifest_file := FileAccess.open(manifest_path, FileAccess.READ)
|
||||
if manifest_file == null:
|
||||
return _failure(&"INVALID_MANIFEST", error_string(FileAccess.get_open_error()))
|
||||
if manifest_file.get_length() <= MAX_MANIFEST_BYTES:
|
||||
var parser := JSON.new()
|
||||
if parser.parse(manifest_file.get_as_text()) == OK and parser.data is Dictionary:
|
||||
var value: Variant = parser.data.get("format_version", null)
|
||||
if _is_integral_json_number(value, 0, 0xffffffff):
|
||||
versions.append(int(value))
|
||||
manifest_file = null
|
||||
var world_path := target_path.path_join(WORLD_FILE)
|
||||
if FileAccess.file_exists(world_path):
|
||||
if not _package_file_is_safe_for_read(world_path):
|
||||
return _failure(&"UNSAFE_PATH", world_path)
|
||||
var world_file := FileAccess.open(world_path, FileAccess.READ)
|
||||
if world_file == null:
|
||||
return _failure(&"READ_WORLD_FAILED", error_string(FileAccess.get_open_error()))
|
||||
if world_file.get_length() >= 8 and world_file.get_buffer(4).get_string_from_ascii() == MAGIC:
|
||||
var version := world_file.get_32()
|
||||
if version not in versions:
|
||||
versions.append(version)
|
||||
world_file = null
|
||||
return {&"ok": true, &"code": &"OK", &"format_versions": versions}
|
||||
|
||||
|
||||
static func _ordinary_format_migration_required(source_path: String, existing_format: int, requested_format: int) -> Dictionary:
|
||||
return _with_context(_failure(&"FORMAT_MIGRATION_REQUIRED", "%d -> %d requires explicit migration or a new slot" % [existing_format, requested_format]), {
|
||||
&"existing_format_version": existing_format,
|
||||
&"requested_format_version": requested_format,
|
||||
&"migration_source_path": source_path,
|
||||
&"formal_and_artifacts_preserved": true,
|
||||
})
|
||||
|
||||
|
||||
static func _write_package_directory(
|
||||
package_path: String,
|
||||
snapshot: Dictionary,
|
||||
@@ -761,6 +861,8 @@ static func _write_package_directory(
|
||||
format_version: int,
|
||||
logical_target_path := ""
|
||||
) -> Dictionary:
|
||||
if format_version in [FORMAT_VERSION, MIMI_PURCHASE_FORMAT_VERSION] and _snapshot_package_format(snapshot) != format_version:
|
||||
return _failure(&"INVALID_SNAPSHOT", "package/domain format differs")
|
||||
if not _operation_paths_are_safe([package_path]):
|
||||
return _failure(&"UNSAFE_PATH", "package path contains a symbolic link")
|
||||
if not _path_is_missing_or_package(package_path):
|
||||
@@ -812,6 +914,8 @@ static func _write_package_directory(
|
||||
&"checksum_world": checksum,
|
||||
&"uncompressed_size": raw_world.size(),
|
||||
}
|
||||
if format_version == MIMI_PURCHASE_FORMAT_VERSION:
|
||||
manifest[&"runtime_schema_version"] = 15
|
||||
for key: Variant in metadata:
|
||||
if key in [&"thumbnail_png", &"thumbnail_path", &"_thumbnail_png"]:
|
||||
continue
|
||||
@@ -839,7 +943,7 @@ static func _write_package_directory(
|
||||
return {&"ok": true, &"code": &"OK", &"manifest": manifest}
|
||||
|
||||
|
||||
static func _load_snapshot_absolute(
|
||||
static func _load_snapshot_absolute_v1(
|
||||
target_path: String,
|
||||
expected_format_version: int,
|
||||
validate_snapshot := true
|
||||
@@ -932,6 +1036,87 @@ static func _load_snapshot_absolute(
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
## Format choice comes only from the decoded owned Runtime, never from a
|
||||
## recursively found schema-looking string or the duplicate manifest field.
|
||||
static func _snapshot_package_format(snapshot: Dictionary) -> int:
|
||||
var systems: Variant = snapshot.get(&"systems", {})
|
||||
if not systems is Dictionary: return FORMAT_VERSION # existing generic extra payload
|
||||
var runtime_data: Variant = systems.get(&"runtime", null)
|
||||
if runtime_data == null: return FORMAT_VERSION # existing generic envelope
|
||||
if not runtime_data is Dictionary: return FORMAT_VERSION # no owned Runtime authority
|
||||
var version: Variant = runtime_data.get(&"runtime_schema_version", 1)
|
||||
if typeof(version) != TYPE_INT: return -1
|
||||
if version >= 1 and version <= 14:
|
||||
return -1 if PurchaseQuoteV2.has_history(runtime_data) else FORMAT_VERSION
|
||||
if version != 15: return -1
|
||||
if not _canonical_owned_key(snapshot, &"systems") or not _canonical_owned_key(systems, &"runtime"): return -1
|
||||
var clock: Variant = snapshot.get(&"clock")
|
||||
if not clock is Dictionary or typeof(clock.get(&"absolute_minute")) != TYPE_INT: return -1
|
||||
return MIMI_PURCHASE_FORMAT_VERSION if PurchaseQuotePlansV2.valid_runtime_format(runtime_data, clock[&"absolute_minute"]) else -1
|
||||
|
||||
|
||||
static func _canonical_owned_key(owner: Dictionary, key: StringName) -> bool:
|
||||
for raw: Variant in owner:
|
||||
if raw == key: return typeof(raw) == TYPE_STRING_NAME
|
||||
return false
|
||||
|
||||
|
||||
static func _package_domain_matches(loaded: Dictionary) -> bool:
|
||||
if not bool(loaded.get(&"ok", false)): return false
|
||||
var manifest: Dictionary = loaded[&"manifest"]
|
||||
var format_version := int(manifest.get("format_version", -1))
|
||||
if format_version not in [FORMAT_VERSION, MIMI_PURCHASE_FORMAT_VERSION]: return false
|
||||
if format_version != _snapshot_package_format(loaded[&"snapshot"]): return false
|
||||
if format_version == MIMI_PURCHASE_FORMAT_VERSION:
|
||||
var duplicate_version: Variant = manifest.get("runtime_schema_version")
|
||||
return _is_integral_json_number(duplicate_version, 15, 15)
|
||||
return true
|
||||
|
||||
|
||||
static func _load_snapshot_absolute(target_path: String, expected_format_version: int,
|
||||
validate_snapshot := true) -> Dictionary:
|
||||
# -2 means either supported current format; -1 still decodes historical
|
||||
# physical packages for explicit migration/reporting. Positive verification
|
||||
# requests keep their exact original physical-format check.
|
||||
if expected_format_version == SUPPORTED_CURRENT_FORMAT:
|
||||
var guard := _guard_identified_formal_format(target_path)
|
||||
if not guard[&"ok"]: return guard
|
||||
var expected := -1 if expected_format_version == SUPPORTED_CURRENT_FORMAT else expected_format_version
|
||||
var loaded := _load_snapshot_absolute_v1(target_path, expected, validate_snapshot)
|
||||
if not loaded[&"ok"]: return loaded
|
||||
var version := int(loaded[&"manifest"].get("format_version", -1))
|
||||
if version in [FORMAT_VERSION, MIMI_PURCHASE_FORMAT_VERSION]:
|
||||
if not _package_domain_matches(loaded):
|
||||
return _with_context(_failure(&"UNSUPPORTED_FORMAT", "package/domain crosslink differs"), {&"formal_and_artifacts_preserved": true})
|
||||
elif expected_format_version == SUPPORTED_CURRENT_FORMAT:
|
||||
return _failure(&"UNSUPPORTED_FORMAT", str(version))
|
||||
return loaded
|
||||
|
||||
|
||||
static func _guard_identified_formal_format(target_path: String) -> Dictionary:
|
||||
var original := _guard_identified_formal_format_v1(target_path)
|
||||
if not original[&"ok"]:
|
||||
# A checksum-damaged future format remains protected by the original
|
||||
# prefix/manifest probe. Only one coherent, fully decoded known2 passes.
|
||||
if original[&"code"] != &"UNSUPPORTED_FORMAT" \
|
||||
or original.get(&"identified_format_version") != MIMI_PURCHASE_FORMAT_VERSION \
|
||||
or original.get(&"identified_format_versions") != [MIMI_PURCHASE_FORMAT_VERSION]: return original
|
||||
var known := _load_snapshot_absolute_v1(target_path, MIMI_PURCHASE_FORMAT_VERSION, true)
|
||||
return {&"ok": true, &"code": &"OK"} if _package_domain_matches(known) else original
|
||||
# Existing malformed/checksum-broken format1 recovery remains allowed. A
|
||||
# successfully decoded Runtime15 hidden in format1 may not be overwritten.
|
||||
var known := _load_snapshot_absolute_v1(target_path, FORMAT_VERSION, false)
|
||||
if known[&"ok"] and not _package_domain_matches(known):
|
||||
var invalid := _failure(&"UNSUPPORTED_FORMAT", "format1 contains reserved/unsupported Runtime history")
|
||||
invalid[&"identified_format_version"] = FORMAT_VERSION
|
||||
invalid[&"identified_format_versions"] = [FORMAT_VERSION]
|
||||
invalid[&"formal_and_artifacts_preserved"] = true
|
||||
return invalid
|
||||
return original
|
||||
|
||||
|
||||
static func _is_integral_json_number(value: Variant, minimum: int, maximum: int) -> bool:
|
||||
if typeof(value) == TYPE_INT:
|
||||
return value >= minimum and value <= maximum
|
||||
@@ -947,7 +1132,7 @@ static func _world_size_is_supported(size: int) -> bool:
|
||||
## Probe only bounded metadata and the fixed binary prefix. An identifiable
|
||||
## unsupported formal format must be rejected before any transaction artifact
|
||||
## is reconciled, even when its compressed payload/checksum is damaged.
|
||||
static func _guard_identified_formal_format(target_path: String) -> Dictionary:
|
||||
static func _guard_identified_formal_format_v1(target_path: String) -> Dictionary:
|
||||
var versions: Array[int] = []
|
||||
var manifest_path := target_path.path_join(MANIFEST_FILE)
|
||||
if FileAccess.file_exists(manifest_path):
|
||||
@@ -1001,6 +1186,15 @@ static func _reconcile_interrupted_formal_save(target_path: String) -> Dictionar
|
||||
if marker_exists and not _formal_transaction_marker_is_valid(marker_path):
|
||||
return _failure(&"TRANSACTION_MARKER_CONFLICT", marker_path)
|
||||
|
||||
# A held migration source owns the absent/formally promoted target. Normal
|
||||
# .bak is an older ordinary-save generation and must not take that name.
|
||||
# Verify migration artifacts before allowing normal-save reconciliation.
|
||||
if DirAccess.dir_exists_absolute(target_path + ".migration.original"):
|
||||
if marker_exists:
|
||||
return _failure(&"MIGRATION_FORMAL_TRANSACTION_CONFLICT", target_path)
|
||||
var migration := _reconcile_interrupted_migration(target_path)
|
||||
return _with_context(migration, {&"migration_reconciliation": migration.duplicate(true)})
|
||||
|
||||
var target_status := _package_status(target_path)
|
||||
var temporary_status := _package_status(temporary_path)
|
||||
var backup_status := _package_status(backup_path)
|
||||
@@ -1425,7 +1619,7 @@ static func _package_status(path: String) -> Dictionary:
|
||||
&"current_loadable": false,
|
||||
&"code": loaded[&"code"],
|
||||
}
|
||||
var current_format := int(loaded[&"manifest"].get("format_version", -1)) == FORMAT_VERSION
|
||||
var current_format := _package_domain_matches(loaded)
|
||||
var schema_valid := bool(SimulationSnapshot.validate(loaded[&"snapshot"])[&"ok"])
|
||||
return {
|
||||
&"exists": true,
|
||||
|
||||
@@ -62,7 +62,7 @@ func stage_dictionary_set(
|
||||
return true,
|
||||
func() -> void:
|
||||
if had_key:
|
||||
target[key] = original_value
|
||||
target[key] = _copy_value(original_snapshot)
|
||||
else:
|
||||
target.erase(key)
|
||||
)
|
||||
@@ -94,7 +94,7 @@ func stage_dictionary_erase(
|
||||
return true,
|
||||
func() -> void:
|
||||
if had_key:
|
||||
target[key] = original_value
|
||||
target[key] = _copy_value(original_snapshot)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ var _transaction_ids: Dictionary = {}
|
||||
var _idempotency_keys: Dictionary = {}
|
||||
var _next_transaction_sequence := 1
|
||||
var _pending_transaction_notifications: Dictionary = {}
|
||||
# Transient nested observer boundary; never serialized as gameplay state.
|
||||
var _notification_scopes: Array[Array] = []
|
||||
|
||||
|
||||
func _init(account_definitions: Dictionary = {}) -> void:
|
||||
@@ -308,7 +310,10 @@ func post(
|
||||
_next_transaction_sequence = generated_sequence + 1
|
||||
var posted_entries := get_transaction_entries(effective_id)
|
||||
if emit_notification:
|
||||
transaction_posted.emit(effective_id, posted_entries)
|
||||
if _notification_scopes.is_empty():
|
||||
transaction_posted.emit(effective_id, posted_entries)
|
||||
else:
|
||||
_notification_scopes.back().append(effective_id)
|
||||
else:
|
||||
_pending_transaction_notifications[effective_id] = true
|
||||
return CommandResultType.succeeded(0, {
|
||||
@@ -318,6 +323,36 @@ func post(
|
||||
})
|
||||
|
||||
|
||||
## Retain ordinary post semantics while postponing observer delivery through a
|
||||
## complete event effect/cost boundary. Explicit emit_notification=false remains
|
||||
## owned by its existing caller, including Operations and supply transactions.
|
||||
func notification_scope_depth() -> int:
|
||||
return _notification_scopes.size()
|
||||
|
||||
|
||||
func begin_notification_scope() -> void:
|
||||
_notification_scopes.append([])
|
||||
|
||||
|
||||
func end_notification_scope() -> void:
|
||||
assert(not _notification_scopes.is_empty(), "Balanced notification scope required.")
|
||||
if _notification_scopes.is_empty():
|
||||
return
|
||||
var queued: Array = _notification_scopes.pop_back()
|
||||
if not _notification_scopes.is_empty():
|
||||
_notification_scopes.back().append_array(queued)
|
||||
return
|
||||
var seen: Dictionary = {}
|
||||
for raw_id: Variant in queued:
|
||||
var transaction_id := StringName(raw_id)
|
||||
# Restoring a ledger removes rolled-back IDs. A failure branch can reuse
|
||||
# an ID; deliver the surviving final receipt once in insertion order.
|
||||
if seen.has(transaction_id) or not _transaction_ids.has(transaction_id):
|
||||
continue
|
||||
seen[transaction_id] = true
|
||||
transaction_posted.emit(transaction_id, get_transaction_entries(transaction_id))
|
||||
|
||||
|
||||
func has_pending_transaction_notification(transaction_id: StringName) -> bool:
|
||||
return _pending_transaction_notifications.has(transaction_id)
|
||||
|
||||
|
||||
+200
-35
@@ -5,6 +5,9 @@ extends RefCounted
|
||||
## choice authority. Narrative text is deliberately outside this service; only
|
||||
## localization keys, content ratings/tags and safe replacement IDs are retained.
|
||||
|
||||
signal effect_transaction_started
|
||||
signal effect_transaction_finished
|
||||
|
||||
signal event_registered(definition: EventDefinition)
|
||||
signal event_enqueued(instance: EventInstance)
|
||||
signal event_started(instance: EventInstance)
|
||||
@@ -47,6 +50,7 @@ var _cost_releaser := Callable()
|
||||
var _cost_committer := Callable()
|
||||
var _condition_callbacks: Dictionary = {}
|
||||
var _relationship_service: Variant = null
|
||||
var _candidate_eligibility_guard := Callable()
|
||||
|
||||
|
||||
func configure_callbacks(
|
||||
@@ -70,6 +74,19 @@ func configure_callbacks(
|
||||
return self
|
||||
|
||||
|
||||
## A domain-owned, read-only guard runs before queue mutation and again before
|
||||
## a queued event acquires actors or executes actions. It is rebuilt on restore.
|
||||
func configure_candidate_eligibility_guard(guard: Callable = Callable()) -> void:
|
||||
_candidate_eligibility_guard = guard
|
||||
|
||||
|
||||
func _candidate_permitted(definition: EventDefinition, context: Dictionary) -> bool:
|
||||
if not _candidate_eligibility_guard.is_valid():
|
||||
return true
|
||||
var result: Variant = _candidate_eligibility_guard.call(definition, context.duplicate(true))
|
||||
return typeof(result) == TYPE_BOOL and result
|
||||
|
||||
|
||||
func configure_relationship_service(service: Variant) -> EventService:
|
||||
_relationship_service = service
|
||||
return self
|
||||
@@ -222,6 +239,8 @@ func enqueue_candidate(
|
||||
var context := _dictionary(_value(options, &"context", {})).duplicate(true)
|
||||
context[&"absolute_minute"] = absolute_minute
|
||||
context[&"actor_ids"] = normalized_actor_ids.duplicate()
|
||||
if not _candidate_permitted(source, context) or not _candidate_permitted(definition, context):
|
||||
return _invalid("Event candidate is prevented by placement protection.", {&"reason": &"PLACEMENT_PROTECTION"})
|
||||
var instance := EventInstanceType.new().configure(
|
||||
instance_id,
|
||||
event_id,
|
||||
@@ -256,6 +275,8 @@ func start_next_event(absolute_minute: int, extra_context: Dictionary = {}) -> C
|
||||
return _invalid("Event processing requires a non-negative minute.")
|
||||
process_expired(absolute_minute)
|
||||
audit_actor_availability(absolute_minute)
|
||||
if pause_requested:
|
||||
return _invalid("Event processing stopped after a fatal failure.", {&"fatal": true})
|
||||
if not _foreground_instance_id.is_empty():
|
||||
return _invalid("A foreground Event is already active.", {
|
||||
&"event_instance_id": _foreground_instance_id,
|
||||
@@ -277,6 +298,10 @@ func start_next_event(absolute_minute: int, extra_context: Dictionary = {}) -> C
|
||||
var context := _merged_context(instance, absolute_minute, extra_context)
|
||||
if not definition.conditions_match(context, _condition_callbacks):
|
||||
continue
|
||||
var original_definition := get_definition(instance.source_event_id)
|
||||
if original_definition == null or not _candidate_permitted(original_definition, context) or not _candidate_permitted(definition, context):
|
||||
_cancel_instance(instance, &"PLACEMENT_PROTECTION", EventInstanceType.State.CANCELLED)
|
||||
continue
|
||||
if instance.foreground and not _acquire_actor_locks(instance):
|
||||
continue
|
||||
_queue.erase(instance_id)
|
||||
@@ -295,21 +320,27 @@ func start_next_event(absolute_minute: int, extra_context: Dictionary = {}) -> C
|
||||
if bool(automatic_result.get(&"fatal", false)):
|
||||
instance.state = EventInstanceType.State.ERROR
|
||||
pause_requested = true
|
||||
_finish_instance(instance, false)
|
||||
var finish := _finish_instance(instance, false)
|
||||
if not bool(finish.get(&"ok", false)):
|
||||
automatic_result[&"cost_release"] = finish
|
||||
else:
|
||||
_cancel_instance(instance, &"ACTION_FAILED", EventInstanceType.State.CANCELLED)
|
||||
var cancelled := _cancel_instance(instance, &"ACTION_FAILED", EventInstanceType.State.CANCELLED)
|
||||
if not bool(cancelled.get(&"ok", false)):
|
||||
automatic_result[&"fatal"] = true
|
||||
automatic_result[&"cost_release"] = cancelled
|
||||
return _invalid(instance.failure_reason, automatic_result)
|
||||
if not definition.choices.is_empty():
|
||||
instance.state = EventInstanceType.State.WAITING_CHOICE
|
||||
instance.choice_instance_id = StringName("choice_instance_%08d" % _next_choice_sequence)
|
||||
_next_choice_sequence += 1
|
||||
_advance_choice_attempt(instance)
|
||||
last_error = ""
|
||||
return _success({
|
||||
&"event_instance_id": instance.event_instance_id,
|
||||
&"choice_instance_id": instance.choice_instance_id,
|
||||
&"waiting_for_choice": true,
|
||||
})
|
||||
_finish_instance(instance, true)
|
||||
var finish := _finish_instance(instance, true)
|
||||
if not bool(finish.get(&"ok", false)):
|
||||
return _invalid(String(finish.get(&"error", "Event cost release failed.")), finish)
|
||||
last_error = ""
|
||||
return _success({
|
||||
&"event_instance_id": instance.event_instance_id,
|
||||
@@ -334,10 +365,12 @@ func process_expired(absolute_minute: int) -> Array[Dictionary]:
|
||||
[],
|
||||
context
|
||||
)
|
||||
_cancel_instance(instance, &"EXPIRED", EventInstanceType.State.EXPIRED)
|
||||
var cancelled := _cancel_instance(instance, &"EXPIRED", EventInstanceType.State.EXPIRED)
|
||||
results.append({
|
||||
&"event_instance_id": instance_id,
|
||||
&"fallback_ok": bool(fallback_result.get(&"ok", false)),
|
||||
&"cost_release": cancelled,
|
||||
&"fatal": bool(cancelled.get(&"fatal", false)),
|
||||
})
|
||||
return results
|
||||
|
||||
@@ -347,6 +380,18 @@ func reserve_choice_cost(
|
||||
choice_id: StringName,
|
||||
absolute_minute: int,
|
||||
extra_context: Dictionary = {}
|
||||
) -> CommandResult:
|
||||
effect_transaction_started.emit()
|
||||
var result := _reserve_choice_cost_inside_transaction(event_instance_id, choice_id, absolute_minute, extra_context)
|
||||
effect_transaction_finished.emit()
|
||||
return result
|
||||
|
||||
|
||||
func _reserve_choice_cost_inside_transaction(
|
||||
event_instance_id: StringName,
|
||||
choice_id: StringName,
|
||||
absolute_minute: int,
|
||||
extra_context: Dictionary = {}
|
||||
) -> CommandResult:
|
||||
var instance := get_instance(event_instance_id)
|
||||
if instance == null:
|
||||
@@ -360,7 +405,10 @@ func reserve_choice_cost(
|
||||
var missing := _missing_actor_ids(instance.actor_ids)
|
||||
if not missing.is_empty():
|
||||
_handle_missing_actors(instance, definition, missing, absolute_minute)
|
||||
return _invalid("Event actor is unavailable.", {&"missing_actor_ids": missing})
|
||||
return _invalid("Event actor is unavailable.", {
|
||||
&"missing_actor_ids": missing, &"fatal": instance.state == EventInstanceType.State.ERROR,
|
||||
&"failure_reason": instance.failure_reason,
|
||||
})
|
||||
var context := _merged_context(instance, absolute_minute, extra_context)
|
||||
if not choice.conditions_match(context, _condition_callbacks):
|
||||
return _invalid("Choice conditions changed and no longer pass.")
|
||||
@@ -384,6 +432,19 @@ func submit_choice(
|
||||
choice_id: StringName,
|
||||
absolute_minute: int,
|
||||
extra_context: Dictionary = {}
|
||||
) -> CommandResult:
|
||||
effect_transaction_started.emit()
|
||||
var result := _submit_choice_inside_transaction(event_instance_id, choice_instance_id, choice_id, absolute_minute, extra_context)
|
||||
effect_transaction_finished.emit()
|
||||
return result
|
||||
|
||||
|
||||
func _submit_choice_inside_transaction(
|
||||
event_instance_id: StringName,
|
||||
choice_instance_id: StringName,
|
||||
choice_id: StringName,
|
||||
absolute_minute: int,
|
||||
extra_context: Dictionary = {}
|
||||
) -> CommandResult:
|
||||
if _processed_choices.has(choice_instance_id):
|
||||
var replay := CommandResultType.from_dictionary(_processed_choices[choice_instance_id])
|
||||
@@ -402,7 +463,10 @@ func submit_choice(
|
||||
var missing := _missing_actor_ids(instance.actor_ids)
|
||||
if not missing.is_empty():
|
||||
_handle_missing_actors(instance, definition, missing, absolute_minute)
|
||||
return _invalid("Event actor is unavailable.", {&"missing_actor_ids": missing})
|
||||
return _invalid("Event actor is unavailable.", {
|
||||
&"missing_actor_ids": missing, &"fatal": instance.state == EventInstanceType.State.ERROR,
|
||||
&"failure_reason": instance.failure_reason,
|
||||
})
|
||||
var choice := definition.get_choice(choice_id)
|
||||
if choice == null:
|
||||
return _not_found("EventChoice", choice_id)
|
||||
@@ -420,30 +484,61 @@ func submit_choice(
|
||||
context
|
||||
)
|
||||
if not bool(transaction_result.get(&"ok", false)):
|
||||
_release_reserved_cost(instance, choice_id, context)
|
||||
var release := _release_reserved_cost(instance, choice_id, context)
|
||||
if not bool(release.get(&"ok", false)):
|
||||
transaction_result[&"fatal"] = true
|
||||
transaction_result[&"cost_release"] = release
|
||||
transaction_result[&"error"] = "Choice action failed and cost release failed: %s" % release.get(&"error", "")
|
||||
if bool(transaction_result.get(&"fatal", false)):
|
||||
var other_release := _release_all_reserved_costs(instance, context, [choice_id])
|
||||
if not bool(other_release.get(&"ok", false)):
|
||||
transaction_result[&"other_cost_release"] = other_release
|
||||
instance.state = EventInstanceType.State.ERROR
|
||||
instance.failure_reason = String(transaction_result.get(&"error", "Failure branch failed."))
|
||||
pause_requested = true
|
||||
_finish_instance(instance, false)
|
||||
_detach_instance(instance)
|
||||
var failed_result := _invalid(
|
||||
String(transaction_result.get(&"error", "Choice Action transaction failed.")),
|
||||
transaction_result
|
||||
)
|
||||
_cache_choice_result(choice_instance_id, failed_result)
|
||||
if instance.state == EventInstanceType.State.WAITING_CHOICE:
|
||||
_advance_choice_attempt(instance)
|
||||
return failed_result
|
||||
var commit_result := _commit_reserved_cost(instance, choice_id, context)
|
||||
if not bool(commit_result.get(&"ok", false)):
|
||||
_rollback_records(transaction_result.get(&"records", []), context)
|
||||
_release_reserved_cost(instance, choice_id, context)
|
||||
var rollback := _rollback_records(transaction_result.get(&"records", []), context)
|
||||
var release := _release_reserved_cost(instance, choice_id, context)
|
||||
if not bool(release.get(&"ok", false)):
|
||||
commit_result[&"fatal"] = true
|
||||
commit_result[&"cost_release"] = release
|
||||
commit_result[&"error"] = "Choice cost commit and release failed: %s" % release.get(&"error", "")
|
||||
if not bool(rollback.get(&"ok", false)):
|
||||
commit_result[&"fatal"] = true
|
||||
commit_result[&"rollback"] = rollback
|
||||
commit_result[&"error"] = "Choice cost failed and action rollback failed: %s" % rollback.get(&"error", "")
|
||||
if bool(commit_result.get(&"fatal", false)):
|
||||
var other_release := _release_all_reserved_costs(instance, context, [choice_id])
|
||||
if not bool(other_release.get(&"ok", false)):
|
||||
commit_result[&"other_cost_release"] = other_release
|
||||
instance.state = EventInstanceType.State.ERROR
|
||||
instance.failure_reason = String(commit_result[&"error"])
|
||||
pause_requested = true
|
||||
_detach_instance(instance)
|
||||
var commit_failure := _invalid(
|
||||
String(commit_result.get(&"error", "Choice cost commit failed.")),
|
||||
commit_result
|
||||
)
|
||||
_cache_choice_result(choice_instance_id, commit_failure)
|
||||
if instance.state == EventInstanceType.State.WAITING_CHOICE:
|
||||
_advance_choice_attempt(instance)
|
||||
return commit_failure
|
||||
instance.selected_choice_id = choice_id
|
||||
_finish_instance(instance, true)
|
||||
var finish := _finish_instance(instance, true)
|
||||
if not bool(finish.get(&"ok", false)):
|
||||
var finish_failure := _invalid(String(finish.get(&"error", "Choice final cost release failed.")), finish)
|
||||
_cache_choice_result(choice_instance_id, finish_failure)
|
||||
return finish_failure
|
||||
var result := _success({
|
||||
&"event_instance_id": event_instance_id,
|
||||
&"choice_instance_id": choice_instance_id,
|
||||
@@ -482,7 +577,9 @@ func cancel_event(event_instance_id: StringName, reason: StringName = &"CANCELLE
|
||||
return _not_found("EventInstance", event_instance_id)
|
||||
if instance.is_terminal():
|
||||
return _invalid("Event is already terminal.")
|
||||
_cancel_instance(instance, reason, EventInstanceType.State.CANCELLED)
|
||||
var cancelled := _cancel_instance(instance, reason, EventInstanceType.State.CANCELLED)
|
||||
if not bool(cancelled.get(&"ok", false)):
|
||||
return _invalid(String(cancelled.get(&"error", "Event cost release failed.")), cancelled)
|
||||
last_error = ""
|
||||
return _success({&"event_instance_id": event_instance_id})
|
||||
|
||||
@@ -765,6 +862,18 @@ func _execute_actions(
|
||||
policy: EventDefinition.TransactionPolicy,
|
||||
failure_actions: Array,
|
||||
context: Dictionary
|
||||
) -> Dictionary:
|
||||
effect_transaction_started.emit()
|
||||
var result := _execute_actions_inside_transaction(actions, policy, failure_actions, context)
|
||||
effect_transaction_finished.emit()
|
||||
return result
|
||||
|
||||
|
||||
func _execute_actions_inside_transaction(
|
||||
actions: Array,
|
||||
policy: EventDefinition.TransactionPolicy,
|
||||
failure_actions: Array,
|
||||
context: Dictionary
|
||||
) -> Dictionary:
|
||||
match policy:
|
||||
EventDefinitionType.TransactionPolicy.SKIP_FAILED:
|
||||
@@ -779,11 +888,13 @@ func _execute_actions(
|
||||
if bool(applied.get(&"ok", false)):
|
||||
records.append({&"action": action, &"token": _copy(applied.get(&"token", null))})
|
||||
else:
|
||||
if bool(applied.get(&"fatal", false)):
|
||||
return applied
|
||||
skipped.append(action.action_id)
|
||||
return {&"ok": true, &"records": records, &"skipped_action_ids": skipped}
|
||||
EventDefinitionType.TransactionPolicy.BRANCH_FAILURE:
|
||||
var primary := _run_rollback_all(actions, context)
|
||||
if bool(primary.get(&"ok", false)):
|
||||
if bool(primary.get(&"ok", false)) or bool(primary.get(&"fatal", false)):
|
||||
return primary
|
||||
var branch := _run_rollback_all(failure_actions, context)
|
||||
if bool(branch.get(&"ok", false)):
|
||||
@@ -807,18 +918,31 @@ func _run_rollback_all(actions: Array, context: Dictionary) -> Dictionary:
|
||||
for action: EventAction in actions:
|
||||
var applied := _execute_action(action, &"apply", context, null)
|
||||
if not bool(applied.get(&"ok", false)):
|
||||
_rollback_records(records, context)
|
||||
return {&"ok": false, &"error": applied.get(&"error", "Action apply failed."), &"records": []}
|
||||
var rollback := _rollback_records(records, context)
|
||||
var failed := {&"ok": false, &"error": applied.get(&"error", "Action apply failed."), &"records": []}
|
||||
if bool(applied.get(&"fatal", false)) or not bool(rollback.get(&"ok", false)):
|
||||
failed[&"fatal"] = true
|
||||
failed[&"rollback"] = rollback
|
||||
failed[&"error"] = "Action transaction rollback failed: %s; %s" % [failed[&"error"], rollback.get(&"error", "")]
|
||||
return failed
|
||||
records.append({&"action": action, &"token": _copy(applied.get(&"token", null))})
|
||||
return {&"ok": true, &"records": records, &"skipped_action_ids": []}
|
||||
|
||||
|
||||
func _rollback_records(raw_records: Variant, context: Dictionary) -> void:
|
||||
func _rollback_records(raw_records: Variant, context: Dictionary) -> Dictionary:
|
||||
if not raw_records is Array:
|
||||
return
|
||||
return {&"ok": false, &"error": "Rollback records are malformed."}
|
||||
var failed_actions: Array[StringName] = []
|
||||
var errors: Array[String] = []
|
||||
# Ask every participant even after a failure; retain reverse apply order.
|
||||
for index: int in range(raw_records.size() - 1, -1, -1):
|
||||
var record: Dictionary = raw_records[index]
|
||||
_execute_action(record[&"action"], &"rollback", context, record.get(&"token"))
|
||||
var action: EventAction = record[&"action"]
|
||||
var result := _execute_action(action, &"rollback", context, record.get(&"token"))
|
||||
if not bool(result.get(&"ok", false)):
|
||||
failed_actions.append(action.action_id)
|
||||
errors.append(String(result.get(&"error", "Rollback participant returned failure.")))
|
||||
return {&"ok": failed_actions.is_empty(), &"failed_action_ids": failed_actions, &"error": "; ".join(errors)}
|
||||
|
||||
|
||||
func _execute_action(
|
||||
@@ -876,7 +1000,7 @@ func _execute_builtin_action(
|
||||
var instance_id := StringName(rollback_token)
|
||||
var scheduled := get_instance(instance_id)
|
||||
if scheduled != null and not scheduled.is_terminal():
|
||||
_cancel_instance(scheduled, &"TRANSACTION_ROLLBACK", EventInstanceType.State.CANCELLED)
|
||||
return _cancel_instance(scheduled, &"TRANSACTION_ROLLBACK", EventInstanceType.State.CANCELLED)
|
||||
return {&"ok": true}
|
||||
EventActionType.Type.ADD_RELATIONSHIP_VALUE, EventActionType.Type.DAMAGE_SHIELD, EventActionType.Type.SET_ROUTE_LOCK:
|
||||
return _execute_relationship_action(action, phase, context, rollback_token)
|
||||
@@ -935,7 +1059,10 @@ func _handle_missing_actors(
|
||||
missing_actor_ids: Array[StringName],
|
||||
absolute_minute: int
|
||||
) -> void:
|
||||
_release_all_reserved_costs(instance, _merged_context(instance, absolute_minute, {}))
|
||||
var release := _release_all_reserved_costs(instance, _merged_context(instance, absolute_minute, {}))
|
||||
if not bool(release.get(&"ok", false)):
|
||||
_mark_release_failure(instance, release)
|
||||
return
|
||||
if definition == null:
|
||||
_cancel_instance(instance, &"DEFINITION_MISSING", EventInstanceType.State.ERROR)
|
||||
return
|
||||
@@ -995,12 +1122,26 @@ func _release_actor_locks(instance: EventInstance) -> void:
|
||||
_foreground_instance_id = &""
|
||||
|
||||
|
||||
func _finish_instance(instance: EventInstance, completed: bool) -> void:
|
||||
_release_all_reserved_costs(instance, instance.context)
|
||||
func _detach_instance(instance: EventInstance) -> void:
|
||||
_release_actor_locks(instance)
|
||||
_queue.erase(instance.event_instance_id)
|
||||
if not instance.deduplication_key.is_empty():
|
||||
_active_deduplication_keys.erase(instance.deduplication_key)
|
||||
|
||||
|
||||
func _mark_release_failure(instance: EventInstance, result: Dictionary) -> void:
|
||||
instance.state = EventInstanceType.State.ERROR
|
||||
instance.failure_reason = String(result.get(&"error", "Event cost release failed."))
|
||||
pause_requested = true
|
||||
_detach_instance(instance)
|
||||
|
||||
|
||||
func _finish_instance(instance: EventInstance, completed: bool) -> Dictionary:
|
||||
var release := _release_all_reserved_costs(instance, instance.context)
|
||||
if not bool(release.get(&"ok", false)):
|
||||
_mark_release_failure(instance, release)
|
||||
return release
|
||||
_detach_instance(instance)
|
||||
if completed:
|
||||
instance.state = EventInstanceType.State.COMPLETED
|
||||
var source := get_definition(instance.source_event_id)
|
||||
@@ -1009,17 +1150,20 @@ func _finish_instance(instance: EventInstance, completed: bool) -> void:
|
||||
if not scope_key.is_empty():
|
||||
_completed_scope_keys[scope_key] = instance.event_instance_id
|
||||
event_finished.emit(instance)
|
||||
return release
|
||||
|
||||
|
||||
func _cancel_instance(instance: EventInstance, reason: StringName, terminal_state: EventInstance.State) -> void:
|
||||
_release_all_reserved_costs(instance, instance.context)
|
||||
_release_actor_locks(instance)
|
||||
_queue.erase(instance.event_instance_id)
|
||||
if not instance.deduplication_key.is_empty():
|
||||
_active_deduplication_keys.erase(instance.deduplication_key)
|
||||
func _cancel_instance(instance: EventInstance, reason: StringName, terminal_state: EventInstance.State) -> Dictionary:
|
||||
var release := _release_all_reserved_costs(instance, instance.context)
|
||||
if not bool(release.get(&"ok", false)):
|
||||
_mark_release_failure(instance, release)
|
||||
event_cancelled.emit(instance, &"COST_RELEASE_FAILED")
|
||||
return release
|
||||
_detach_instance(instance)
|
||||
instance.state = terminal_state
|
||||
instance.failure_reason = String(reason)
|
||||
event_cancelled.emit(instance, reason)
|
||||
return release
|
||||
|
||||
|
||||
func _reserve_cost(cost: Dictionary, context: Dictionary) -> Dictionary:
|
||||
@@ -1037,14 +1181,30 @@ func _release_reserved_cost(instance: EventInstance, choice_id: StringName, cont
|
||||
var result := {&"ok": true}
|
||||
if _cost_releaser.is_valid():
|
||||
result = _normalize_operation_result(_cost_releaser.call(_copy(reservation.get(&"token")), context))
|
||||
instance.reserved_costs.erase(choice_id)
|
||||
elif not _dictionary(reservation.get(&"cost", {})).is_empty():
|
||||
result = {&"ok": false, &"error": "Choice Cost releaser is not configured."}
|
||||
if bool(result.get(&"ok", false)):
|
||||
instance.reserved_costs.erase(choice_id)
|
||||
else:
|
||||
result[&"fatal"] = true
|
||||
result[&"failed_choice_id"] = choice_id
|
||||
return result
|
||||
|
||||
|
||||
func _release_all_reserved_costs(instance: EventInstance, context: Dictionary) -> void:
|
||||
var choice_ids := _sorted_name_keys(instance.reserved_costs)
|
||||
for choice_id: StringName in choice_ids:
|
||||
_release_reserved_cost(instance, choice_id, context)
|
||||
func _release_all_reserved_costs(instance: EventInstance, context: Dictionary, excluded_choice_ids: Array[StringName] = []) -> Dictionary:
|
||||
var failed_ids: Array[StringName] = []
|
||||
var failures: Array[Dictionary] = []
|
||||
for choice_id: StringName in _sorted_name_keys(instance.reserved_costs):
|
||||
if choice_id in excluded_choice_ids:
|
||||
continue
|
||||
var released := _release_reserved_cost(instance, choice_id, context)
|
||||
if not bool(released.get(&"ok", false)):
|
||||
failed_ids.append(choice_id)
|
||||
failures.append(released)
|
||||
if failed_ids.is_empty():
|
||||
return {&"ok": true}
|
||||
return {&"ok": false, &"fatal": true, &"failed_choice_ids": failed_ids,
|
||||
&"failures": failures, &"error": "Event cost release failed for choices: %s" % [failed_ids]}
|
||||
|
||||
|
||||
func _commit_reserved_cost(instance: EventInstance, choice_id: StringName, context: Dictionary) -> Dictionary:
|
||||
@@ -1256,3 +1416,8 @@ func _invalid(message: String, details: Dictionary = {}) -> CommandResult:
|
||||
|
||||
func _not_found(kind: String, entity_id: StringName) -> CommandResult:
|
||||
return _invalid("%s not found: %s" % [kind, entity_id], {&"entity_id": entity_id})
|
||||
|
||||
|
||||
func _advance_choice_attempt(instance: EventInstance) -> void:
|
||||
instance.choice_instance_id = StringName("choice_instance_%08d" % _next_choice_sequence)
|
||||
_next_choice_sequence += 1
|
||||
|
||||
@@ -82,7 +82,7 @@ func configure(
|
||||
customer_capacity = int(_value(options, &"customer_capacity", -1))
|
||||
service_attribute = StringName(_value(options, &"service_attribute", &""))
|
||||
related_facility_id = StringName(_value(options, &"related_facility_id", &""))
|
||||
related_facility_operating = related_facility_id.is_empty()
|
||||
related_facility_operating = related_facility_id.is_empty() and not requires_operating_kitchen()
|
||||
suspension_reason = StringName(_value(options, &"suspension_reason", &""))
|
||||
queue_capacity = maxi(int(_value(options, &"queue_capacity", 0)), 0)
|
||||
base_service_minutes = maxi(int(_value(options, &"base_service_minutes", 1)), 1)
|
||||
@@ -107,6 +107,12 @@ func configure(
|
||||
return self
|
||||
|
||||
|
||||
# Original 8.2 requires each dining venue to use a BACK kitchen. Geometry and
|
||||
# shared logistics are separate contracts; this single-ID rule does not infer either.
|
||||
func requires_operating_kitchen() -> bool:
|
||||
return definition_id in [&"cafeteria", &"restaurant", &"tavern"]
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return not instance_id.is_empty() \
|
||||
and not definition_id.is_empty() \
|
||||
@@ -159,7 +165,7 @@ func is_within_opening_hours(absolute_minute: int) -> bool:
|
||||
func set_operational_conditions(conditions: Dictionary, absolute_minute: int) -> bool:
|
||||
if _has_value(conditions, &"related_facility_id"):
|
||||
related_facility_id = StringName(_value(conditions, &"related_facility_id", &""))
|
||||
related_facility_operating = related_facility_id.is_empty()
|
||||
related_facility_operating = related_facility_id.is_empty() and not requires_operating_kitchen()
|
||||
if StringName(_value(conditions, &"suspension_reason", &"")) in [&"DESTROYED", &"RAIDED"]:
|
||||
suspension_reason = StringName(_value(conditions, &"suspension_reason", &""))
|
||||
if _has_value(conditions, &"durability"):
|
||||
|
||||
@@ -145,6 +145,12 @@ func set_operational_status(
|
||||
var related := StringName(raw_related)
|
||||
if related == instance_id or (not related.is_empty() and not has_instance(related)):
|
||||
return _invalid("Related facility is missing or refers to itself.")
|
||||
if instance.requires_operating_kitchen() and not related.is_empty():
|
||||
var kitchen := get_instance(related)
|
||||
if kitchen.definition_id != &"kitchen" or kitchen.origin.z != WorldService.LAYER_BACK:
|
||||
return _invalid("Dining facilities require one existing BACK kitchen.", {&"reason": &"INVALID_RELATED_KITCHEN"})
|
||||
if not _required_related_placement_matches(instance, kitchen):
|
||||
return _invalid("The BACK kitchen must lie inside this FRONT dining facility's XY footprint.", {&"reason": &"INVALID_RELATED_KITCHEN_PLACEMENT"})
|
||||
var seen := {instance_id: true}
|
||||
var cursor := related
|
||||
while not cursor.is_empty() and has_instance(cursor):
|
||||
@@ -253,7 +259,7 @@ func create_request(
|
||||
)
|
||||
|
||||
|
||||
func enqueue_request(request: Variant, absolute_minute: int = -1) -> CommandResult:
|
||||
func enqueue_request(request: Variant, absolute_minute: int = -1, commit_clock: bool = true) -> CommandResult:
|
||||
if request == null or not request is ServiceRequest:
|
||||
return _invalid("Service request is missing or has an invalid type.")
|
||||
var queued_request: ServiceRequest = request
|
||||
@@ -269,7 +275,11 @@ func enqueue_request(request: Variant, absolute_minute: int = -1) -> CommandResu
|
||||
"Facility queue is full.",
|
||||
{&"instance_id": instance.instance_id, &"queue_capacity": instance.queue_capacity}
|
||||
)
|
||||
var minute := _resolve_minute(absolute_minute)
|
||||
# Queue registration can precede the Clock increment in APPLY_COMMANDS.
|
||||
# Preserve the request effective time without claiming participant advancement.
|
||||
if not commit_clock and absolute_minute != current_minute + 1:
|
||||
return _invalid("Deferred queue registration must target the next uncommitted minute.")
|
||||
var minute := _resolve_minute(absolute_minute) if commit_clock else absolute_minute
|
||||
queued_request.enqueued_at = minute
|
||||
_requests[queued_request.request_id] = queued_request
|
||||
_known_request_ids[queued_request.request_id] = true
|
||||
@@ -342,6 +352,76 @@ func cancel_request(request_id: StringName, reason: StringName = &"GUEST_LEFT")
|
||||
return _not_found("Queued service request", request_id)
|
||||
|
||||
|
||||
func _required_related_kind_matches(instance: FacilityInstance) -> bool:
|
||||
if not instance.requires_operating_kitchen():
|
||||
return true
|
||||
var kitchen := get_instance(instance.related_facility_id)
|
||||
return kitchen != null and kitchen.definition_id == &"kitchen" and kitchen.origin.z == WorldService.LAYER_BACK
|
||||
|
||||
|
||||
# Original 2.2 and 8.3 put a BACK kitchen behind the FRONT dining venue.
|
||||
# XY containment is Root's operational interpretation, not a width/origin-x
|
||||
# equality rule. Use actual occupied cells; do not synthesize missing geometry,
|
||||
# rewrite old IDs, or persist this derived predicate.
|
||||
func _required_related_placement_matches(instance: FacilityInstance, proposed_kitchen: FacilityInstance = null) -> bool:
|
||||
if not instance.requires_operating_kitchen():
|
||||
return true
|
||||
var kitchen: FacilityInstance = proposed_kitchen if proposed_kitchen != null else get_instance(instance.related_facility_id)
|
||||
if kitchen == null or kitchen.definition_id != &"kitchen" \
|
||||
or instance.origin.z != WorldService.LAYER_FRONT or kitchen.origin.z != WorldService.LAYER_BACK \
|
||||
or instance.occupied_cells.is_empty() or kitchen.occupied_cells.is_empty():
|
||||
return false
|
||||
var front_xy: Dictionary = {}
|
||||
for cell: Vector3i in instance.occupied_cells:
|
||||
if cell.z != WorldService.LAYER_FRONT:
|
||||
return false
|
||||
front_xy[Vector2i(cell.x, cell.y)] = true
|
||||
for cell: Vector3i in kitchen.occupied_cells:
|
||||
if cell.z != WorldService.LAYER_BACK or not front_xy.has(Vector2i(cell.x, cell.y)):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# Read-only derived eligibility. Legacy cached state/IDs remain untouched until
|
||||
# actual gameplay evaluates a facility or advances one minute.
|
||||
func get_required_kitchen_report(instance_id: StringName) -> Dictionary:
|
||||
var instance := get_instance(instance_id)
|
||||
if instance == null:
|
||||
return {}
|
||||
var required := instance.requires_operating_kitchen()
|
||||
var related := instance.related_facility_id
|
||||
var kitchen := get_instance(related)
|
||||
var valid_kind := _required_related_kind_matches(instance)
|
||||
var valid_placement := _required_related_placement_matches(instance)
|
||||
var operating := not required or (valid_placement and _restored_dependency_operates(related, {}))
|
||||
var reason: StringName = &""
|
||||
if required:
|
||||
if related.is_empty(): reason = &"KITCHEN_NOT_LINKED"
|
||||
elif kitchen == null: reason = &"KITCHEN_REMOVED"
|
||||
elif not valid_kind: reason = &"INVALID_RELATED_KITCHEN"
|
||||
elif not valid_placement: reason = &"INVALID_RELATED_KITCHEN_PLACEMENT"
|
||||
elif not kitchen.suspension_reason.is_empty(): reason = &"KITCHEN_SUSPENDED"
|
||||
elif not operating: reason = &"KITCHEN_INACTIVE"
|
||||
return {&"required": required, &"related_facility_id": related, &"exists": kitchen != null, &"valid_kind": valid_kind, &"operating": operating, &"reason": reason}
|
||||
|
||||
|
||||
# Read-only eligibility of a proposed link. Does not select a kitchen, alter an
|
||||
# existing related ID, evaluate state, advance time, or clear persistent suspension.
|
||||
func get_kitchen_placement_report(instance_id: StringName, kitchen_id: StringName) -> Dictionary:
|
||||
var instance := get_instance(instance_id)
|
||||
var kitchen := get_instance(kitchen_id)
|
||||
var required := instance != null and instance.requires_operating_kitchen()
|
||||
var valid_kind := kitchen != null and kitchen.definition_id == &"kitchen" and kitchen.origin.z == WorldService.LAYER_BACK
|
||||
var valid_placement := required and valid_kind and _required_related_placement_matches(instance, kitchen)
|
||||
var reason: StringName = &""
|
||||
if instance == null: reason = &"FACILITY_NOT_FOUND"
|
||||
elif not required: reason = &"KITCHEN_NOT_REQUIRED"
|
||||
elif kitchen == null: reason = &"KITCHEN_REMOVED"
|
||||
elif not valid_kind: reason = &"INVALID_RELATED_KITCHEN"
|
||||
elif not valid_placement: reason = &"INVALID_RELATED_KITCHEN_PLACEMENT"
|
||||
return {&"facility_id": instance_id, &"kitchen_id": kitchen_id, &"required": required, &"exists": kitchen != null, &"valid_kind": valid_kind, &"valid_placement": valid_placement, &"reason": reason}
|
||||
|
||||
|
||||
func refresh_operating_dependencies(minute: int) -> void:
|
||||
var resolved: Dictionary = {}
|
||||
for instance_id: StringName in _sorted_keys(_instances):
|
||||
@@ -357,7 +437,7 @@ func _resolve_operating_dependency(id: StringName, minute: int, resolved: Dictio
|
||||
var instance := get_instance(id)
|
||||
instance.apply_pending_policy(minute)
|
||||
var related := instance.related_facility_id
|
||||
instance.related_facility_operating = related.is_empty() or _resolve_operating_dependency(related, minute, resolved, visiting)
|
||||
instance.related_facility_operating = _required_related_placement_matches(instance) and (related.is_empty() or _resolve_operating_dependency(related, minute, resolved, visiting))
|
||||
var old_state := instance.state
|
||||
instance.evaluate_state(minute)
|
||||
_emit_state_change(instance, old_state)
|
||||
@@ -686,7 +766,7 @@ func restore_snapshot(snapshot: Dictionary) -> bool:
|
||||
_next_service_sequence = restored_next_sequence
|
||||
current_minute = restored_minute
|
||||
for instance: FacilityInstance in _instances.values():
|
||||
instance.related_facility_operating = _restored_dependency_operates(instance.related_facility_id, {})
|
||||
instance.related_facility_operating = _required_related_placement_matches(instance) and _restored_dependency_operates(instance.related_facility_id, {})
|
||||
instance.operation_state = FacilityInstanceType.OperationState.SUSPENDED if not instance.suspension_reason.is_empty() else (FacilityInstanceType.OperationState.OPERATING if instance.state in [FacilityInstanceType.State.OPEN, FacilityInstanceType.State.DEGRADED] and instance.related_facility_operating else FacilityInstanceType.OperationState.INACTIVE)
|
||||
last_error = ""
|
||||
return true
|
||||
@@ -699,7 +779,7 @@ func _restored_dependency_operates(id: StringName, seen: Dictionary) -> bool:
|
||||
return false
|
||||
seen[id] = true
|
||||
var instance := get_instance(id)
|
||||
return instance.suspension_reason.is_empty() and instance.state in [FacilityInstanceType.State.OPEN, FacilityInstanceType.State.DEGRADED] and _restored_dependency_operates(instance.related_facility_id, seen)
|
||||
return _required_related_placement_matches(instance) and instance.suspension_reason.is_empty() and instance.state in [FacilityInstanceType.State.OPEN, FacilityInstanceType.State.DEGRADED] and _restored_dependency_operates(instance.related_facility_id, seen)
|
||||
|
||||
|
||||
func _saved_policy_is_valid(policy: Dictionary) -> bool:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user