diff --git a/autoload/simulation_runtime.gd b/autoload/simulation_runtime.gd index 6c5621f..64e1d61 100644 --- a/autoload/simulation_runtime.gd +++ b/autoload/simulation_runtime.gd @@ -20,6 +20,7 @@ const OPERATING_EXPENSE_ACCOUNT := &"OPERATING_EXPENSE" const AUTHORITY_FINE_ACCOUNT := &"AUTHORITY_FINE_EXPENSE" const WAGE_EXPENSE_ACCOUNT := &"WAGE_EXPENSE" const RUNTIME_SNAPSHOT_VERSION := 14 +const PeepingRooms := preload("res://scripts/special/peeping_room_service.gd") const AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION := 14 const EVENT_THREAT_RUNTIME_SNAPSHOT_VERSION := 13 const MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION := 15 @@ -97,6 +98,7 @@ const DEFENSE_COMMAND_TYPES: Array[StringName] = [ var definition_registry := DefinitionRegistry.new() var command_queue := CommandQueue.new() var id_generator := StableIdGenerator.new() +var peeping_room_service := PeepingRooms.new() var domain_events := DomainEventStream.new() var world_service := WorldService.new() var path_service := PathService.new() @@ -233,6 +235,7 @@ func initialize(clock: SimulationKernel, state: Variant, seed := DEFAULT_WORLD_S _hired_staff_candidate_ids.clear() command_queue = CommandQueue.new() id_generator = StableIdGenerator.new() + peeping_room_service = PeepingRooms.new() domain_events = DomainEventStream.new() world_service = WorldService.new() path_service = PathService.new() @@ -253,6 +256,7 @@ func initialize(clock: SimulationKernel, state: Variant, seed := DEFAULT_WORLD_S _initialize_s5_defense() construction_service = ConstructionService.new(world_service, construction_resource_state) construction_service.current_minute = _clock.absolute_minute + peeping_room_service.configure(world_service, facility_service, construction_service, path_service, id_generator) _connect_construction_signals() var initial_infrastructure_ok := _initialize_infrastructure_services() _next_command_id = 1 @@ -298,6 +302,8 @@ func initialize(clock: SimulationKernel, state: Variant, seed := DEFAULT_WORLD_S 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")) + for command_type: StringName in [&"peeping_room_validate", &"peeping_room_place", &"peeping_room_relink", &"peeping_room_record_evidence"]: + register_command_executor(command_type, Callable(self, "_execute_peeping_room")) register_command_executor(&"set_elevator_operation", Callable(self, "_execute_set_elevator_operation")) register_command_executor(&"start_excavation", Callable(self, "_execute_start_excavation")) register_command_executor(&"pause_excavation", Callable(self, "_execute_pause_excavation")) @@ -565,6 +571,8 @@ func create_snapshot() -> Dictionary: } if PurchaseQuoteV2.has_history(owned_systems[&"runtime"]): owned_systems[&"runtime"][&"runtime_schema_version"] = MIMI_PURCHASE_RUNTIME_SNAPSHOT_VERSION + if not peeping_room_service.is_empty(): + owned_systems[&"runtime"][&"special_peeping_rooms"] = peeping_room_service.create_snapshot() # 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. @@ -666,6 +674,8 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo runtime_current_keys.append(&"facility_cleanliness") if runtime_schema_version >= AUTO_PURCHASE_RUNTIME_SNAPSHOT_VERSION: runtime_current_keys.append(&"auto_purchase_plans") + if runtime_data.has(&"special_peeping_rooms"): + runtime_current_keys.append(&"special_peeping_rooms") if not _dictionary_has_exact_canonical_keys(runtime_data, runtime_current_keys) \ or typeof(runtime_data.get(&"runtime_schema_version", null)) != TYPE_INT \ or typeof(runtime_data.get(&"world_seed", null)) != TYPE_INT \ @@ -1290,6 +1300,16 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo var restored_next_id := int(runtime_data.get(&"next_command_id", 0)) if restored_next_id <= 0: return false + var restored_peeping_rooms := PeepingRooms.new() + restored_peeping_rooms.configure(restored_world, restored_facilities, restored_construction, restored_path, restored_id_generator) + if runtime_data.has(&"special_peeping_rooms"): + if not runtime_data[&"special_peeping_rooms"] is Dictionary: + return false + for raw_key: Variant in runtime_data: + if raw_key == &"special_peeping_rooms" and typeof(raw_key) != TYPE_STRING_NAME: + return false + if not restored_peeping_rooms.restore_section(runtime_data.get(&"special_peeping_rooms", null), restored_minute): + return false if not SimulationSnapshot.restore(snapshot, _clock, _state): return false world_seed = restored_world_seed @@ -1297,6 +1317,7 @@ func _restore_snapshot_internal(snapshot: Dictionary, recovery_mode: bool) -> bo _staffing_projection_enabled = restored_staffing_projection command_queue = restored_queue id_generator = restored_id_generator + peeping_room_service = restored_peeping_rooms domain_events = restored_domain_events _rng_streams = restored_streams world_service = restored_world @@ -5546,6 +5567,7 @@ func _connect_facility_signals() -> void: return facility_service.instance_registered.connect(Callable(self, "_on_cleanliness_facility_registered")) facility_service.instance_unregistered.connect(Callable(self, "_on_cleanliness_facility_unregistered")) + facility_service.instance_unregistered.connect(Callable(self, "_on_peeping_target_removed")) facility_service.facility_state_changed.connect(Callable(self, "_on_facility_state_changed")) facility_service.service_started.connect(Callable(self, "_on_service_started")) facility_service.service_interrupted.connect(Callable(self, "_on_service_interrupted")) @@ -10817,6 +10839,7 @@ func _on_excavation_completed(report: Dictionary) -> void: )) func _on_facility_demolished(instance_id: StringName, refund: Dictionary) -> void: + peeping_room_service.target_removed(instance_id) if not mana_network_service.get_node(instance_id).is_empty(): mana_network_service.unregister_node(instance_id) if facility_service.has_instance(instance_id): @@ -10831,6 +10854,31 @@ func _on_facility_demolished(instance_id: StringName, refund: Dictionary) -> voi 0 )) +func _on_peeping_target_removed(instance_id: StringName) -> void: + peeping_room_service.target_removed(instance_id) + +func _execute_peeping_room(command: SimulationCommand) -> CommandResult: + var payload := command.payload + var report: Dictionary + if command.command_type == &"peeping_room_place": + report = PeepingRooms.result(&"BUILD_DEFINITION_PENDING_D04") + elif command.command_type == &"peeping_room_validate": + if typeof(payload.get(&"origin")) != TYPE_VECTOR3I or typeof(payload.get(&"footprint", Vector2i(2, 1))) != TYPE_VECTOR2I or typeof(payload.get(&"target_instance_id", &"")) not in [TYPE_STRING, TYPE_STRING_NAME]: + report = PeepingRooms.result(&"INVALID_PAYLOAD") + else: + report = peeping_room_service.validate_placement(payload[&"origin"], payload.get(&"footprint", Vector2i(2, 1)), StringName(payload.get(&"target_instance_id", &""))) + else: + var key: StringName = &"target_instance_id" if command.command_type == &"peeping_room_relink" else &"source_event_key" + if typeof(payload.get(&"room_id")) not in [TYPE_STRING, TYPE_STRING_NAME] or typeof(payload.get(key, &"")) not in [TYPE_STRING, TYPE_STRING_NAME]: + report = PeepingRooms.result(&"INVALID_PAYLOAD") + elif command.command_type == &"peeping_room_relink": + report = peeping_room_service.relink(StringName(payload[&"room_id"]), StringName(payload.get(key, &""))) + else: + report = peeping_room_service.record_evidence(StringName(payload[&"room_id"]), StringName(payload.get(key, &"")), _runtime_event_minute()) + if report[&"ok"]: + return CommandResult.succeeded(command.command_id, report) + return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, String(report[&"reason"]), report) + func _on_ledger_transaction_posted(transaction_id: StringName, _entries: Array) -> void: if _operations_flushing_economy_notifications.has(transaction_id): return diff --git a/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json b/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json index 2eecb80..fce033f 100644 --- a/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json +++ b/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json @@ -3734,6 +3734,61 @@ "path": "docs/spec/acceptance/P23-03.json", "scope": "component and read-only observation only; native unexecuted; whole unit blocked" } + }, + { + "test": "tests/special/test_peeping_room_geometry.gd", + "template": "Peeping room geometry: checks, failures", + "required": true, + "fixed_check_count_used": false, + "source_sha256_at_schema_audit": "103d85f83e1e97a8637892504264275eef23f172eeec56c94e5c022d81bf6945", + "authority": { + "path": "docs/plans/original-completion-2026-10-02/execution/p13-02/CODE1_TASK_SPEC.json", + "scope": "new backend suite marker; native execution pending" + } + }, + { + "test": "tests/special/test_peeping_room_reference.gd", + "template": "Peeping room reference: checks, failures", + "required": true, + "fixed_check_count_used": false, + "source_sha256_at_schema_audit": "188b00a60f7aefda69dc4973f1de9c14447a0d4bcdd1257a2eabe7c4782e4caf", + "authority": { + "path": "docs/plans/original-completion-2026-10-02/execution/p13-02/CODE1_TASK_SPEC.json", + "scope": "new backend suite marker; native execution pending" + } + }, + { + "test": "tests/special/test_peeping_room_evidence.gd", + "template": "Peeping room evidence: checks, failures", + "required": true, + "fixed_check_count_used": false, + "source_sha256_at_schema_audit": "390e0032d14de9a8875ced163a922341ce655744200d78b4ffa776d42c4f8c2a", + "authority": { + "path": "docs/plans/original-completion-2026-10-02/execution/p13-02/CODE1_TASK_SPEC.json", + "scope": "new backend suite marker; native execution pending" + } + }, + { + "test": "tests/special/test_peeping_room_save.gd", + "template": "Peeping room save: checks, failures", + "required": true, + "fixed_check_count_used": false, + "source_sha256_at_schema_audit": "ba0e7644277477c1821135cae6d6fd533651799c6f9bd8c43ccf45e63b96c63c", + "authority": { + "path": "docs/plans/original-completion-2026-10-02/execution/p13-02/CODE1_TASK_SPEC.json", + "scope": "new backend suite marker; native execution pending" + } + }, + { + "test": "tests/special/test_peeping_room_runtime.gd", + "template": "Peeping room runtime: checks, failures", + "required": true, + "fixed_check_count_used": false, + "source_sha256_at_schema_audit": "924efd34414c12a325a1b700f799aef0396de9f15441fd88a3608292ff221657", + "authority": { + "path": "docs/plans/original-completion-2026-10-02/execution/p13-02/CODE1_TASK_SPEC.json", + "scope": "new backend suite marker; native execution pending" + } } ], "unknown_roster_suite_policy": "refuse, explicitly review its source/schema before accepting", diff --git a/docs/plans/original-completion-2026-10-02/execution/p13-02/CODE1_IMPLEMENTATION.md b/docs/plans/original-completion-2026-10-02/execution/p13-02/CODE1_IMPLEMENTATION.md new file mode 100644 index 0000000..83340e5 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p13-02/CODE1_IMPLEMENTATION.md @@ -0,0 +1,148 @@ +# P13-02-CODE1 implementation handoff + +This is non-graphic state/reference/occupancy/save machinery only. It does not +complete P13-02, grant partial credit, or change the original 30/88 denominator +or formal 14/88 count. Native execution and acceptance are pending. + +## Declared scope and verification + +Before editing, the worker declared the new service, bounded Runtime hooks, +special tests, narrow runner, marker additions and this documentation. SaveService +was conditional on an exact-key dependency; inspection found +`SupplyAutoPurchaseQuotePlans.valid_runtime_format` has that dependency. +Only allowed paths were edited. No asset, UI, content, model, policy, clock phase, +or P04 change is involved. + +The full ignored `build/owner-handoff/ACTUAL_OPUS_RUN_DECISION.json` was read and +left untouched. Base HEAD is `1e8d4ea88876865a72547ff0edf43ed5b0e74c7e`. +The immutable spec was not edited. Its SHA-256 without the terminal newline is +the registered `8ea65f2eefedc772c08170186f5cd03ac017432f4b069ade4f9210d4215c2d43`; +the on-disk file including that newline is +`988d6e18a49372549863cd433432c2852c962fa64b138e490361c023e80249dc`. + +Worker verification: Python AST parsing/compilation only, without executing the +runner or importing its dependencies; source/JSON/hash review. GDScript parsing, +all tests, `--prepare-only`, native collection and regression were **not run**. +No check totals or native success receipts are claimed. No git writes, commits, +branches, other agents, integration or Runner operations were performed. + +## Service and commands + +`PeepingRoomService` is a separate RefCounted service with no content or UI. +All command executors use the existing Runtime queue; queue/history receipts +remain normal Runtime behavior even for rejected/read-only domain commands. + +| Command | Preconditions and result | +| --- | --- | +| `peeping_room_validate` | `origin: Vector3i`, optional `footprint: Vector2i` (2x1), optional `target_instance_id: String/StringName`. Pure domain validation. | +| `peeping_room_place` | Always `BUILD_DEFINITION_PENDING_D04`; no occupancy, resource, room or ID mutation. | +| `peeping_room_relink` | Existing `room_id`, explicit nonempty `target_instance_id`; full geometry and ownership validation excluding only the room's exact own occupancy. Leaves `LINKED`, stopped, `RELINKED_PENDING_RESUME`. | +| `peeping_room_record_evidence` | Existing `room_id`, nonblank named `source_event_key`. Uses Runtime minute, never caller-supplied time. Checks stopped, target power, target closure/opening hours, completed construction authority, and lost target in that order. Otherwise rejects with `RECORDER_ELIGIBILITY_PENDING_D04_D08_P16`. Never writes the ledger. | + +Malformed command field types return `INVALID_PAYLOAD`. Missing/blank source keys +return `SOURCE_EVENT_KEY_REQUIRED` before operational checks. Loss latches both +`TARGET_LOST` and stopped, so collection after normal demolition returns `STOPPED` +while the room's link/stop reason identifies `TARGET_LOST`. No resume command or +automatic resume is defined. Power/closure guards read actual target conditions; +they do not define special-room power demand, staffing or normal operation. + +Placement is exactly 2x1 BACK, fully behind a single completed FRONT +`public_bath` or `adult_private_room`. It checks bounds/excavation, both real +FacilityService and ConstructionService ownership, FRONT cells, World occupancy, +and both World walkability and PathService membership. Exact rejection families: +`WRONG_LAYER`, `WRONG_FOOTPRINT`, `OUT_OF_BOUNDS`, `NOT_EXCAVATED`, +`TARGET_INCOMPLETE`, `TARGET_MISSING`, `PARTIAL_TARGET`, `MULTIPLE_TARGETS`, +`TARGET_MISMATCH`, `TARGET_UNKNOWN`, `TARGET_NOT_ALLOWED`, `TARGET_WRONG_LAYER`, +`CORRIDOR_CONFLICT`, `BACK_OCCUPIED`. The named `vip_suite` request returns +`PENDING_P10_01`; `vip_private_salon` is not an allowed substitute. + +Any existing corridor cell conflicts. A proposed cut through a connected corridor +is rejected before mutation, preserving the route; no reroute or bypass is +invented. Accepted fixture cells were already nonwalkable and become occupied +under `special_peeping_room`. No special-room construction weight/cost is assumed. + +Runtime demolition and FacilityService unregistration latch loss. Collection also +detects missing FRONT occupancy/identity. Rebuilding the same cells produces a +different construction ID and never silently reattaches rooms. Relink is explicit. + +## CONTROLLED_FIXTURE boundary + +Only service methods named `controlled_fixture_place`, +`controlled_fixture_set_stopped`, and `controlled_fixture_record_evidence` create +test rooms, enable fixture collection, or write evidence. They are labeled +CONTROLLED_FIXTURE in code and tests. They have no command/palette registration, +and no Runtime executor calls them. Test helpers disclose direct underground +geometry setup and MIMI registration; those do not assert natural unlock/join or +production eligibility. Target construction uses existing definitions/costs/times. + +Room IDs come from the existing StableIdGenerator's `peeping_room` counter. +Evidence IDs encode the UTF-8 lengths and hex bytes of the full +`(room_id, target_instance_id, source_event_key)` tuple. This is injective rather +than delimiter concatenation or a hash with possible collisions. Replays return +the existing ID with `duplicated=true` and retain the first minute. Evidence is +only IDs, source key and minute. Room `target_history` retains prior bindings so +relink does not orphan existing opaque evidence. + +## Save invariants + +Runtime versions remain 14 and 15, with version 15 only for existing v2 purchase +history. Optional canonical StringName key `special_peeping_rooms` has section +version 1 and exact keys `version`, `rooms`, `evidence`. Empty service state emits +no key, preserving original snapshots. Absent sections restore empty only if no +world occupancy uses the special kind or `peeping_room.` prefix. Thus removing +the section from a current room-bearing save is rejected without a version bump. + +Restore stages the entire section against staged world, path, facilities, +construction and ID generator before committing any Runtime state. It checks +canonical keys/types, version, duplicate room/evidence identities, deterministic +evidence tuple encoding, bounds/geometry, room/occupancy correspondence in both +directions, generator high-water mark, known historical facility IDs, present +target definition/geometry, loss/stopped coherence, and evidence room/history/time +references. Null/present-empty sections are invalid; absence is the only empty +encoding. Existing pending command/reference and schema migration checks remain. + +SaveService validates a present section through the same staged service validator, +then supplies a shallow copy without this one key to the original purchase-format +allowlist. No other keys are removed. The original format 1 (Runtime 1–14), +format 2 (Runtime 15), manifest duplicate 15 and MIMI checks remain unchanged. + +## Verification handoff + +Five strict suites cover geometry/corridor, removal/relink, evidence/guards, +typed atomic save corruption and physical format 1, and Runtime commands plus +physical MIMI format 2. Tests use `var_to_bytes` for whole Runtime atomicity and +roundtrip checks. New marker rows carry final test source hashes; prior rows and +policy are unchanged. A helper under `tests/special/` is not a discovered suite. + +`tools/run_p13_reference_tests.py` defaults to read-only verification of +`$BUILD_OUTPUT_DIR/p13-reference-native`, failing closed without an absolute +environment path. Explicit `--output --collect` is for Root only. The strict flag +parser rejects abbreviations, repeated flags, positional inputs and `=` aliases. +`--prepare-only` is non-native but was not executed by this worker. + +The runner imports the existing P18 core by path. Since its three entry functions +hardcode their label and seven/nine counts, a bounded AST adapter changes only +protocol/runner identities, those explicit roster cardinalities, and diagnostic +labels. It checks the exact adaptation-site counts and fails closed on drift. +All confinement, inventory, JSON, sealing, source/engine/marker/policy binding, +process, diagnostic, profile and cleanup gates are reused. It neither copies the +whole runner nor modifies P18/helpers. The whole selected source map binds both +runner sources. Phase count derives from `len(ROSTER) + 2`. + +Roster includes all five new suites and existing construction/world/path/runtime +construction, supply migration, transport save, Runtime weather, snapshot +serialization, save lifecycle and MIMI purchase suites. Receipts retain +`formal_acceptance=false` and `original_unit_complete=false`. + +Root must perform serialized native collection with the allocation/private +profile, then registered `p13-reference` validation, a separate actual Opus +review, and the final exact-version required regression. Only those steps can +establish execution results. + +## Still pending, without invented values + +D04 build/upkeep/build minutes and other product numbers; recorder staffing and +natural triggers/rates D04/D08/P16; VIP suite P10-01; D05 transaction decisions +(no tick-phase changes here); P19 evidence use and P24 camera UI; normal palette +registration/construction/operation and natural production evidence; whole P13 +acceptance, final review/integration, and applicable native Windows conditions. diff --git a/scripts/core/save_service.gd b/scripts/core/save_service.gd index 92464b9..236a8f1 100644 --- a/scripts/core/save_service.gd +++ b/scripts/core/save_service.gd @@ -1048,6 +1048,13 @@ static func _snapshot_package_format(snapshot: Dictionary) -> int: 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 runtime_data.has(&"special_peeping_rooms"): + var section_clock: Variant = snapshot.get(&"clock") + if not section_clock is Dictionary or typeof(section_clock.get(&"absolute_minute")) != TYPE_INT: return -1 + if not preload("res://scripts/special/peeping_room_service.gd").valid_runtime_section(runtime_data, section_clock[&"absolute_minute"]): return -1 + # Keep PurchaseQuotePlansV2's exact original allowlist and all format rules. + runtime_data = runtime_data.duplicate() + runtime_data.erase(&"special_peeping_rooms") if version >= 1 and version <= 14: return -1 if PurchaseQuoteV2.has_history(runtime_data) else FORMAT_VERSION if version != 15: return -1 diff --git a/scripts/special/peeping_room_service.gd b/scripts/special/peeping_room_service.gd new file mode 100644 index 0000000..3d0d08f --- /dev/null +++ b/scripts/special/peeping_room_service.gd @@ -0,0 +1,271 @@ +class_name PeepingRoomService +extends RefCounted + +## Non-graphic state/reference machinery; production definitions remain pending. +const SECTION_VERSION := 1 +const ALLOWED_TARGETS := [&"public_bath", &"adult_private_room"] +const OCCUPANCY_KIND := &"special_peeping_room" +const ID_PREFIX := &"peeping_room" +const ROOM_KEYS := [&"room_id", &"origin", &"footprint", &"cells", &"target_instance_id", &"target_definition_id", &"target_history", &"link_state", &"stopped", &"stop_reason"] +const EVIDENCE_KEYS := [&"evidence_id", &"room_id", &"target_instance_id", &"source_event_key", &"minute"] + +var _rooms: Dictionary = {} +var _evidence: Dictionary = {} +var _world: WorldService +var _facilities: FacilityService +var _construction: ConstructionService +var _path: PathService +var _ids: StableIdGenerator + +func configure(world: WorldService, facilities: FacilityService, construction: ConstructionService, path: PathService, ids: StableIdGenerator) -> void: + _world = world + _facilities = facilities + _construction = construction + _path = path + _ids = ids + +static func result(reason: StringName = &"OK") -> Dictionary: + return {&"ok": reason == &"OK", &"reason": reason} + +func is_empty() -> bool: + return _rooms.is_empty() and _evidence.is_empty() + +func get_room(room_id: StringName) -> Dictionary: + return (_rooms.get(room_id, {}) as Dictionary).duplicate(true) + +## Pure validation. own_room is supplied internally only for explicit relink/restore. +func validate_placement(origin: Vector3i, footprint: Vector2i = Vector2i(2, 1), target_id: StringName = &"", own_room: StringName = &"") -> Dictionary: + if origin.z != WorldService.LAYER_BACK: return result(&"WRONG_LAYER") + if footprint != Vector2i(2, 1): return result(&"WRONG_FOOTPRINT") + if target_id == &"vip_suite": return result(&"PENDING_P10_01") + var cells := _world.cells_for_footprint(origin, footprint) + var targets: Array[StringName] = [] + var missing := false + for cell: Vector3i in cells: + if not _world.is_in_bounds(cell): return result(&"OUT_OF_BOUNDS") + if cell.y < 0 and not _world.is_excavated(cell): return result(&"NOT_EXCAVATED") + var front := Vector3i(cell.x, cell.y, WorldService.LAYER_FRONT) + if _world.get_occupant_kind(front) == &"construction": return result(&"TARGET_INCOMPLETE") + var occupant := _world.get_occupant(front) + if occupant.is_empty(): missing = true + elif occupant not in targets: targets.append(occupant) + if targets.size() > 1: return result(&"MULTIPLE_TARGETS") + if missing: return result(&"TARGET_MISSING" if targets.is_empty() else &"PARTIAL_TARGET") + var resolved_id := targets[0] + if not target_id.is_empty() and target_id != resolved_id: return result(&"TARGET_MISMATCH") + var target := _facilities.get_instance(resolved_id) + if target == null: return result(&"TARGET_UNKNOWN") + if target.definition_id not in ALLOWED_TARGETS: return result(&"TARGET_NOT_ALLOWED") + if target.origin.z != WorldService.LAYER_FRONT: return result(&"TARGET_WRONG_LAYER") + var built := _construction.get_facility(resolved_id) + if built.is_empty(): return result(&"TARGET_INCOMPLETE") + if built.get(&"definition_id") != target.definition_id: return result(&"TARGET_MISMATCH") + for cell: Vector3i in cells: + var front := Vector3i(cell.x, cell.y, WorldService.LAYER_FRONT) + if front not in target.occupied_cells or front not in built[&"occupied_cells"]: return result(&"PARTIAL_TARGET") + # Reject cutting an existing corridor; never construct an implicit bypass. + if _world.is_walkable(cell) or _path.has_cell(cell): return result(&"CORRIDOR_CONFLICT") + var occupancy := _world.validate_occupancy(cells) + if not occupancy[&"ok"]: + if own_room.is_empty() or _world.get_entity_cells(own_room) != cells or _world.get_entity_kind(own_room) != OCCUPANCY_KIND: + return result(&"BACK_OCCUPIED") + var report := result() + report.merge({&"cells": cells, &"target_instance_id": resolved_id, &"target_definition_id": target.definition_id}) + return report + +## CONTROLLED_FIXTURE: tests only. No Runtime executor or palette calls this. +func controlled_fixture_place(origin: Vector3i, target_id: StringName = &"") -> Dictionary: + var report := validate_placement(origin, Vector2i(2, 1), target_id) + if not report[&"ok"]: return report + var room_id := _ids.next_id(ID_PREFIX) + if room_id.is_empty(): return result(&"ID_EXHAUSTED") + if not _world.occupy(room_id, report[&"cells"], OCCUPANCY_KIND): return result(&"BACK_OCCUPIED") + _rooms[room_id] = {&"room_id": room_id, &"origin": origin, &"footprint": Vector2i(2, 1), + &"cells": report[&"cells"].duplicate(), &"target_instance_id": report[&"target_instance_id"], + &"target_definition_id": report[&"target_definition_id"], + &"target_history": [report[&"target_instance_id"]], &"link_state": &"LINKED", + &"stopped": true, &"stop_reason": &"CONTROLLED_FIXTURE"} + report[&"room_id"] = room_id + return report + +func target_removed(target_id: StringName) -> void: + for room: Dictionary in _rooms.values(): + if room[&"target_instance_id"] == target_id: + room[&"link_state"] = &"TARGET_LOST" + room[&"stopped"] = true + room[&"stop_reason"] = &"TARGET_LOST" + +func detect_target_loss() -> void: + for room: Dictionary in _rooms.values(): + if room[&"link_state"] == &"TARGET_LOST": continue + if not _target_still_present(room): target_removed(room[&"target_instance_id"]) + +func _target_still_present(room: Dictionary) -> bool: + var target := _facilities.get_instance(room[&"target_instance_id"]) + if target == null or target.definition_id != room[&"target_definition_id"]: return false + for cell: Vector3i in room[&"cells"]: + if _world.get_occupant(Vector3i(cell.x, cell.y, WorldService.LAYER_FRONT)) != target.instance_id: return false + return true + +func relink(room_id: StringName, target_id: StringName) -> Dictionary: + if not _rooms.has(room_id): return result(&"ROOM_MISSING") + if target_id.is_empty(): return result(&"TARGET_MISSING") + var room: Dictionary = _rooms[room_id] + var report := validate_placement(room[&"origin"], room[&"footprint"], target_id, room_id) + if not report[&"ok"]: return report + room[&"target_instance_id"] = target_id + room[&"target_definition_id"] = report[&"target_definition_id"] + if target_id not in room[&"target_history"]: room[&"target_history"].append(target_id) + room[&"link_state"] = &"LINKED" + room[&"stopped"] = true + room[&"stop_reason"] = &"RELINKED_PENDING_RESUME" + return result() + +func _collection_precondition(room_id: StringName, source_event_key: StringName, minute: int) -> Dictionary: + if String(source_event_key).strip_edges().is_empty(): return result(&"SOURCE_EVENT_KEY_REQUIRED") + if minute < 0: return result(&"INVALID_MINUTE") + if not _rooms.has(room_id): return result(&"ROOM_MISSING") + detect_target_loss() + var room: Dictionary = _rooms[room_id] + # Required precedence: stopped, power, closed, construction, lost target. + if room[&"stopped"]: return result(&"STOPPED") + var target := _facilities.get_instance(room[&"target_instance_id"]) + if target != null: + if not target.power_available: return result(&"NO_POWER") + if not target.operator_open or not target.is_within_opening_hours(minute): return result(&"CLOSED") + if _construction.get_facility(target.instance_id).is_empty(): return result(&"UNDER_CONSTRUCTION") + if room[&"link_state"] == &"TARGET_LOST" or target == null: return result(&"TARGET_LOST") + return result() + +func record_evidence(room_id: StringName, source_event_key: StringName, minute: int) -> Dictionary: + var report := _collection_precondition(room_id, source_event_key, minute) + return report if not report[&"ok"] else result(&"RECORDER_ELIGIBILITY_PENDING_D04_D08_P16") + +## CONTROLLED_FIXTURE: explicit test state, never a production resume command. +func controlled_fixture_set_stopped(room_id: StringName, stopped: bool) -> bool: + if not _rooms.has(room_id): return false + detect_target_loss() + var room: Dictionary = _rooms[room_id] + if not stopped and room[&"link_state"] == &"TARGET_LOST": return false + room[&"stopped"] = stopped + room[&"stop_reason"] = &"CONTROLLED_FIXTURE" if stopped else &"" + return true + +## CONTROLLED_FIXTURE: sole ledger write entry point, opaque event keys only. +func controlled_fixture_record_evidence(room_id: StringName, source_event_key: StringName, minute: int) -> Dictionary: + var report := _collection_precondition(room_id, source_event_key, minute) + if not report[&"ok"]: return report + var target_id: StringName = _rooms[room_id][&"target_instance_id"] + var evidence_id := _evidence_id(room_id, target_id, source_event_key) + var duplicated := _evidence.has(evidence_id) + if not duplicated: + _evidence[evidence_id] = {&"evidence_id": evidence_id, &"room_id": room_id, + &"target_instance_id": target_id, &"source_event_key": source_event_key, &"minute": minute} + return {&"ok": true, &"reason": &"OK", &"evidence_id": evidence_id, &"duplicated": duplicated} + +static func _evidence_id(room_id: StringName, target_id: StringName, event_key: StringName) -> StringName: + # Length-prefixed UTF-8 hex is injective; delimiters in input cannot alias. + var pieces: Array[String] = [] + for value: StringName in [room_id, target_id, event_key]: + var bytes := String(value).to_utf8_buffer() + pieces.append("%d:%s" % [bytes.size(), bytes.hex_encode()]) + return StringName("peeping_evidence:" + ":".join(pieces)) + +func create_snapshot() -> Dictionary: + var rooms: Array[Dictionary] = [] + var evidence: Array[Dictionary] = [] + var keys := _rooms.keys() + keys.sort() + for key: StringName in keys: rooms.append(_rooms[key].duplicate(true)) + keys = _evidence.keys() + keys.sort() + for key: StringName in keys: evidence.append(_evidence[key].duplicate(true)) + return {&"version": SECTION_VERSION, &"rooms": rooms, &"evidence": evidence} + +static func _exact(value: Variant, keys: Array) -> bool: + if not value is Dictionary or value.size() != keys.size(): return false + for key: Variant in value: + if typeof(key) != TYPE_STRING_NAME or key not in keys: return false + return true + +## Save format 15 has a fixed purchase-key allowlist outside this task's scope. +## Validate this independent section before passing only the original keys to it. +static func valid_runtime_section(data: Dictionary, minute: int) -> bool: + if not data.get(&"special_peeping_rooms") is Dictionary: return false + for key: Variant in data: + if key == &"special_peeping_rooms" and typeof(key) != TYPE_STRING_NAME: return false + for key: StringName in [&"world", &"path", &"facilities", &"construction", &"id_generator"]: + if not data.get(key) is Dictionary: return false + var world := WorldService.new() + var path := PathService.new() + var facilities := FacilityService.new() + var ids := StableIdGenerator.new() + if not world.restore_snapshot(data[&"world"]) or not path.restore_snapshot(data[&"path"]) or not facilities.restore_snapshot(data[&"facilities"]) or not ids.restore_snapshot(data[&"id_generator"]): return false + var construction := ConstructionService.new(world) + if not construction.restore_snapshot(data[&"construction"]): return false + var service := PeepingRoomService.new() + service.configure(world, facilities, construction, path, ids) + return service.restore_section(data[&"special_peeping_rooms"], minute) + +## Staged restore validates both directions of world ownership before committing. +func restore_section(section: Variant, minute: int) -> bool: + var rooms: Dictionary = {} + var evidence: Dictionary = {} + if section != null: + if not _exact(section, [&"version", &"rooms", &"evidence"]): return false + if typeof(section[&"version"]) != TYPE_INT or section[&"version"] != SECTION_VERSION: return false + if not section[&"rooms"] is Array or not section[&"evidence"] is Array: return false + if section[&"rooms"].is_empty(): return false # absence represents empty state + var known: Array = _facilities.create_snapshot()[&"known_instance_ids"] + for raw: Variant in section[&"rooms"]: + if not _exact(raw, ROOM_KEYS): return false + for key: StringName in [&"room_id", &"target_instance_id", &"target_definition_id", &"link_state", &"stop_reason"]: + if typeof(raw[key]) != TYPE_STRING_NAME: return false + var room_id: StringName = raw[&"room_id"] + var suffix := String(room_id).trim_prefix("peeping_room.") + if suffix.length() != 12 or not suffix.is_valid_int() or int(suffix) <= 0 or String(room_id) != "peeping_room.%012d" % int(suffix): return false + if int(suffix) > _ids.get_counter(ID_PREFIX) or rooms.has(room_id): return false + if typeof(raw[&"origin"]) != TYPE_VECTOR3I or typeof(raw[&"footprint"]) != TYPE_VECTOR2I: return false + if raw[&"origin"].z != WorldService.LAYER_BACK or raw[&"footprint"] != Vector2i(2, 1): return false + var cells := _world.cells_for_footprint(raw[&"origin"], raw[&"footprint"]) + if not raw[&"cells"] is Array or raw[&"cells"] != cells: return false + for cell: Variant in raw[&"cells"]: + if typeof(cell) != TYPE_VECTOR3I: return false + if _world.get_entity_cells(room_id) != cells or _world.get_entity_kind(room_id) != OCCUPANCY_KIND: return false + for cell: Vector3i in cells: + if _world.is_walkable(cell) or _path.has_cell(cell): return false + if raw[&"target_definition_id"] not in ALLOWED_TARGETS: return false + if typeof(raw[&"stopped"]) != TYPE_BOOL or not raw[&"target_history"] is Array: return false + var history := {} + for target: Variant in raw[&"target_history"]: + if typeof(target) != TYPE_STRING_NAME or String(target) not in known or history.has(target): return false + history[target] = true + if not history.has(raw[&"target_instance_id"]): return false + var current_target := _facilities.get_instance(raw[&"target_instance_id"]) + if current_target != null and current_target.definition_id != raw[&"target_definition_id"]: return false + if raw[&"link_state"] == &"TARGET_LOST": + if not raw[&"stopped"] or raw[&"stop_reason"] != &"TARGET_LOST" or _target_still_present(raw): return false + elif raw[&"link_state"] == &"LINKED": + if not validate_placement(raw[&"origin"], raw[&"footprint"], raw[&"target_instance_id"], room_id)[&"ok"]: return false + if _facilities.get_instance(raw[&"target_instance_id"]).definition_id != raw[&"target_definition_id"]: return false + if raw[&"stopped"]: + if raw[&"stop_reason"] not in [&"CONTROLLED_FIXTURE", &"RELINKED_PENDING_RESUME"]: return false + elif raw[&"stop_reason"] != &"": return false + else: return false + rooms[room_id] = raw.duplicate(true) + for raw: Variant in section[&"evidence"]: + if not _exact(raw, EVIDENCE_KEYS): return false + for key: StringName in [&"evidence_id", &"room_id", &"target_instance_id", &"source_event_key"]: + if typeof(raw[key]) != TYPE_STRING_NAME or raw[key].is_empty(): return false + if String(raw[&"source_event_key"]).strip_edges().is_empty(): return false + if typeof(raw[&"minute"]) != TYPE_INT or raw[&"minute"] < 0 or raw[&"minute"] > minute: return false + if not rooms.has(raw[&"room_id"]) or raw[&"target_instance_id"] not in rooms[raw[&"room_id"]][&"target_history"]: return false + var expected := _evidence_id(raw[&"room_id"], raw[&"target_instance_id"], raw[&"source_event_key"]) + if expected != raw[&"evidence_id"] or evidence.has(expected): return false + evidence[expected] = raw.duplicate(true) + for occupied: Dictionary in _world.create_snapshot()[&"occupancies"]: + if occupied[&"kind"] == OCCUPANCY_KIND or String(occupied[&"entity_id"]).begins_with("peeping_room."): + if not rooms.has(occupied[&"entity_id"]): return false + _rooms = rooms + _evidence = evidence + return true diff --git a/tests/special/peeping_room_fixture.gd b/tests/special/peeping_room_fixture.gd new file mode 100644 index 0000000..f354693 --- /dev/null +++ b/tests/special/peeping_room_fixture.gd @@ -0,0 +1,59 @@ +extends SceneTree + +## CONTROLLED_FIXTURE. No production placement, recorder or eligibility claim. +const Rooms := preload("res://scripts/special/peeping_room_service.gd") +var checks := 0 +var failures := 0 +var fixtures: Array[Dictionary] = [] + +func _initialize() -> void: + call_deferred("_run") + +func expect(ok: bool, label: String) -> void: + checks += 1 + if not ok: + failures += 1 + push_error(label) + +func fixture(build_bath := true) -> Dictionary: + var clock: Variant = load("res://autoload/game_clock.gd").new() + var state: Variant = load("res://autoload/game_state.gd").new() + var runtime: Variant = load("res://autoload/simulation_runtime.gd").new() + clock.reset(1, 6, 0) + state.reset() + expect(runtime.initialize(clock, state, 1302)[&"valid"], "CONTROLLED_FIXTURE real Runtime initializes") + clock.set_paused(true) + runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false}) + var f := {&"clock": clock, &"state": state, &"runtime": runtime, &"target": &"", &"origin": Vector3i(-6, 1, 1)} + fixtures.append(f) + if build_bath: + var built: CommandResult = runtime.submit_command(&"build_facility", {&"definition_id": &"public_bath", &"origin": Vector3i(-6, 0, 0)}) + expect(built.is_success(), "real paid public bath construction") + clock.advance_minutes(runtime.definition_registry.get_facility(&"public_bath").build_minutes) + for target: FacilityInstance in runtime.facility_service.get_instances(): + if target.definition_id == &"public_bath": f[&"target"] = target.instance_id + expect(not f[&"target"].is_empty(), "completed FRONT target has real stable identity") + return f + +func place(f: Dictionary, origin: Variant = null) -> StringName: + var report: Dictionary = f[&"runtime"].peeping_room_service.controlled_fixture_place(f[&"origin"] if origin == null else origin) + expect(report[&"ok"], "CONTROLLED_FIXTURE only room placement: " + String(report[&"reason"])) + return report.get(&"room_id", &"") + +func enable_fixture(f: Dictionary, room: StringName) -> void: + var runtime: Variant = f[&"runtime"] + expect(runtime.peeping_room_service.controlled_fixture_set_stopped(room, false), "CONTROLLED_FIXTURE explicitly enables ledger exercise") + expect(runtime.facility_service.set_operational_status(f[&"target"], {&"power_available": true, &"operator_open": true}, f[&"clock"].absolute_minute).is_success(), "CONTROLLED_FIXTURE target conditions") + +func rejects_atomically(runtime: Variant, snapshot: Dictionary) -> bool: + var before := var_to_bytes(runtime.create_snapshot()) + return not runtime.restore_snapshot(snapshot) and before == var_to_bytes(runtime.create_snapshot()) + +func finish(label: String) -> void: + for f: Dictionary in fixtures: + f[&"runtime"].free() + f[&"clock"].free() + f[&"state"].free() + fixtures.clear() + print("%s: %d checks, %d failures" % [label, checks, failures]) + quit(1 if failures else 0) diff --git a/tests/special/test_p13_uid_policy.py b/tests/special/test_p13_uid_policy.py new file mode 100644 index 0000000..110f3ff --- /dev/null +++ b/tests/special/test_p13_uid_policy.py @@ -0,0 +1,96 @@ +"""Static UID policy cases; no engine, native worker or regression invocation.""" +import importlib.util +from pathlib import Path +import sys +import tempfile +import unittest + +sys.dont_write_bytecode = True +TOOLS = Path(__file__).resolve().parents[2] / 'tools' +sys.path.insert(0, str(TOOLS)) +spec = importlib.util.spec_from_file_location('p13_uid_policy', TOOLS / 'run_p13_reference_tests.py') +runner = importlib.util.module_from_spec(spec) +spec.loader.exec_module(runner) + + +class UIDPolicyTests(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.stage = Path(self.tmp.name) + self.owner = 'tests/special/example.gd' + self.other = 'scripts/special/peeping_room_service.gd' + for name in (self.owner, self.other): + self.write(name, b'extends RefCounted\n') + self.write('project.godot', b'[application]\n') + self.write('scripts/existing.gd.uid', b'uid://existing\n') + self.source = runner.shared.inventory(self.stage, stage=True) + self.plan = runner.uid_plan(self.source) + self.write(self.owner + '.uid', b'uid://first\n') + self.write(self.other + '.uid', b'uid://second\n') + + def write(self, name, raw): + path = self.stage / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(raw) + + def snapshot(self): + return runner.uid_snapshot(self.stage, self.source, self.plan) + + def test_accept_and_owner_seal(self): + accepted = self.snapshot() + self.assertEqual(set(accepted), {self.owner + '.uid', self.other + '.uid'}) + pin = accepted[self.owner + '.uid'] + self.assertEqual(pin['raw_bytes'], list(b'uid://first\n')) + self.assertEqual(pin['text'], 'uid://first\n') + self.assertEqual(pin['owner_sha256'], self.source[self.owner]['sha256']) + self.assertEqual(pin['sha256'], runner.shared.sha(self.stage / (self.owner + '.uid'))) + + def test_unknown_and_extra(self): + for name in ('scripts/unknown.gd.uid', 'extra.txt'): + with self.subTest(name=name): + self.write(name, b'uid://extra\n') + with self.assertRaises(AssertionError): + self.snapshot() + (self.stage / name).unlink() + + def test_missing(self): + (self.stage / (self.owner + '.uid')).unlink() + with self.assertRaises(AssertionError): + self.snapshot() + + def test_malformed(self): + for raw in (b'uid://UPPER\n', b'uid://first', b'uid://first\nextra', + b'uid://12345678901234\n', b'uid://\n', b'uid://first\r\n'): + with self.subTest(raw=raw): + self.write(self.owner + '.uid', raw) + with self.assertRaises(AssertionError): + self.snapshot() + + def test_duplicate_and_existing_collision(self): + for raw in (b'uid://second\n', b'uid://existing\n'): + with self.subTest(raw=raw): + self.write(self.owner + '.uid', raw) + with self.assertRaises(AssertionError): + self.snapshot() + + def test_modified_original(self): + for name in (self.owner, 'project.godot', 'scripts/existing.gd.uid'): + with self.subTest(name=name): + path = self.stage / name + original = path.read_bytes() + path.write_bytes(original + b'tamper') + with self.assertRaises(AssertionError): + self.snapshot() + path.write_bytes(original) + + def test_symlink(self): + path = self.stage / (self.owner + '.uid') + path.unlink() + path.symlink_to(self.stage / (self.other + '.uid')) + with self.assertRaises(AssertionError): + self.snapshot() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/special/test_peeping_room_evidence.gd b/tests/special/test_peeping_room_evidence.gd new file mode 100644 index 0000000..e8b3286 --- /dev/null +++ b/tests/special/test_peeping_room_evidence.gd @@ -0,0 +1,40 @@ +extends "res://tests/special/peeping_room_fixture.gd" + +func _run() -> void: + var f := fixture() + var runtime: Variant = f[&"runtime"] + var room := place(f) + var other := place(f, Vector3i(-4, 1, 1)) + var service: Variant = runtime.peeping_room_service + var minute: int = f[&"clock"].absolute_minute + expect(service.record_evidence(room, &"", minute)[&"reason"] == &"SOURCE_EVENT_KEY_REQUIRED", "named source key required first") + expect(service.controlled_fixture_record_evidence(room, &"fixture.event", minute)[&"reason"] == &"STOPPED", "initial stopped state rejects even fixture ledger") + enable_fixture(f, room) + enable_fixture(f, other) + var target: FacilityInstance = runtime.facility_service.get_instance(f[&"target"]) + target.power_available = false + target.operator_open = false + expect(service.record_evidence(room, &"fixture.event", minute)[&"reason"] == &"NO_POWER", "power rejection precedes closed") + target.power_available = true + expect(service.record_evidence(room, &"fixture.event", minute)[&"reason"] == &"CLOSED", "closed target rejects") + target.operator_open = true + # CONTROLLED_FIXTURE temporarily omit construction authority, leaving live target. + var empty_construction := ConstructionService.new(runtime.world_service) + service.configure(runtime.world_service, runtime.facility_service, empty_construction, runtime.path_service, runtime.id_generator) + expect(service.record_evidence(room, &"fixture.event", minute)[&"reason"] == &"UNDER_CONSTRUCTION", "uncompleted construction authority rejects collection") + service.configure(runtime.world_service, runtime.facility_service, runtime.construction_service, runtime.path_service, runtime.id_generator) + var before := var_to_bytes(service.create_snapshot()) + expect(service.record_evidence(room, &"fixture.event", minute)[&"reason"] == &"RECORDER_ELIGIBILITY_PENDING_D04_D08_P16", "production collection never invents eligibility") + expect(before == var_to_bytes(service.create_snapshot()), "production collection leaves ledger unchanged") + var first: Dictionary = service.controlled_fixture_record_evidence(room, &"fixture.event", minute) + var replay: Dictionary = service.controlled_fixture_record_evidence(room, &"fixture.event", minute + 1) + expect(first[&"ok"] and not first[&"duplicated"] and replay[&"duplicated"] and replay[&"evidence_id"] == first[&"evidence_id"], "tuple replay returns exactly one stable ID") + var second: Dictionary = service.controlled_fixture_record_evidence(other, &"fixture.event", minute) + var third: Dictionary = service.controlled_fixture_record_evidence(room, &"fixture:event:日本語", minute) + expect(second[&"evidence_id"] != first[&"evidence_id"] and third[&"evidence_id"] != first[&"evidence_id"], "different room/key distinct, delimiter and UTF-8 input supported") + expect(service.create_snapshot()[&"evidence"].size() == 3, "replay adds no ledger record") + var snapshot: Dictionary = runtime.create_snapshot() + expect(runtime.restore_snapshot(snapshot), "active fixture evidence restores") + var restored: Dictionary = runtime.peeping_room_service.controlled_fixture_record_evidence(room, &"fixture.event", minute) + expect(restored[&"duplicated"] and restored[&"evidence_id"] == first[&"evidence_id"], "evidence stable across restore") + finish("Peeping room evidence") diff --git a/tests/special/test_peeping_room_geometry.gd b/tests/special/test_peeping_room_geometry.gd new file mode 100644 index 0000000..d95206b --- /dev/null +++ b/tests/special/test_peeping_room_geometry.gd @@ -0,0 +1,62 @@ +extends "res://tests/special/peeping_room_fixture.gd" + +func _run() -> void: + var f := fixture() + var runtime: Variant = f[&"runtime"] + var service: Variant = runtime.peeping_room_service + var origin: Vector3i = f[&"origin"] + var before := var_to_bytes(runtime.create_snapshot()) + expect(service.validate_placement(origin)[&"ok"], "public bath entirely behind one completed FRONT target") + expect(service.validate_placement(Vector3i(-6, 1, 0))[&"reason"] == &"WRONG_LAYER", "FRONT room rejected") + expect(service.validate_placement(origin, Vector2i(1, 2))[&"reason"] == &"WRONG_FOOTPRINT", "rotated footprint rejected") + expect(service.validate_placement(origin, Vector2i(2, 1), &"vip_suite")[&"reason"] == &"PENDING_P10_01", "VIP unavailable, never substituted") + expect(service.validate_placement(Vector3i(-1, 1, 1))[&"reason"] == &"PARTIAL_TARGET", "partial target rejected") + expect(service.validate_placement(Vector3i(8, 1, 1))[&"reason"] == &"TARGET_MISSING", "missing target rejected") + expect(service.validate_placement(origin, Vector2i(2, 1), &"missing")[&"reason"] == &"TARGET_MISMATCH", "named reference must match FRONT occupant") + expect(before == var_to_bytes(runtime.create_snapshot()), "geometry validation is pure across whole Runtime") + # Complete real second bath; the 2x1 footprint at the boundary spans targets. + expect(runtime.submit_command(&"build_facility", {&"definition_id": &"public_bath", &"origin": Vector3i(0, 0, 0)}).is_success(), "second paid target starts") + expect(service.validate_placement(Vector3i(0, 1, 1))[&"reason"] == &"TARGET_INCOMPLETE", "construction reservation is not a completed target") + f[&"clock"].advance_minutes(runtime.definition_registry.get_facility(&"public_bath").build_minutes) + expect(service.validate_placement(Vector3i(-1, 1, 1))[&"reason"] == &"MULTIPLE_TARGETS", "multiple FRONT target footprints rejected") + # A real path joining both sides through the proposed footprint must survive. + for x in range(-7, -3): runtime.world_service.set_walkable(Vector3i(x, 1, 1), true) + expect(runtime.rebuild_path_graph_from_world(), "CONTROLLED_FIXTURE corridor graph rebuild") + var path_before: Array = runtime.path_service.find_path(Vector3i(-7, 1, 1), Vector3i(-4, 1, 1)) + expect(not path_before.is_empty(), "proposed footprint lies across a connected corridor") + expect(service.validate_placement(origin)[&"reason"] == &"CORRIDOR_CONFLICT", "would disconnect BACK corridor: reject with no bypass") + expect(runtime.path_service.find_path(Vector3i(-7, 1, 1), Vector3i(-4, 1, 1)) == path_before, "rejected cut preserves route") + for x in range(-7, -3): runtime.world_service.set_walkable(Vector3i(x, 1, 1), false) + expect(service.validate_placement(origin)[&"reason"] == &"CORRIDOR_CONFLICT", "stale path authority also rejects conflict") + expect(runtime.rebuild_path_graph_from_world(), "corridor fixture removed explicitly") + var room := place(f) + expect(service.validate_placement(origin)[&"reason"] == &"BACK_OCCUPIED", "dedicated room occupancy blocks overlap") + expect(runtime.world_service.get_entity_kind(room) == Rooms.OCCUPANCY_KIND, "dedicated cross-reference occupancy kind") + _private_geometry() + finish("Peeping room geometry") + +func _private_geometry() -> void: + var f := fixture(false) + var runtime: Variant = f[&"runtime"] + # CONTROLLED_FIXTURE underground geometry only: no unlock/normal palette claim. + var bounds: Dictionary = runtime.world_service.get_bounds() + bounds[&"min_y"] = -1 + expect(runtime.world_service.expand_bounds(bounds), "CONTROLLED_FIXTURE underground bounds") + var definition: FacilityDefinition = runtime.definition_registry.get_facility(&"adult_private_room") + for x in range(2): + var cell := Vector3i(x, -1, 0) + runtime.world_service.set_excavated(cell, true) + runtime.world_service.set_support(cell, definition.weight_per_cell, true) + var started: CommandResult = runtime.construction_service.start_construction(definition, Vector3i(0, -1, 0)) + expect(started.is_success(), "CONTROLLED_FIXTURE paid real adult_private_room definition") + f[&"clock"].advance_minutes(definition.build_minutes) + var service: Variant = runtime.peeping_room_service + expect(service.validate_placement(Vector3i(0, -1, 1))[&"ok"], "adult_private_room valid completed geometry") + var target_id: StringName = runtime.world_service.get_occupant(Vector3i(0, -1, 0)) + var target: FacilityInstance = runtime.facility_service.get_instance(target_id) + if target != null: + target.definition_id = &"vip_private_salon" + expect(service.validate_placement(Vector3i(0, -1, 1))[&"reason"] == &"TARGET_NOT_ALLOWED", "salon is not VIP suite substitute") + target.definition_id = &"adult_private_room" + runtime.facility_service.unregister_instance(target_id) + expect(service.validate_placement(Vector3i(0, -1, 1))[&"reason"] == &"TARGET_UNKNOWN", "world occupant without actual FacilityService target rejected") diff --git a/tests/special/test_peeping_room_reference.gd b/tests/special/test_peeping_room_reference.gd new file mode 100644 index 0000000..ba3697f --- /dev/null +++ b/tests/special/test_peeping_room_reference.gd @@ -0,0 +1,37 @@ +extends "res://tests/special/peeping_room_fixture.gd" + +func _run() -> void: + var f := fixture() + var runtime: Variant = f[&"runtime"] + var room := place(f) + enable_fixture(f, room) + var old_target: StringName = f[&"target"] + var old_evidence: Dictionary = runtime.peeping_room_service.controlled_fixture_record_evidence(room, &"fixture.event", f[&"clock"].absolute_minute) + expect(old_evidence[&"ok"], "fixture evidence before removal") + expect(runtime.submit_command(&"demolish_facility", {&"instance_id": old_target}).is_success(), "real Runtime demolition") + var lost: Dictionary = runtime.peeping_room_service.get_room(room) + expect(lost[&"link_state"] == &"TARGET_LOST" and lost[&"stopped"], "target removal immediately marks lost and stopped") + expect(runtime.peeping_room_service.record_evidence(room, &"fixture.after_loss", f[&"clock"].absolute_minute)[&"reason"] == &"STOPPED", "collection rejected after loss with prescribed stopped precedence") + expect(not runtime.peeping_room_service.controlled_fixture_set_stopped(room, false), "lost target cannot resume even through fixture") + var lost_snapshot: Dictionary = runtime.create_snapshot() + expect(runtime.restore_snapshot(lost_snapshot), "lost reference roundtrip retains historical identity") + expect(var_to_bytes(lost_snapshot) == var_to_bytes(runtime.create_snapshot()), "lost snapshot exact typed roundtrip") + expect(runtime.submit_command(&"build_facility", {&"definition_id": &"public_bath", &"origin": Vector3i(-6, 0, 0)}).is_success(), "rebuild at same geometry starts") + f[&"clock"].advance_minutes(runtime.definition_registry.get_facility(&"public_bath").build_minutes) + var new_target: StringName = runtime.world_service.get_occupant(Vector3i(-6, 0, 0)) + expect(new_target != old_target, "rebuild receives different target identity") + expect(runtime.peeping_room_service.get_room(room)[&"target_instance_id"] == old_target, "no implicit relink on geometry reuse") + var rejected: CommandResult = runtime.submit_command(&"peeping_room_relink", {&"room_id": room, &"target_instance_id": old_target}) + expect(not rejected.is_success(), "explicit old target relink rejected") + expect(runtime.submit_command(&"peeping_room_relink", {&"room_id": room, &"target_instance_id": new_target}).is_success(), "explicit new target full validation") + var linked: Dictionary = runtime.peeping_room_service.get_room(room) + expect(linked[&"stopped"] and linked[&"stop_reason"] == &"RELINKED_PENDING_RESUME", "explicit relink never auto resumes") + f[&"target"] = new_target + enable_fixture(f, room) + var new_evidence: Dictionary = runtime.peeping_room_service.controlled_fixture_record_evidence(room, &"fixture.event", f[&"clock"].absolute_minute) + expect(new_evidence[&"ok"] and new_evidence[&"evidence_id"] != old_evidence.get(&"evidence_id"), "same room/event on different target never aliases") + var linked_snapshot: Dictionary = runtime.create_snapshot() + expect(runtime.restore_snapshot(linked_snapshot) and var_to_bytes(linked_snapshot) == var_to_bytes(runtime.create_snapshot()), "relinked ledger keeps both historical target bindings") + expect(runtime.facility_service.unregister_instance(new_target).is_success(), "equivalent FacilityService removal path") + expect(runtime.peeping_room_service.get_room(room)[&"link_state"] == &"TARGET_LOST", "unregister signal also invalidates target") + finish("Peeping room reference") diff --git a/tests/special/test_peeping_room_runtime.gd b/tests/special/test_peeping_room_runtime.gd new file mode 100644 index 0000000..06ef0ea --- /dev/null +++ b/tests/special/test_peeping_room_runtime.gd @@ -0,0 +1,91 @@ +extends "res://tests/special/peeping_room_fixture.gd" + +func _run() -> void: + var f := fixture() + var runtime: Variant = f[&"runtime"] + # Direct executor invocation distinguishes read-only domain state from queue receipts. + var validation := SimulationCommand.new() + validation.command_type = &"peeping_room_validate" + validation.payload = {&"origin": f[&"origin"]} + var before := var_to_bytes(runtime.create_snapshot()) + expect(runtime._execute_peeping_room(validation).is_success(), "read-only validator executor") + expect(before == var_to_bytes(runtime.create_snapshot()), "validator changes no whole Runtime bytes") + var public_validation: CommandResult = runtime.submit_command(&"peeping_room_validate", validation.payload) + expect(public_validation.is_success(), "registered validation command runs through real queue") + var placement := SimulationCommand.new() + placement.command_type = &"peeping_room_place" + placement.payload = validation.payload.duplicate(true) + var placement_before := var_to_bytes(runtime.create_snapshot()) + var direct_unavailable: CommandResult = runtime._execute_peeping_room(placement) + expect(not direct_unavailable.is_success() and direct_unavailable.details[&"reason"] == &"BUILD_DEFINITION_PENDING_D04", "direct production placement unavailable with precise reason") + expect(placement_before == var_to_bytes(runtime.create_snapshot()), "direct placement adds no room, occupancy, cost or ID in whole typed Runtime") + var domain_before := var_to_bytes([runtime.world_service.create_snapshot(), runtime.economy_service.create_snapshot(), runtime.peeping_room_service.create_snapshot()]) + var ids_before: Dictionary = runtime.id_generator.create_snapshot() + var counters_before: Dictionary = ids_before["counters"] + var event_counter_before: int = counters_before.get("domain_event", 0) + var event_ids_before := {} + for event: SimulationDomainEvent in runtime.domain_events.recent_events(): + event_ids_before[event.event_id] = true + var unavailable: CommandResult = runtime.submit_command(&"peeping_room_place", validation.payload) + expect(not unavailable.is_success() and unavailable.details[&"reason"] == &"BUILD_DEFINITION_PENDING_D04", "production placement unavailable with precise reason") + expect(domain_before == var_to_bytes([runtime.world_service.create_snapshot(), runtime.economy_service.create_snapshot(), runtime.peeping_room_service.create_snapshot()]), "queued placement adds no room, occupancy or cost") + var ids_after: Dictionary = runtime.id_generator.create_snapshot() + var counters_after: Dictionary = ids_after["counters"] + expect(ids_before["version"] == ids_after["version"], "queued placement preserves ID generator version") + for prefix: Variant in counters_before: + if prefix != "domain_event": + expect(counters_after.has(prefix) and counters_after.get(prefix) == counters_before[prefix], "queued placement consumes no non-event ID or removes prefix: %s" % prefix) + for prefix: Variant in counters_after: + if prefix != "domain_event": + expect(counters_before.has(prefix), "queued placement adds no non-event ID prefix: %s" % prefix) + expect(counters_after.has("domain_event") and counters_after["domain_event"] == event_counter_before + 1, "queued placement allocates exactly one normal receipt event ID") + var new_events: Array[SimulationDomainEvent] = [] + for event: SimulationDomainEvent in runtime.domain_events.recent_events(): + if not event_ids_before.has(event.event_id): + new_events.append(event) + expect(new_events.size() == 1, "queued placement publishes exactly one new receipt event") + if new_events.size() == 1: + var receipt: SimulationDomainEvent = new_events[0] + expect(receipt.event_id == StringName("domain_event.%012d" % (event_counter_before + 1)), "placement receipt uses the next domain event ID") + expect(receipt.event_type == &"COMMAND_FAILED", "unavailable placement receipt is COMMAND_FAILED") + expect(receipt.causation_command_id == unavailable.command_id, "placement receipt causation matches rejected command") + var room := place(f) + enable_fixture(f, room) + var blocked: CommandResult = runtime.submit_command(&"peeping_room_record_evidence", {&"room_id": room, &"source_event_key": &"fixture.command"}) + expect(not blocked.is_success() and blocked.details[&"reason"] == &"RECORDER_ELIGIBILITY_PENDING_D04_D08_P16", "registered evidence command cannot reach ledger") + var missing: CommandResult = runtime.submit_command(&"peeping_room_record_evidence", {&"room_id": room}) + expect(not missing.is_success() and missing.details[&"reason"] == &"SOURCE_EVENT_KEY_REQUIRED", "public command requires named key") + var malformed: CommandResult = runtime.submit_command(&"peeping_room_validate", {&"origin": [0, 1, 1]}) + expect(not malformed.is_success() and malformed.details[&"reason"] == &"INVALID_PAYLOAD", "wrong payload type safely rejected") + expect(runtime.peeping_room_service.create_snapshot()[&"evidence"].is_empty(), "commands cannot inject arbitrary content events") + var fixture_command: CommandResult = runtime.submit_command(&"controlled_fixture_place", validation.payload) + expect(not fixture_command.is_success(), "no fixture command registration") + _mimi_format(f) + finish("Peeping room runtime") + +func _mimi_format(f: Dictionary) -> void: + var runtime: Variant = f[&"runtime"] + const Quote := preload("res://scripts/supply/supply_purchase_quote_v2.gd") + expect(runtime.submit_command(&"build_facility", {&"definition_id": &"warehouse", &"origin": Vector3i(2, 0, 1)}).is_success(), "paid warehouse for independent existing MIMI format path") + f[&"clock"].advance_minutes(runtime.definition_registry.get_facility(&"warehouse").build_minutes) + var warehouse: StringName = &"" + for facility: FacilityInstance in runtime.facility_service.get_instances(): + if facility.definition_id == &"warehouse": warehouse = facility.instance_id + expect(not warehouse.is_empty(), "completed real warehouse") + var mimi := StaffRuntime.new().configure(Quote.MIMI, &"porter", {&"employment_kind": StaffRuntime.EmploymentKind.UNIQUE}) + expect(runtime.staff_service.register_staff(mimi).is_success(), "CONTROLLED_FIXTURE existing MIMI unique registration, no natural join claim") + expect(runtime.submit_command(&"set_staff_work_assignment", {&"staff_id": Quote.MIMI, &"facility_id": warehouse, &"template_id": &"NIGHT"}).is_success(), "existing persistent MIMI assignment") + expect(runtime.submit_command(&"purchase_supplies", {&"warehouse_id": warehouse, &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 1}).is_success(), "existing real v2 paid purchase") + var snapshot: Dictionary = runtime.create_snapshot() + expect(snapshot[&"systems"][&"runtime"][&"runtime_schema_version"] == 15 and snapshot[&"systems"][&"runtime"].has(&"special_peeping_rooms"), "v2 purchase history and special section coexist") + expect(runtime.restore_snapshot(snapshot) and var_to_bytes(snapshot) == var_to_bytes(runtime.create_snapshot()), "v15 optional section exact typed restore") + var directory := "user://tests/p13_reference_mimi_%d" % OS.get_process_id() + expect(SaveService.save_snapshot(directory, snapshot)[&"ok"], "physical v15 save accepts validated extension without loosening purchase allowlist") + var loaded := SaveService.load_snapshot(directory) + expect(loaded[&"ok"], "physical MIMI section save loads") + if loaded[&"ok"]: + expect(loaded[&"manifest"]["format_version"] == 2 and loaded[&"manifest"]["runtime_schema_version"] == 15, "format2 manifest still exactly15") + expect(runtime.restore_snapshot(loaded[&"snapshot"]), "MIMI package restore reaches same staged section validator") + var bad := snapshot.duplicate(true) + bad[&"systems"][&"runtime"][&"special_peeping_rooms"][&"version"] = 2 + expect(not SaveService.save_snapshot(directory + "_bad", bad)[&"ok"], "save-format adapter rejects malformed optional section") diff --git a/tests/special/test_peeping_room_save.gd b/tests/special/test_peeping_room_save.gd new file mode 100644 index 0000000..a89533f --- /dev/null +++ b/tests/special/test_peeping_room_save.gd @@ -0,0 +1,66 @@ +extends "res://tests/special/peeping_room_fixture.gd" + +func _run() -> void: + var f := fixture() + var runtime: Variant = f[&"runtime"] + var empty: Dictionary = runtime.create_snapshot() + expect(not empty[&"systems"][&"runtime"].has(&"special_peeping_rooms"), "empty service does not alter existing snapshot keys") + expect(runtime.restore_snapshot(empty) and var_to_bytes(empty) == var_to_bytes(runtime.create_snapshot()), "pre-extension v14 absence exact roundtrip") + var legacy := empty.duplicate(true) + legacy[&"systems"][&"runtime"][&"runtime_schema_version"] = 13 + legacy[&"systems"][&"runtime"].erase(&"auto_purchase_plans") + expect(runtime.restore_snapshot(legacy) and runtime.peeping_room_service.is_empty(), "genuine v13 absent section migrates empty") + expect(runtime.restore_snapshot(empty), "restore canonical baseline before controlled room") + var room := place(f) + enable_fixture(f, room) + expect(runtime.peeping_room_service.controlled_fixture_record_evidence(room, &"fixture.save", f[&"clock"].absolute_minute)[&"ok"], "controlled evidence before mid-state save") + expect(runtime.submit_command(&"peeping_room_relink", {&"room_id": room, &"target_instance_id": f[&"target"]}, &"player", room, 0, f[&"clock"].absolute_minute + 1).is_success(), "future explicit relink remains pending at checkpoint") + var snapshot: Dictionary = runtime.create_snapshot() + expect(snapshot[&"systems"][&"runtime"][&"runtime_schema_version"] == 14, "extension never bumps Runtime version") + expect(runtime.restore_snapshot(snapshot) and var_to_bytes(snapshot) == var_to_bytes(runtime.create_snapshot()), "exact typed room/evidence whole Runtime roundtrip") + var directory := "user://tests/p13_reference_save_%d" % OS.get_process_id() + var saved := SaveService.save_snapshot(directory, snapshot) + expect(saved[&"ok"], "actual SaveService writes section in format 1") + var loaded := SaveService.load_snapshot(directory) + expect(loaded[&"ok"], "actual SaveService reads section") + if loaded[&"ok"]: + expect(loaded[&"manifest"]["format_version"] == 1, "existing format mapping unchanged") + expect(runtime.restore_snapshot(loaded[&"snapshot"]), "loaded physical package reaches Runtime validation") + expect(var_to_bytes(snapshot) == var_to_bytes(runtime.create_snapshot()), "physical save retains all typed fields") + for mutation in range(18): + var bad := snapshot.duplicate(true) + var data: Dictionary = bad[&"systems"][&"runtime"] + var section: Dictionary = data[&"special_peeping_rooms"] + match mutation: + 0: data.erase(&"special_peeping_rooms") + 1: data[&"special_peeping_rooms"] = null + 2: section[&"version"] = 2 + 3: section[&"version"] = 1.0 + 4: section[&"unknown"] = true + 5: section.erase(&"evidence") + 6: section[&"rooms"].append(section[&"rooms"][0].duplicate(true)) + 7: section[&"evidence"].append(section[&"evidence"][0].duplicate(true)) + 8: section[&"rooms"][0][&"target_instance_id"] = &"missing.target" + 9: section[&"rooms"][0][&"target_definition_id"] = &"adult_private_room" + 10: section[&"rooms"][0][&"origin"] = Vector3i(8, 1, 1) + 11: section[&"rooms"][0][&"stopped"] = 1 + 12: section[&"evidence"][0][&"room_id"] = &"peeping_room.999999999999" + 13: section[&"evidence"][0][&"source_event_key"] = &"aliased.event" + 14: section[&"evidence"][0][&"minute"] = f[&"clock"].absolute_minute + 1 + 15: data[&"id_generator"]["counters"]["peeping_room"] = 0 + 16: + section.erase(&"version") + section["version"] = 1 + 17: + section[&"rooms"][0][&"link_state"] = &"TARGET_LOST" + section[&"rooms"][0][&"stopped"] = true + section[&"rooms"][0][&"stop_reason"] = &"TARGET_LOST" + expect(rejects_atomically(runtime, bad), "malformed/current missing/reference/duplicate rejection atomic case %d" % mutation) + var orphan := empty.duplicate(true) + orphan[&"systems"][&"runtime"][&"world"] = snapshot[&"systems"][&"runtime"][&"world"].duplicate(true) + expect(rejects_atomically(runtime, orphan), "world-owned room without any section rejects atomically") + var removed_world := snapshot.duplicate(true) + removed_world[&"systems"][&"runtime"][&"world"] = empty[&"systems"][&"runtime"][&"world"].duplicate(true) + expect(rejects_atomically(runtime, removed_world), "section-owned room without world occupancy rejects atomically") + expect(runtime.restore_snapshot(empty) and runtime.peeping_room_service.is_empty(), "absent legacy section clears previous nonempty service on valid load") + finish("Peeping room save") diff --git a/tools/run_p13_reference_tests.py b/tools/run_p13_reference_tests.py new file mode 100644 index 0000000..8beb67b --- /dev/null +++ b/tools/run_p13_reference_tests.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +"""P13 reference evidence: read-only verification by default; collection is Root-only.""" +import argparse +import ast +import inspect +import json +import os +import re +import stat +import types +from pathlib import Path +import shutil +import sys + +sys.dont_write_bytecode = True +import run_all_tests as shared + +PROTOCOL = 'guarded-p13-reference-v1' +CANONICAL_EVIDENCE = 'p13-reference-native-uid-corrected' +NEW_TESTS = [ + 'tests/special/test_peeping_room_geometry.gd', + 'tests/special/test_peeping_room_reference.gd', + 'tests/special/test_peeping_room_evidence.gd', + 'tests/special/test_peeping_room_save.gd', + 'tests/special/test_peeping_room_runtime.gd', +] +ROSTER = NEW_TESTS + [ + 'tests/s1/test_construction_service.gd', + 'tests/s1/test_world_service.gd', + 'tests/s1/test_path_service.gd', + 'tests/s1/test_runtime_world_construction.gd', + 'tests/supply/test_supply_save_migration.gd', + 'tests/transport/test_transport_save.gd', + 'tests/environment/test_runtime_weather.gd', + 'tests/s6/test_runtime_snapshot_serialization.gd', + 'tests/s6/test_runtime_save_lifecycle.gd', + 'tests/supply/test_mimi_purchase_quote.gd', +] + + +def uid_plan(refs): + return {name + '.uid': {'owner': name, 'owner_sha256': pin['sha256'], + 'file_type': 'regular'} + for name, pin in sorted(refs.items()) + if name.endswith('.gd') and name + '.uid' not in refs + and (name.startswith('tests/special/') + or name == 'scripts/special/peeping_room_service.gd')} + + +def uid_snapshot(stage, source, plan): + """Accept exactly the source inventory and finite native UID plan.""" + actual = shared.inventory(stage, stage=True) + shared.require(set(actual) == set(source) | set(plan), 'exact source and planned UID set') + shared.require({n: actual[n] for n in source} == source, 'original source/settings unchanged') + seen = set() + for name in source: + if name.endswith('.uid'): + raw = (stage / name).read_bytes().rstrip(b'\r\n') + shared.require(raw not in seen, 'existing project UID collision') + seen.add(raw) + result = {} + for name, binding in plan.items(): + path = stage / name + shared.require(stat.S_ISREG(path.lstat().st_mode) and not path.is_symlink() + and path.stat().st_nlink == 1, 'ordinary generated UID') + raw = path.read_bytes() + shared.require(re.fullmatch(rb'uid://[a-z0-9]{1,13}\n', raw) is not None, + 'complete canonical UID bytes') + shared.require(raw.rstrip(b'\n') not in seen, 'generated UID collision') + seen.add(raw.rstrip(b'\n')) + shared.require(stat.S_ISREG((stage / binding['owner']).lstat().st_mode) + and shared.sha(stage / binding['owner']) == binding['owner_sha256'], + 'UID owner content binding') + result[name] = dict(binding, bytes=len(raw), raw_bytes=list(raw), + sha256=shared.sha(path), text=raw.decode('ascii')) + return result + + +def install_uid_policy(namespace): + """Specialize private globals; shared and P18 functions remain untouched.""" + original_collect, original_seal = namespace['collect'], namespace['seal'] + state = {} + + def reports(stage, expected, previous, requested): + shared.require(not requested and not previous, 'P13 has no generated reports') + snapshot = uid_snapshot(stage, state['source'], state['plan']) + if state['accepted'] is None: + shared.require(state['phase'] == 'import', 'UID generation only during import') + state['accepted'] = snapshot + expected.update({n: {'bytes': p['bytes'], 'sha256': p['sha256']} + for n, p in snapshot.items()}) + shared.require(snapshot == state['accepted'], 'sealed UID bytes unchanged') + return shared.declared_reports(stage, expected, previous, requested) + + phase_globals = dict(vars(shared), declared_reports=reports) + execute = types.FunctionType(shared.execute_phase.__code__, phase_globals) + + def phase(*args): + stage, out, label = args[1:4] + state['phase'] = label + if label == 'import': + shared.require(shared.inventory(stage, stage=True) == state['source'], + 'original stage before import') + before = {} + shared.write(out / 'uid-plan.json', state['plan']) + else: + before = uid_snapshot(stage, state['source'], state['plan']) + shared.require(before == state['accepted'], 'UID set before phase unchanged') + row = execute(*args) + after = uid_snapshot(stage, state['source'], state['plan']) + shared.require(after == state['accepted'], 'UID set after phase unchanged') + shared.write(out / (label + '-uids.json'), {'before': before, 'after': after}) + return row + + namespace['shared'] = types.SimpleNamespace(**dict(vars(shared), execute_phase=phase)) + + def collect(args, root, out, refs, *rest): + state.update(source=namespace['content_map'](refs), plan=uid_plan(refs), accepted=None) + return original_collect(args, root, out, refs, *rest) + + def seal(out, refs, engine, contract_sha): + shared.write(out / 'accepted-uids.json', state['accepted']) + return original_seal(out, refs, engine, contract_sha) + + def retained(stage, source, out): + plan = namespace['read_json'](out / 'uid-plan.json') + shared.require(plan == uid_plan(source), 'independently derived exact UID plan') + accepted = uid_snapshot(stage, source, plan) + shared.require(namespace['read_json'](out / 'accepted-uids.json') == accepted, + 'retained raw UID bytes and owner bindings') + for label in ['import', 'parse'] + [str(i).zfill(3) for i in range(len(ROSTER))]: + shared.require(namespace['read_json'](out / (label + '-uids.json')) == + {'before': {} if label == 'import' else accepted, 'after': accepted}, + 'all phase UID before/after seals') + return True + + namespace.update(collect=collect, seal=seal, uid_retained=retained) + + +def protocol_core(): + """Reuse P18's complete guarded core without editing or copying its runner. + + P18 predates configurable protocol/roster constants. Specialize its three + entry functions' identity literals and explicit roster cardinalities, plus + one retained-stage comparison for the finite UID policy. The local phase + adapter retains every original shared gate and seals its accepted sidecars. + Fail closed if an upstream function changes any of these adapter sites. + Both runner sources remain bound by the whole selected source map. + """ + core = shared.module('p13_p18_core', shared.HERE / 'run_p18_relationship_tests.py') + namespace = dict(vars(core), ROSTER=ROSTER, __file__=__file__) + replacements = { + 'guarded-p18-v2': PROTOCOL, + 'tools/run_p18_relationship_tests.py': 'tools/run_p13_reference_tests.py', + 'exact seven unique tests': 'exact unique P13 roster', + 'exact nine phases': 'exact P13 phase count', + 'all nine captured engines': 'all P13 captured engines', + } + cardinalities = { + 'len(set(ROSTER)) == 7': 'len(set(ROSTER)) == len(ROSTER)', + 'len(phases) == 9': 'len(phases) == len(ROSTER) + 2', + "len(tests) == 7": 'len(tests) == len(ROSTER)', + } + + class Specialize(ast.NodeTransformer): + def __init__(self): + self.identities = 0 + self.counts = 0 + self.summary = 0 + + def visit_Constant(self, node): + if isinstance(node.value, str) and node.value in replacements: + self.identities += 1 + return ast.copy_location(ast.Constant(replacements[node.value]), node) + return node + + def visit_Compare(self, node): + expression = ast.unparse(node) + if expression in cardinalities: + self.counts += 1 + return ast.copy_location(ast.parse(cardinalities[expression], mode='eval').body, node) + return self.generic_visit(node) + + def visit_Dict(self, node): + for index, key in enumerate(node.keys): + if isinstance(key, ast.Constant) and key.value == 'actual_suites': + shared.require(isinstance(node.values[index], ast.Constant) + and node.values[index].value == 7, 'upstream suite summary unchanged') + node.values[index] = ast.parse('len(ROSTER)', mode='eval').body + self.summary += 1 + return self.generic_visit(node) + + expected = {'collect': (2, 0, 0), 'seal': (1, 0, 0), 'verify': (6, 3, 1)} + for name, counts in expected.items(): + adapter = Specialize() + tree = adapter.visit(ast.parse(inspect.getsource(getattr(core, name)))) + shared.require((adapter.identities, adapter.counts, adapter.summary) == counts, + 'unchanged upstream adaptation sites: ' + name) + if name == 'verify': + old = "shared.inventory(confined(out, 'stage'), stage=True) == source" + matches = [node for node in ast.walk(tree) + if isinstance(node, ast.Compare) and ast.unparse(node) == old] + shared.require(len(matches) == 1, 'unchanged retained-stage UID adapter site') + + class Retained(ast.NodeTransformer): + def visit_Compare(self, node): + if ast.unparse(node) == old: + return ast.copy_location(ast.parse( + "uid_retained(confined(out, 'stage'), source, out)", mode='eval').body, node) + return self.generic_visit(node) + + tree = Retained().visit(tree) + exec(compile(ast.fix_missing_locations(tree), str(shared.HERE / 'run_p18_relationship_tests.py'), 'exec'), namespace) + install_uid_policy(namespace) + return namespace + + +def main(argv=None): + argv = list(sys.argv[1:] if argv is None else argv) + parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False) + parser.add_argument('--output') + modes = parser.add_mutually_exclusive_group() + modes.add_argument('--collect', action='store_true', help='Root only; fresh native collection') + modes.add_argument('--prepare-only', action='store_true') + seen, index = set(), 0 + while index < len(argv): + flag = argv[index] + shared.require(flag in ('--output', '--collect', '--prepare-only', '--help') + and flag not in seen, 'unknown, duplicate or malformed flag') + seen.add(flag) + if flag == '--output': + index += 1 + shared.require(index < len(argv) and argv[index] and not argv[index].startswith('--'), 'output value required') + index += 1 + args = parser.parse_args(argv) + shared.require(not args.collect or args.output is not None, 'Root collection requires explicit --output') + if args.output is None: + base = os.environ.get('BUILD_OUTPUT_DIR') + shared.require(bool(base) and Path(base).is_absolute(), 'absolute BUILD_OUTPUT_DIR required') + args.output = str(Path(base) / CANONICAL_EVIDENCE) + args.timeout = 1200 # unchanged guarded core timeout; not a product value + core = protocol_core() + root = shared.HERE.parent + out = core['plain_path'](args.output) + refs = shared.source_refs(root) + shared.require(len(set(ROSTER)) == len(ROSTER) and set(ROSTER).issubset(shared.roster(root)), 'complete unique discovered roster') + markers = shared.module('p13_exact_markers', shared.HERE / 'strict_test_markers.py') + contract_path = root / 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json' + contract_sha = shared.sha(contract_path) + contracts = markers.load_contracts(contract_path) + shared.require(all(name in contracts for name in ROSTER), 'complete marker roster') + policy = shared.read(shared.SUPPORT / 'suite-policy.json') + shared.policy_coverage(ROSTER, refs, policy) + for test in ROSTER: + shared.phase_policy(test, Path(refs[test]['path']), policy['suite_policies']) + for test in NEW_TESTS: + shared.require(contracts[test]['source_sha256_at_schema_audit'] == refs[test]['sha256'], 'new strict source-hashed marker') + parse_paths = ['res://' + name for name in sorted(refs) if name.endswith('.gd')] + if args.prepare_only: + ancestor = out.parent + while not ancestor.exists(): + ancestor = ancestor.parent + free = shutil.disk_usage(ancestor).free + size = sum(pin['bytes'] for pin in refs.values()) + print(json.dumps(dict(protocol=PROTOCOL, roster=ROSTER, source_files=len(refs), + source_bytes=size, free_bytes=free, conservative_required_bytes=size + shared.MIN_SPACE, + capacity_available=free >= size + shared.MIN_SPACE, parse_paths=len(parse_paths), + future_native_phases=len(ROSTER) + 2, actual_checks=None, engines=0, stages=0, + formal_acceptance=False, original_unit_complete=False))) + return 0 + if args.collect: + shared.require(not out.exists() and out.parent.is_dir(), 'fresh output with existing parent required') + shared.require(not out.is_relative_to(root), 'private collection outside source tree') + return core['collect'](args, root, out, refs, markers, contracts, policy, contract_path, contract_sha, parse_paths) + return core['verify'](out, refs, markers, contracts, policy, contract_sha, parse_paths) + + +if __name__ == '__main__': + try: + raise SystemExit(main()) + except (AssertionError, OSError, ValueError, KeyError, TypeError) as error: + print(type(error).__name__ + ': ' + str(error), file=sys.stderr) + raise SystemExit(1)