P08-02: 旧配置の安全な変更

This commit is contained in:
Agent Control Runner
2026-10-13 20:05:01 +09:00
parent e196578c1a
commit 6c7c724410
12 changed files with 18616 additions and 0 deletions
@@ -3668,6 +3668,17 @@
"text": "\tprint(\"TOMARI_FLOOR_TROUBLE checks=%d failures=%d\" % [checks,failures])"
}
]
},
{
"test": "tests/s1/test_p08_combined_migration.gd",
"template": "P08 combined migration: <checks> checks, <failures> failures",
"required": true,
"fixed_check_count_used": false,
"source_sha256_at_schema_audit": "9284a8cfb478b4c8d69f6d8166eec6c41107e3902029289b3c0e9ba48c370855",
"authority": {
"path": "docs/spec/acceptance/P08-02.json",
"scope": "new standalone mixed suite; native execution pending"
}
}
],
"unknown_roster_suite_policy": "refuse, explicitly review its source/schema before accepting",
@@ -0,0 +1,35 @@
# P08-02 retained implementation condition matrix
Status: submitted for Root verification; original P08-02 acceptance remains pending.
Frozen spec: `docs/spec/acceptance/P08-02.json`, stripped UTF-8 SHA-256
`19f02bb7912a4d7354e565186f02366fc1d4dd698468d26142e8204fd14ebfb0`.
The raw file has a trailing newline. The six rows are facets of one existing unit, not six units.
All rows use `tests/fixtures/p08-02/mixed_world.gd` through `writer.gd` and
`reader.gd`, and the standalone `tests/s1/test_p08_combined_migration.gd`.
Driver phases are `off-positive`, `off-refusal`, `on-positive`, `on-refusal`,
each with `-writer` then `-reader`; import/parse precede them.
| Required condition | Retained fixture/test coverage | Driver phase | Checker rule (`tools/check_p08_migration_evidence.py`) | Negative mutation (`tools/test_p08_migration_evidence.py`) | Pending evidence |
|---|---|---|---|---|---|
| mixed_construction | `setup`: two paid old-size premium jobs plus completed dorm; `run`: progress/reservations and completion shape | All four pairs; positive applies batch, refusal continues old shape | `semantics` checks active paid old-size jobs and completed dorm; `case_gate` permits only explicit typed geometry deltas and requires four-cell positive jobs | `partial_batch`; unapproved typed changes are compared without dropping fields | Native runtime/save and distinct-PID continuation; normal OS flow; Root final verification pending |
| occupancy_change | `invariant`: restore validation of ownership/support; migrated occupied/reserved cells captured in whole snapshots | All four pairs | `semantics`: disjoint cells, exact construction/world ownership and operational facility cells; `GEOMETRY` finite allowlist | `orphan_occupancy` | Native added/released cells and support validation; OS model/formal records; Root review pending |
| actual_actor_positions | Mid-route walker, fractional credit, real staff/rescue/cargo and EV positions; no route changes approved | All four pairs, checkpoint and minutes 001–160 | `typed_differences` forbids actor/credit/route/money deltas; full typed and raw Variant writer/reader equality | `duplicate_actor`, `unequal_cross_pid` | Native actual positions/credit/routes and cross-PID equality; OS state captures; Root review pending |
| combined_rescue_logistics_ev | `setup`: actual in-flight rescue, purchased carried food with original requests, four EV boarded plus one waiting; `run`: patient treated, original cargo completed, each EV arrival once | All four pairs and uncapped events | `semantics`: IDs, endpoints, reservations and EV actors; `case_gate`: final patient health, original cargo completion and exactly one arrival per original EV request | `lost_patient`, `lost_cargo`, `lost_ev` | Native coherent same-save mixed state and continuation; OS save/model attribution; Root review pending |
| no_overlap_orphan_loss | Whole typed snapshots/raw Variant files and uncapped events; runtime/cargo invariants | ON and OFF positive/refusal pairs | Ordered four-case roster, actual mode, distinct lifecycle PIDs, strict markers, all 162 labels compared, semantic ownership/reference checks | `duplicate_actor`, `orphan_occupancy`, `unequal_cross_pid`, `missing_on`, `missing_phase`, `duplicate_pid`, `wrong_source`, `stale_source` | Native ON/OFF and separate writer/reader PIDs; normal-menu OS proof; final regression and Root verification pending |
| unsolvable_preservation | Real pre-existing migration backup, late maintenance obstruction at dorm, independently migratable first job; refusal rollback and unchanged live/formal/backup | `off-refusal`, `on-refusal` writer/reader | Live equality; original equals refusal checkpoint; before/after member hash maps equal; raw four-member original/result/backup hashes bound | `partial_batch`, `overwritten_original`, `overwritten_backup` | Native late refusal and separate-PID original continuation; OS cancel/changed-source preservation; Root review pending |
The standalone suite executes both roles in one process and does not establish distinct-PID proof.
Only the Root collector launches separate phases through existing `execute_phase`/owned-outer
and fresh guarded profiles. Exactly one public suite marker exists:
`P08 combined migration: <checks> checks, <failures> failures`.
The driver privately owns the writer/reader marker templates; no second public entry is added.
Local Python tests are synthetic checker fixtures, including synthetic lifecycle/OS/regression
records; their success establishes checker reachability and rejection tests only.
Native Godot/OS execution, normal production menu preview/cancel/confirm/changed-source rejection,
final full regression, final submitted reviewer-packet measurement, applicable Windows coverage,
independent Opus review, integration/current verification and ledger closure remain Root conditions.
Controlled old-save setup must be labelled controlled, never natural production.
Checker integrity success cannot close the original unit. No production defect or product fix
was demonstrated by the authorized local checks; retained code and marker contract are reused.
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
# P08-02 diagnostic fixture
Controlled fixture inputs are diagnostic, not production balance values.
Native collection and acceptance remain pending.
+210
View File
@@ -0,0 +1,210 @@
extends RefCounted
## All seeds, actor IDs, positions, food quantities and schedules are diagnostic inputs.
var checks := 0
var failures := 0
var runtime: Variant
var clock: Variant
var state: Variant
var directory: String
var mode: String
var slot: String
var serial := 0
var events: Array = []
var identity: Dictionary = {}
const MEMBERS := [SaveService.WORLD_FILE, SaveService.MANIFEST_FILE, SaveService.THUMBNAIL_FILE, SaveService.JOURNAL_FILE]
func expect(ok: bool, label: String) -> bool:
checks += 1
if not ok:
failures += 1
push_error("P08 mixed: " + label)
return ok
func command(kind: StringName, payload: Dictionary) -> bool:
serial += 1
var result: CommandResult = runtime.submit_command(kind, payload, &"player", &"", 0, -1, StringName("p08.%d" % serial))
return expect(result.is_success(), String(kind) + ": " + result.message)
func registry(old: bool) -> void:
var paths: Array[String] = FacilityCatalog.FACILITY_PATHS + FacilityCatalog.INFRASTRUCTURE_FACILITY_PATHS + FacilityCatalog.RESCUE_FACILITY_PATHS
paths.append_array(["res://tests/fixtures/legacy_premium_room.tres", "res://tests/fixtures/legacy_staff_dormitory.tres"] if old else ["res://data/facilities/expansion/premium_room.tres", "res://data/facilities/expansion/staff_dormitory.tres"])
expect(runtime.definition_registry.rebuild_from_paths(paths)[&"valid"], "definition registry")
func build(id: StringName, cell: Vector3i, complete := true) -> bool:
if not command(&"build_facility", {&"definition_id":id, &"origin":cell}): return false
if complete: clock.advance_minutes(runtime.definition_registry.get_facility(id).build_minutes)
return true
func instance(id: StringName) -> FacilityInstance:
for room: FacilityInstance in runtime.facility_service.get_instances():
if room.definition_id == id: return room
return null
## Scalar text avoids loss of integer precision; raw Variant bytes accompany JSON.
func typed(value: Variant) -> Dictionary:
var result := {"type":type_string(typeof(value))}
if value is Dictionary:
result["entries"] = []
for key: Variant in value: result["entries"].append([typed(key),typed(value[key])])
elif value is Array:
result["element_type"] = value.get_typed_builtin()
result["element_class"] = String(value.get_typed_class_name())
result["items"] = []
for item: Variant in value: result["items"].append(typed(item))
else: result["text"] = var_to_str(value)
return result
func put(name: String, value: Variant) -> void:
var path := directory.path_join(name)
if not expect(not FileAccess.file_exists(path+".var") and not FileAccess.file_exists(path+".json"), "fresh proof "+name): return
var file := FileAccess.open(path+".var",FileAccess.WRITE)
if not expect(file != null,"Variant output"): return
file.store_buffer(var_to_bytes(value)); file.close()
file = FileAccess.open(path+".json",FileAccess.WRITE)
if not expect(file != null,"typed output"): return
file.store_string(JSON.stringify(typed(value))); file.close()
func get_proof(name: String) -> Variant:
return bytes_to_var(FileAccess.get_file_as_bytes(directory.path_join(name+".var")))
func capture(name: String, value: Variant) -> void:
put(mode+"-"+name,value)
if mode == "reader": expect(var_to_bytes(value) == var_to_bytes(get_proof("writer-"+name)),"whole cross-PID "+name)
func copy_package(source: String, destination: String) -> void:
expect(DirAccess.make_dir_recursive_absolute(destination) == OK,"package directory")
for member: String in MEMBERS:
var data := FileAccess.get_file_as_bytes(source.path_join(member))
if not expect(not data.is_empty() and not FileAccess.file_exists(destination.path_join(member)),"fresh complete member "+member): continue
var file := FileAccess.open(destination.path_join(member),FileAccess.WRITE)
if not expect(file != null,"member writable"): continue
file.store_buffer(data); file.close()
expect(data == FileAccess.get_file_as_bytes(destination.path_join(member)),"whole member copy verified")
func packages() -> Dictionary:
var hashes := {}
for suffix: String in ["",SaveService.MIGRATION_BACKUP_SUFFIX]:
for member: String in MEMBERS: hashes[suffix+"/"+member] = FileAccess.get_sha256(ProjectSettings.globalize_path(slot+suffix).path_join(member))
return hashes
func observe(event: SimulationDomainEvent) -> void: events.append(event.to_dictionary())
func setup(operations: bool) -> bool:
registry(true)
if operations and not expect(runtime.enable_production_operations().get(&"ok",false),"Operations enabled at Day 1 06:00"): return false
for row: Array in [[&"warehouse",Vector3i(-8,0,1)],[&"kitchen",Vector3i(2,0,1)],[&"infirmary",Vector3i(6,0,1)],[&"staff_door",Vector3i(0,0,1)],[&"reception",Vector3i(-6,0,0)],[&"reception",Vector3i(5,0,0)],[&"guest_elevator",Vector3i.ZERO]]:
if not build(row[0],row[1],false): return false
clock.advance_minutes(180)
for y: int in [1,2,3]:
if not build(&"service_stairs",Vector3i(-9,y,1),false) or not build(&"guest_stairs",Vector3i(4,y,0),false): return false
clock.advance_minutes(runtime.definition_registry.get_facility(&"guest_stairs").build_minutes)
if not build(&"staff_dormitory",Vector3i(-8,2,1)): return false
identity[&"dorm"] = instance(&"staff_dormitory").instance_id
if not build(&"premium_room",Vector3i(-6,2,0),false) or not build(&"premium_room",Vector3i(5,2,0),false): return false
clock.advance_minutes(7)
identity[&"jobs"] = []
for job: ConstructionJob in runtime.construction_service.get_jobs():
if not job.is_terminal(): identity[&"jobs"].append(job.job_id)
if not expect(identity[&"jobs"].size() == 2,"two paid active jobs"): return false
var warehouse := instance(&"warehouse")
var kitchen := instance(&"kitchen")
if not expect(warehouse != null and kitchen != null,"completed logistics endpoints"): return false
if not command(&"hire_staff_candidate",{&"candidate_id":&"staff_gina"}): return false
if not command(&"set_staff_work_assignment",{&"staff_id":&"staff_gina",&"facility_id":warehouse.instance_id,&"template_id":&"DAY"}): return false
if not command(&"purchase_supplies",{&"warehouse_id":warehouse.instance_id,&"item_id":SupplyCatalog.RAW_FOOD,&"quantity":8}): return false
if not command(&"set_kitchen_production",{&"facility_id":kitchen.instance_id,&"enabled":true,&"target_stock":4}): return false
var cargo_ready := false
for minute: int in range(60):
clock.advance_minutes(1)
if not runtime.staff_service.get_staff(&"staff_gina").carried_inventory_id.is_empty(): cargo_ready = true; break
if not expect(cargo_ready,"real purchased cargo picked up"): return false
for id: StringName in [&"patient",&"carrier"]:
if not command(&"register_staff",{&"staff_id":id,&"role_id":&"porter",&"options":{&"current_cell":Vector3i(-3,0,1) if id == &"patient" else Vector3i(-1,0,1),&"health":0 if id == &"patient" else 100,&"state":StaffRuntime.State.AVAILABLE,&"role_tags":[&"medic"],&"permitted_task_tags":[&"rescue"]}}): return false
var carrying := false
for minute: int in range(6):
clock.advance_minutes(1)
for task: StaffTask in runtime.staff_service.get_tasks():
if task.task_type == &"rescue_transport" and task.state == StaffTask.State.MOVING:
carrying = true; identity[&"rescue_task"] = task.task_id
if carrying: break
if not expect(carrying,"actual rescue transport"): return false
for index: int in range(5):
if not command(&"spawn_guest",{&"guest_id":StringName("p08.passenger.%d" % index),&"guest_type_id":&"traveler",&"gold":100,&"options":{&"current_cell":Vector3i.ZERO,&"state":GuestRuntime.State.MOVING,&"state_entered_at":clock.absolute_minute-1,&"planned_exit_at":clock.absolute_minute+2000,&"path_handle":{&"target_cell":Vector3i(0,1,0)}}}): return false
clock.advance_minutes(1)
if not command(&"spawn_guest",{&"guest_id":&"p08.walker",&"guest_type_id":&"traveler",&"gold":100,&"options":{&"current_cell":Vector3i(4,0,0),&"state":GuestRuntime.State.MOVING,&"state_entered_at":clock.absolute_minute-1,&"planned_exit_at":clock.absolute_minute+2000,&"path_handle":{&"target_cell":Vector3i(4,3,0)}}}): return false
clock.advance_minutes(1)
identity[&"ev_requests"] = []
for trip: Dictionary in runtime.elevators.get_report()[&"trips"]: identity[&"ev_requests"].append(trip[&"request_id"])
identity[&"logistics_requests"] = []
for request: LogisticsRequest in runtime.logistics_requests.values():
if not request.is_terminal(): identity[&"logistics_requests"].append(request.request_id)
expect(not runtime.staff_service.get_staff(&"staff_gina").carried_inventory_id.is_empty(),"cargo in SAME save as patient and EV")
expect(runtime.staff_service.get_staff(&"patient").health == 0,"patient remains untreated")
var car: Dictionary = runtime.elevators.get_report()[&"shafts"][0]
expect(car[&"passenger_count"] == 4 and car[&"waiting_count"] == 1,"passengers and queue coexist")
expect(runtime.create_snapshot()[&"systems"][&"runtime"][&"movement_credits"].size() > 0,"fractional movement credit")
for id: StringName in identity[&"jobs"]:
var job: ConstructionJob = runtime.construction_service.get_job(id)
expect(not job.is_terminal() and job.progress > 0 and job.reserved_cells.size() == 6,"old-size paid jobs remain active")
return failures == 0
func invariant() -> void:
expect(runtime.staff_service.validate_invariants()[&"ok"],"staff ownership")
expect(SupplyLogisticsBridge.validate_crosslinks(runtime.logistics_requests,runtime.staff_service,runtime.inventory_service,runtime.facility_service),"cargo crosslinks")
var snap: Dictionary = runtime.create_snapshot()
expect(runtime.restore_snapshot(snap),"whole Runtime ownership/support/transport validation")
expect(var_to_bytes(snap) == var_to_bytes(runtime.create_snapshot()),"whole canonical typed state")
func run(tree: SceneTree, output: String, role: String, operations: bool, refusal: bool) -> Dictionary:
directory = output; mode = role
slot = "user://p08-%d/formal" % Time.get_ticks_usec()
tree.root.get_node("GameClock").set_paused(true); tree.root.get_node("GameClock").set_process(false)
tree.root.get_node("SimulationRuntime").configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false})
runtime = load("res://autoload/simulation_runtime.gd").new()
clock = load("res://autoload/game_clock.gd").new(); state = load("res://autoload/game_state.gd").new()
clock.reset(1,6,0); state.reset(); expect(runtime.initialize(clock,state,42588)[&"valid"],"current initialization")
clock.set_paused(true); runtime.configure_save_policy({&"enabled":false,&"daily_checkpoint_enabled":false,&"auto_migrate":false})
if mode == "writer":
expect(DirAccess.make_dir_recursive_absolute(directory) == OK,"proof directory")
if not setup(operations): return finish()
registry(false)
var baseline: Dictionary = runtime.create_snapshot()
expect(SaveService.save_snapshot(slot,baseline)[&"ok"],"formal original saved")
if refusal:
var initial: Dictionary = runtime.migrate_facility_dimensions_at_path(slot)
expect(initial.get(&"ok",false) and initial.get(&"backup_created",false),"real prior migration backup")
expect(runtime.staff_service.create_task(&"maintenance",10,clock.absolute_minute,{&"target_entity_id":identity[&"dorm"],&"target_cell":Vector3i(-9,3,1),&"estimated_minutes":30},&"p08.late").is_success(),"later old entrance task")
if not command(&"register_staff",{&"staff_id":&"maintainer",&"role_id":&"porter",&"options":{&"current_cell":Vector3i(-9,1,1),&"state":StaffRuntime.State.AVAILABLE}}): return finish()
expect(runtime.staff_service.assign_task_to_staff(&"p08.late",&"maintainer",clock.absolute_minute).is_success(),"real task ownership")
expect(SaveService.save_snapshot(slot,runtime.create_snapshot())[&"ok"],"obstructed source saved")
put("identity",identity); put("original",runtime.create_snapshot())
copy_package(ProjectSettings.globalize_path(slot),directory.path_join("original/formal"))
var live: Dictionary = runtime.create_snapshot()
var before := packages(); put("before-members",before)
var first: Dictionary = runtime.prepare_facility_dimension_snapshot(identity[&"jobs"][0])
expect(first.get(&"ok",false),"earlier candidate independently migratable")
put("first-candidate",first)
var preview: Dictionary = runtime.preview_facility_dimensions_at_path(slot); put("preview",preview)
var result: Dictionary = runtime.migrate_facility_dimensions_at_path(slot,&"",String(preview.get(&"source_digest",""))); put("migration",result)
expect(var_to_bytes(live) == var_to_bytes(runtime.create_snapshot()),"live unchanged")
put("after-live",runtime.create_snapshot())
if refusal:
expect(not result.get(&"ok",true) and result.get(&"details",{}).get(&"rolled_back",false) and result.get(&"details",{}).get(&"facility_id",&"") == identity[&"dorm"],"later refusal rolls back earlier candidates")
expect(before == packages(),"original and existing backup unchanged")
else: expect(preview.get(&"ok",false) and result.get(&"ok",false) and result.get(&"backup_created",false),"positive whole batch")
put("after-members",packages())
copy_package(ProjectSettings.globalize_path(slot),directory.path_join("result/formal"))
copy_package(ProjectSettings.globalize_path(slot+SaveService.MIGRATION_BACKUP_SUFFIX),directory.path_join("backup/formal"))
else:
registry(false); identity = get_proof("identity")
copy_package(directory.path_join("result/formal"),ProjectSettings.globalize_path(slot))
copy_package(directory.path_join("backup/formal"),ProjectSettings.globalize_path(slot+SaveService.MIGRATION_BACKUP_SUFFIX))
var loaded := SaveService.load_snapshot(slot)
expect(loaded.get(&"ok",false) and runtime.restore_snapshot(loaded.get(&"snapshot",{})),"formal continuation restore")
expect(runtime.get_operations_report().get(&"enabled",false) == operations,"actual Operations mode")
invariant(); capture("checkpoint",runtime.create_snapshot())
expect(runtime.domain_events.subscribe(&"*",&"p08.uncapped",Callable(self,"observe")),"uncapped events")
for minute: int in range(160):
clock.advance_minutes(1); capture("minute-%03d" % (minute+1),runtime.create_snapshot())
capture("events",events)
for id: StringName in identity[&"jobs"]:
var job: ConstructionJob = runtime.construction_service.get_job(id)
expect(job.state == ConstructionJob.State.COMPLETED and runtime.facility_service.get_instance(job.facility_instance_id).occupied_cells.size() == (6 if refusal else 4),"paid job completes once with expected shape")
expect(runtime.staff_service.get_staff(&"patient").health == 100,"patient treated")
for id: StringName in identity[&"logistics_requests"]: expect(runtime.logistics_requests[id].state == LogisticsRequest.State.COMPLETED,"original cargo delivered")
for id: StringName in identity[&"ev_requests"]:
var count := 0
for event: Dictionary in events:
if event["event_type"] == "ELEVATOR_ARRIVED" and event["payload"].get(&"request_id",&"") == id: count += 1
expect(count == 1,"original EV request completes exactly once")
invariant()
put(mode+"-process",{&"pid":OS.get_process_id(),&"operations":operations,&"refusal":refusal,&"checks":checks,&"failures":failures})
return finish()
func finish() -> Dictionary:
runtime.free(); clock.free(); state.free()
return {"checks":checks,"failures":failures}
+7
View File
@@ -0,0 +1,7 @@
extends SceneTree
func _initialize() -> void: call_deferred("run")
func run() -> void:
var fixture := preload("res://tests/fixtures/p08-02/mixed_world.gd").new()
var result: Dictionary = fixture.run(self,OS.get_environment("P08_PAIR_ROOT"),"reader",OS.get_environment("P08_OPERATIONS") == "on",OS.get_environment("P08_CASE") == "refusal")
print("P08 mixed reader: %d checks, %d failures" % [result.checks,result.failures])
quit(0 if result.failures == 0 else 1)
+7
View File
@@ -0,0 +1,7 @@
extends SceneTree
func _initialize() -> void: call_deferred("run")
func run() -> void:
var fixture := preload("res://tests/fixtures/p08-02/mixed_world.gd").new()
var result: Dictionary = fixture.run(self,OS.get_environment("P08_PAIR_ROOT"),"writer",OS.get_environment("P08_OPERATIONS") == "on",OS.get_environment("P08_CASE") == "refusal")
print("P08 mixed writer: %d checks, %d failures" % [result.checks,result.failures])
quit(0 if result.failures == 0 else 1)
+15
View File
@@ -0,0 +1,15 @@
extends SceneTree
func _initialize() -> void: call_deferred("run")
func run() -> void:
var checks := 0
var failures := 0
for operations: bool in [false,true]:
for refusal: bool in [false,true]:
var output := ProjectSettings.globalize_path("user://p08-suite-%d-%s-%s" % [Time.get_ticks_usec(),operations,refusal])
for role: String in ["writer","reader"]:
var fixture := preload("res://tests/fixtures/p08-02/mixed_world.gd").new()
var result: Dictionary = fixture.run(self,output,role,operations,refusal)
checks += result.checks; failures += result.failures
if result.failures > 0: break
print("P08 combined migration: %d checks, %d failures" % [checks,failures])
quit(0 if failures == 0 else 1)
@@ -0,0 +1 @@
uid://bq08mixed02wld
+295
View File
@@ -0,0 +1,295 @@
#!/usr/bin/env python3
"""Read-only artifact integrity gate. Success NEVER accepts original P08-02."""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import re
import sys
sys.dont_write_bytecode = True
HERE = Path(__file__).resolve().parent
CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
SPEC_SHA = '19f02bb7912a4d7354e565186f02366fc1d4dd698468d26142e8204fd14ebfb0'
CONDITIONS = ('mixed_construction','occupancy_change','actual_actor_positions',
'combined_rescue_logistics_ev','no_overlap_orphan_loss','unsolvable_preservation')
MEMBERS = ('world.bin','manifest.json','thumbnail.png','journal.log')
CASES = ('off-positive','off-refusal','on-positive','on-refusal')
LABELS = ['checkpoint'] + ['minute-%03d' % n for n in range(1,161)] + ['events']
def sha(path): return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def load(path): return json.loads(Path(path).read_bytes())
def module(name, path):
spec = importlib.util.spec_from_file_location(name,path)
obj = importlib.util.module_from_spec(spec); spec.loader.exec_module(obj); return obj
def decode(node):
"""Only decode for semantic checks; whole typed nodes remain comparison authority."""
t = node['type']
if t == 'Dictionary':
pairs = [(decode(k),decode(v)) for k,v in node['entries']]
keys = [json.dumps(k,sort_keys=True) for k,v in pairs]
if len(keys) != len(set(keys)): raise ValueError('duplicate typed dictionary key')
return {k:v for k,v in pairs}
if t == 'Array': return [decode(n) for n in node['items']]
text = node['text']
if t in ('String','StringName'): return json.loads(text.removeprefix('&'))
if t == 'int': return int(text)
if t == 'float': return float(text)
if t == 'bool':
if text not in ('true','false'): raise ValueError('invalid bool')
return text == 'true'
if t == 'Nil': return None
if t in ('Vector2i','Vector3i','Vector2','Vector3'):
return [float(x.strip()) if '.' in x else int(x.strip()) for x in text.strip('()').split(',')]
# Preserve unfamiliar scalar types verbatim; never silently drop them.
return (t,text)
def indexed(rows, key):
result = {row[key]:row for row in rows}
if len(result) != len(rows): raise ValueError('duplicate '+key)
return result
def semantics(snapshot, identity, initial=False):
"""Independent ID/cell/reference checks on actual serialized Runtime state."""
r = snapshot['systems']['runtime']
guests = indexed(r['guests']['guests'],'guest_id')
staff = indexed(r['staff']['staff'],'staff_id')
if set(guests) & set(staff): raise ValueError('duplicate actor across domains')
jobs = indexed(r['construction']['jobs'],'job_id')
facilities = indexed(r['construction']['facilities'],'instance_id')
operational = indexed(r['facilities']['instances'],'instance_id')
owners = {k:v['occupied_cells'] for k,v in facilities.items()}
owners.update({k:v['reserved_cells'] for k,v in jobs.items() if v['state'] not in ('COMPLETED','CANCELLED')})
occupied = indexed(r['world']['occupancies'],'entity_id')
cells = set()
for owner,row in occupied.items():
actual = {tuple(c) for c in row['cells']}
if len(actual) != len(row['cells']) or cells & actual: raise ValueError('overlap occupancy')
cells |= actual
if owner not in owners or actual != {tuple(c) for c in owners[owner]}: raise ValueError('orphan occupancy/ownership')
if set(owners) != set(occupied): raise ValueError('lost occupancy owner')
for fid,facility in operational.items():
if fid not in facilities or facility['occupied_cells'] != facilities[fid]['occupied_cells']:
raise ValueError('operational/construction ownership mismatch')
for jid in identity['jobs']:
if jid not in jobs: raise ValueError('lost paid job')
if initial and (jobs[jid]['progress'] <= 0 or jobs[jid]['state'] != 'BUILDING' or len(jobs[jid]['reserved_cells']) != 6):
raise ValueError('missing paid old-size progress/reservation')
if len(identity['jobs']) < 2 or identity['dorm'] not in facilities: raise ValueError('mixed jobs/completed facility missing')
tasks = indexed(r['staff']['tasks'],'task_id')
if 'patient' not in staff or 'carrier' not in staff: raise ValueError('lost patient/carrier')
rescue = tasks[identity['rescue_task']]
if rescue['target_entity_id'] != 'patient' or rescue['rescue_facility_id'] not in facilities:
raise ValueError('rescue patient/destination reference')
if initial and (rescue['state'] != 'MOVING' or rescue['assigned_staff_id'] != 'carrier' or staff['patient']['health'] != 0):
raise ValueError('rescue is not actually in flight')
inventories = indexed(r['inventory']['inventories'],'owner_inventory_id')
requests = indexed(r['logistics_requests'],'request_id')
reservations = indexed(r['inventory']['reservations'],'reservation_id')
if not identity['logistics_requests']: raise ValueError('missing original food request')
for rid in identity['logistics_requests']:
req = requests[rid]
if req['source_inventory_id'] not in inventories or req['destination_inventory_id'] not in inventories:
raise ValueError('orphan cargo endpoint')
if initial:
carrier = staff[req['assigned_carrier_id']]
cargo = inventories[carrier['carried_inventory_id']]
if not cargo['stacks'] or not req['reservation_ids'] or any(x not in reservations for x in req['reservation_ids']):
raise ValueError('lost cargo/reservation')
trips = indexed(r['elevators']['trips'],'request_id')
actors = set(guests) | set(staff)
for trip in trips.values():
actor = trip['actor_id']
if actor not in actors and actor.removeprefix('guest:').removeprefix('staff:') not in actors:
raise ValueError('orphan EV actor')
if initial:
if len(identity['ev_requests']) != 5 or not set(identity['ev_requests']) <= set(trips): raise ValueError('lost EV request')
if sum(len(c['boarded']) for c in r['elevators']['shafts']) != 4: raise ValueError('EV passengers/queue absent')
if 'p08.walker' not in guests or not r['movement_credits']: raise ValueError('mid-route guest/credit absent')
return r
def typed_differences(a, b, path=()):
if a == b: return []
if a.get('type') != b.get('type'): return [path]
if a['type'] == 'Dictionary':
aa = {str(decode(k)):(k,v) for k,v in a['entries']}; bb = {str(decode(k)):(k,v) for k,v in b['entries']}
if aa.keys() != bb.keys(): return [path]
differences = []
for k in aa:
if aa[k][0] != bb[k][0]: differences.append(path+(k,'@key-type'))
differences += typed_differences(aa[k][1],bb[k][1],path+(k,))
return differences
if a['type'] == 'Array' and a.get('element_type') == b.get('element_type') and a.get('element_class') == b.get('element_class'):
if len(a['items']) != len(b['items']): return [path]
return [p for i,(x,y) in enumerate(zip(a['items'],b['items'])) for p in typed_differences(x,y,path+(str(i),))]
return [path]
# Explicit geometry changes only. No actor, economy, request, event or route wildcard.
# This fixture deliberately places actors away from removed cells: NO route changes approved.
GEOMETRY = [
r'systems/runtime/world/occupancies/\d+/cells(?:/.*)?',
r'systems/runtime/world/dirty_cells(?:/.*)?',
r'systems/runtime/world/revisions/(world|occupancy)',
r'systems/runtime/construction/jobs/\d+/reserved_cells(?:/.*)?',
r'systems/runtime/construction/facilities/\d+/occupied_cells(?:/.*)?',
r'systems/runtime/facilities/instances/\d+/occupied_cells(?:/.*)?',
]
def check(project_root, evidence_dir):
root, evidence = Path(project_root).resolve(), Path(evidence_dir).resolve()
deficits = []
def need(ok, reason):
if not ok: deficits.append(reason)
def guarded(label, fn):
try: return fn()
except (KeyError,ValueError,TypeError,OSError,IndexError,AttributeError) as exc:
deficits.append(label+': '+str(exc)); return None
index = guarded('index',lambda:load(evidence/'index.json'))
if not isinstance(index,dict): return deficits or ['index is not an object']
def artifact(name):
if not isinstance(name,str) or Path(name).is_absolute() or '..' in Path(name).parts: raise ValueError('unsafe artifact path')
path = evidence/name
if not path.is_file() or any(p.is_symlink() for p in [path,*path.parents] if p != evidence.parent): raise ValueError('missing/aliased artifact '+name)
pin = index['artifacts'][name]
if pin != {'bytes':path.stat().st_size,'sha256':sha(path)}: raise ValueError('artifact hash '+name)
return path
def document(name): return load(artifact(name))
need(index.get('schema') == 'p08-evidence-v1','schema')
need(index.get('acceptance') == 'pending','integrity is not unit acceptance')
need(index.get('collection_error') is None,'collection failure')
def source_gate():
runner = module('p08_readonly_source',root/'tools/run_all_tests.py')
actual = {n:p['sha256'] for n,p in runner.source_refs(root).items()}
need(index['source_map'] == actual,'wrong/stale/incomplete final source map')
spec = load(root/'docs/spec/acceptance/P08-02.json')
need(index['spec_sha256'] == SPEC_SHA == hashlib.sha256((root/'docs/spec/acceptance/P08-02.json').read_text().strip().encode()).hexdigest(),'frozen spec SHA')
for name,pin in spec['current_source_pins'].items(): need(actual.get(name) == pin,'frozen production source '+name)
inputs = {str(p.relative_to(root)):sha(p) for p in (root/'docs/spec/inputs/p08-02').iterdir() if p.is_file()}
need(index['inputs'] == inputs,'frozen input map')
need(index['marker_contract_sha256'] == sha(root/CONTRACT),'marker contract source')
need(re.fullmatch('[0-9a-f]{40}',index['commit']) is not None,'submitted commit identity')
guarded('source',source_gate)
for name in index.get('artifacts',{}): guarded('artifact '+name,lambda name=name:artifact(name))
markers = module('p08_evidence_markers',root/'tools/strict_test_markers.py')
contracts = markers.load_contracts(root/CONTRACT)
pids = set()
def phase(name, role):
receipt = document(name); pid = receipt['actual_pid']
need(type(pid) is int and pid > 0 and pid not in pids,'distinct native PID '+name); pids.add(pid)
parent = str(Path(name).parent)
prefix = '' if parent == '.' else parent+'/'
folder = prefix+receipt['phase']+'/'
start,end,outer = [document(folder+n) for n in ('started.json','ended.json','outer-owned-finally.json')]
for row in (start,end):
need(row['actual_pid'] == pid and row['command'] == receipt['command'],'raw lifecycle PID/argv '+name)
need(end.get('exit_code') == 0 and end.get('engine_ended') is True,'ended exit '+name)
need(start.get('live_identity_exact') is True or start.get('tracked_child_ended_before_initial_probe') is True,'native Popen identity '+name)
need(outer['actual_captured_pids'] == [pid] and outer['all_captured_ended'] is True and not outer['outer_cleanup_errors'] and outer['foreign_process_or_group_signals'] == 0,'owned cleanup '+name)
native = receipt['native_process']
need(native['exit_code'] == 0 and native['helper_exit_code'] == 0 and native['timed_out'] is False and native['lifecycle_error'] is None,'native exit/errors '+name)
need(receipt['exit_code'] == 0 and receipt['engine_ended'] is True,'phase ended '+name)
if role:
test = 'tests/fixtures/p08-02/'+role+'.gd'
need(receipt['test'] == test and receipt['source_sha256'] == index['source_map'][test],'phase source '+name)
raw = artifact(prefix+receipt['log']).read_text()
parsed = markers.parse_raw(test,raw,{test:{'template':'P08 mixed '+role+': <checks> checks, <failures> failures'}},receipt['source_sha256'])
need(not parsed['errors'],'strict completion '+name+': '+str(parsed['errors']))
profile = receipt['actual_private_profile']['report']
need(profile['fresh_before_parent_initialize'] is True and profile['profile_writes_before_guard'] == 0 and profile['actual_user_data_dir'] == profile['expected_user_data_dir'],'guarded fresh profile '+name)
return receipt
def case_gate(case):
name = case['id']; prefix = case['directory']+'/'
need(name == case['operations']+'-'+case['kind'],'case identity '+name)
need(len(case['phases']) == 2,'writer and reader phases '+name)
receipts = [phase(n,r) for n,r in zip(case['phases'],('writer','reader'))]
def typed(name):
artifact(prefix+name+'.var')
return document(prefix+name+'.json')
identity = decode(typed('identity'))
original = typed('original'); after_live = typed('after-live')
need(original == after_live,'live partial batch '+name)
before_members,after_members = [decode(typed(n)) for n in ('before-members','after-members')]
for package in ('original','result','backup'):
for member in MEMBERS: artifact(prefix+package+'/formal/'+member)
for member in MEMBERS:
need(before_members['/'+member] == sha(artifact(prefix+'original/formal/'+member)),'original overwritten '+name+'/'+member)
need(after_members['/'+member] == sha(artifact(prefix+'result/formal/'+member)),'result member '+name+'/'+member)
need(after_members['.pre_migration.bak/'+member] == sha(artifact(prefix+'backup/formal/'+member)),'backup overwritten '+name+'/'+member)
refusal = case['kind'] == 'refusal'
if refusal: need(before_members == after_members,'refusal original/backup overwritten '+name)
source = semantics(decode(original),identity,True)
need(source['operations']['enabled'] == (case['operations']=='on'),'missing actual Operations mode '+name)
for role,receipt in zip(('writer','reader'),receipts):
proc = decode(typed(role+'-process'))
need(proc['pid'] == receipt['actual_pid'] and proc['failures'] == 0 and proc['operations'] == (case['operations']=='on') and proc['refusal'] == refusal,'fixture/native binding '+name+'/'+role)
checkpoint = typed('writer-checkpoint')
if refusal: need(original == checkpoint,'partial earlier candidate '+name)
else:
for path in typed_differences(original,checkpoint):
need(any(re.fullmatch(pattern,'/'.join(path)) for pattern in GEOMETRY),'unapproved typed migration delta '+name+'/'+ '/'.join(path))
migrated = decode(checkpoint)['systems']['runtime']
jobs = indexed(migrated['construction']['jobs'],'job_id')
need(all(len(jobs[j]['reserved_cells']) == 4 for j in identity['jobs']),'positive batch not applied '+name)
for label in LABELS:
writer,reader = typed('writer-'+label),typed('reader-'+label)
need(writer == reader,'cross-PID full typed state/events '+name+'/'+label)
need(sha(artifact(prefix+'writer-'+label+'.var')) == sha(artifact(prefix+'reader-'+label+'.var')),'cross-PID raw Variant '+name+'/'+label)
if label != 'events': semantics(decode(writer),identity)
final = decode(typed('writer-minute-160'))['systems']['runtime']
need(indexed(final['staff']['staff'],'staff_id')['patient']['health'] == 100,'patient lost/untreated '+name)
final_requests = indexed(final['logistics_requests'],'request_id')
need(all(final_requests[i]['state']=='COMPLETED' for i in identity['logistics_requests']),'original cargo completion '+name)
events = decode(typed('writer-events'))
for rid in identity['ev_requests']:
need(sum(e['event_type']=='ELEVATOR_ARRIVED' and e['payload'].get('request_id')==rid for e in events)==1,'EV original request exactly once '+name+'/'+rid)
cases = index.get('cases',[])
need([c.get('id') for c in cases] == list(CASES),'missing/duplicate ON/OFF positive/refusal case')
for case in cases: guarded('case '+str(case.get('id')),lambda case=case:case_gate(case))
for condition in CONDITIONS:
expected = [c for c in CASES if condition != 'unsolvable_preservation' or c.endswith('refusal')]
need(index.get('conditions',{}).get(condition) == expected,'condition mapping '+condition)
def os_gate():
proof = document(index['os_proof'])
need(proof['commit'] == index['commit'] and proof['source_map'] == index['source_map'],'OS current candidate')
need(proof['input_method'] == 'native_os' and proof['setup'] == 'controlled_old_save','OS input provenance/setup')
actions = proof['actions']
need([a['action'] for a in actions] == ['preview','cancel','confirm','changed_source_rejection'],'OS actions missing')
for action in actions:
raw = document(action['input_receipt'])
need(raw['method'] == 'native_os' and raw['event_type'] in ('mouse','keyboard') and type(raw['target_pid']) is int and raw['target_pid'] > 0 and raw['timestamp_ns'] > 0,'fake OS signal')
for key in ('screen_before','screen_after','model_before','model_after','formal_before','formal_after'): artifact(action[key])
if action['action'] in ('preview','cancel','changed_source_rejection'):
need(document(action['formal_before']) == document(action['formal_after']),'OS formal preservation '+action['action'])
guarded('OS proof',os_gate)
def regression_gate():
proof = document(index['regression']); run_input = document(proof['input']); ended = document(proof['ended'])
need(run_input['selected_source_map'] == index['source_map'],'final regression source map')
need(proof['commit'] == index['commit'],'final regression commit')
need(not ended['error'] and not ended['cleanup_errors'] and not ended['guard_errors'],'regression failure')
runner = module('p08_regression_roster',root/'tools/run_all_tests.py')
roster = runner.roster(root)
need(run_input['roster'] == roster,'full final roster')
rows = [x for x in ended['phases'] if x.get('test')]
need([x['test'] for x in rows] == roster,'full final regression receipt')
for row in rows:
raw = artifact(proof['directory']+'/'+row['log']).read_text()
need(not markers.parse_raw(row['test'],raw,contracts,index['source_map'][row['test']])['errors'],'regression strict '+row['test'])
need(row['exit_code'] == 0 and row['engine_ended'] is True,'regression native exit '+row['test'])
guarded('final regression',regression_gate)
return sorted(set(deficits))
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--verify-only',action='store_true',required=True)
parser.add_argument('--project-root',type=Path,default=HERE.parent)
args = parser.parse_args(argv)
build = os.environ.get('BUILD_OUTPUT_DIR')
deficits = ['BUILD_OUTPUT_DIR missing'] if not build else check(args.project_root,Path(build)/'p08-evidence')
print(json.dumps({'integrity':'deficient' if deficits else 'verified','deficits':deficits,
'acceptance':'pending','conditions':{c:'pending' for c in CONDITIONS}},ensure_ascii=False,indent=2))
return 1 if deficits else 0
if __name__ == '__main__': raise SystemExit(main())
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""Root-only native collection. Does not run regression or claim acceptance."""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import time
sys.dont_write_bytecode = True
HERE = Path(__file__).resolve().parent
CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
CONDITIONS = ('mixed_construction', 'occupancy_change', 'actual_actor_positions',
'combined_rescue_logistics_ev', 'no_overlap_orphan_loss', 'unsolvable_preservation')
def module(name, path):
spec = importlib.util.spec_from_file_location(name, path)
obj = importlib.util.module_from_spec(spec)
spec.loader.exec_module(obj)
return obj
def absolute(value):
path = Path(value)
if not path.is_absolute():
raise argparse.ArgumentTypeError('absolute path required')
return path.resolve()
def collect(args):
root, out = args.project_root, args.output
r = module('p08_standard_runner', root/'tools/run_all_tests.py')
commit = subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip()
r.require(commit == args.commit, 'actual final submitted commit required')
r.require(not out.exists(), 'fresh output; never overwrite earlier proof')
refs = r.source_refs(root)
outer = r.module('p08_owned_outer', r.SUPPORT/'owned_outer.py')
r.require(not outer.native.helper['live_engines'](), 'one native engine')
engine = shutil.which(args.godot)
r.require(engine is not None, 'native engine exists')
engine = str(Path(engine).resolve())
out.mkdir(parents=True)
stage = out/'stage'; stage.mkdir()
(out/'starters').mkdir(); (out/'bootstrap').mkdir()
for name, pin in r.read(r.SUPPORT/'bootstrap-scripts.json').items():
data = pin['text'].encode()
r.require(hashlib.sha256(data).hexdigest() == pin['sha256'], 'bootstrap pin')
(out/'bootstrap'/name).write_bytes(data)
for name, pin in refs.items():
target = stage/name; target.parent.mkdir(parents=True, exist_ok=True)
r.copy_source(Path(pin['path']), target)
r.require(r.sha(target) == pin['sha256'], 'whole copied source '+name)
expected = {n: {'bytes':v['bytes'], 'sha256':v['sha256']} for n,v in refs.items()}
selected = {'root_before':refs, 'map':{n:v['sha256'] for n,v in refs.items()}, 'timeout':180}
r.require(r.inventory(stage, stage=True) == expected, 'complete private source')
protected = r.default_player(root); public = (stage/'project.godot').read_bytes()
paths = ['res://'+n for n in sorted(refs) if n.endswith('.gd')]
r.write(out/'parse-paths.json', paths)
markers = r.module('p08_strict_markers', root/'tools/strict_test_markers.py')
contracts = markers.load_contracts(root/CONTRACT)
for role in ('writer','reader'):
name = 'tests/fixtures/p08-02/'+role+'.gd'
contracts[name] = {'test':name, 'template':'P08 mixed '+role+': <checks> checks, <failures> failures', 'required':True}
policy = r.read(r.SUPPORT/'suite-policy.json')
diagnostics = r.module('p08_diagnostics', r.SUPPORT/'diagnostic_gate.py')
owners = r.module('p08_owners', r.SUPPORT/'ordinary_proof_gate.py')
audio = r.module('p08_audio', r.SUPPORT/'audio_evidence_gate.py')
r.write(out/'runner-input.json', {'protocol':'p08-mixed-v1', 'commit':commit,
'selected_source_map':selected['map'], 'engine':engine, 'engine_sha256':r.sha(engine),
'owner_pid':os.getpid(), 'default_player':protected, 'diagnostic_seed':42588})
phases = []; previous = {}; cases = []; error = None
deadline = time.monotonic()+900
saved_env = {k:os.environ.get(k) for k in ('P08_PAIR_ROOT','P08_OPERATIONS','P08_CASE')}
def phase(label, test):
remaining = int(deadline-time.monotonic())
r.require(remaining >= (180 if label == 'import' else 1), '900s collection deadline')
selected['timeout'] = min(180,remaining)
phase_out = out/'runs'/label
phase_out.mkdir(parents=True)
(phase_out/'starters').mkdir(); (phase_out/'bootstrap').mkdir()
for original in (out/'bootstrap').iterdir():
r.copy_source(original,phase_out/'bootstrap'/original.name)
r.require(r.sha(original)==r.sha(phase_out/'bootstrap'/original.name),'bootstrap full copy')
r.write(phase_out/'parse-paths.json',paths)
row = r.execute_phase(engine, stage, phase_out, label, test, selected, expected, previous,
policy, contracts, paths, public, protected, root, outer, markers, diagnostics, owners, audio)
phases.append('runs/'+label+'/'+label+'-receipt.json')
r.require(not row['failed'], 'strict phase failed: '+label)
try:
phase('import', None); phase('parse', None)
for operations in ('off','on'):
for case in ('positive','refusal'):
name = operations+'-'+case; pair = out/'pairs'/name
os.environ.update(P08_PAIR_ROOT=str(pair), P08_OPERATIONS=operations, P08_CASE=case)
case_phases = []
preserved = {}
for role in ('writer','reader'):
label = name+'-'+role
test = 'tests/fixtures/p08-02/'+role+'.gd'
phase(label, test)
actual = r.inventory(pair)
r.require(all(actual.get(n) == pin for n,pin in preserved.items()), 'reader preserved every writer byte')
preserved = actual
case_phases.append(phases[-1])
cases.append({'id':name, 'operations':operations, 'kind':case,
'directory':'pairs/'+name, 'phases':case_phases, 'files':preserved})
except BaseException as caught:
error = type(caught).__name__+': '+str(caught)
finally:
cleanup, cleanup_errors = outer.finish_captured(outer.OWNED)
for key,value in saved_env.items():
if value is None: os.environ.pop(key, None)
else: os.environ[key] = value
r.require(r.source_refs(root) == refs and r.default_player(root) == protected, 'protected root/profile unchanged')
r.write(out/'runner-ended.json', {'error':error, 'cleanup':cleanup, 'cleanup_errors':cleanup_errors,
'phases':phases, 'formal_acceptance':False, 'native_Windows':'pending'})
inputs = {str(p.relative_to(root)):r.sha(p) for p in (root/'docs/spec/inputs/p08-02').iterdir() if p.is_file()}
# The collector is intentionally incomplete until Root supplies OS and regression artifacts.
r.write(out/'index.json', {'schema':'p08-evidence-v1', 'commit':commit,
'source_map':selected['map'], 'inputs':inputs, 'marker_contract_sha256':r.sha(root/CONTRACT),
'spec_sha256':hashlib.sha256((root/'docs/spec/acceptance/P08-02.json').read_text().strip().encode()).hexdigest(),
'cases':cases, 'phases':phases, 'conditions':{c:[x['id'] for x in cases if c != 'unsolvable_preservation' or x['kind']=='refusal'] for c in CONDITIONS},
'artifacts':{str(p.relative_to(out)):{'bytes':p.stat().st_size,'sha256':r.sha(p)}
for p in out.rglob('*') if p.is_file() and 'stage' not in p.relative_to(out).parts},
'os_proof':None, 'regression':None,
'acceptance':'pending', 'collection_error':error})
return 1 if error or cleanup_errors else 0
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--project-root', type=absolute, required=True)
parser.add_argument('--output', type=absolute, required=True)
parser.add_argument('--commit', required=True)
parser.add_argument('--godot', default='godot')
parser.add_argument('--collect', action='store_true', required=True)
return collect(parser.parse_args(argv))
if __name__ == '__main__':
raise SystemExit(main())
+187
View File
@@ -0,0 +1,187 @@
#!/usr/bin/env python3
"""Synthetic evidence ONLY: disposable end-to-end check() positives and corruptions.
No Godot/OS execution, no actual acceptance proof. Mutations update hashes so the
semantic gate, not merely a stale digest, must reject invalid state.
"""
import copy
import hashlib
import json
from pathlib import Path
import tempfile
import unittest
import check_p08_migration_evidence as c
ROOT = Path(__file__).resolve().parent.parent
BANNER = 'Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org\n'
def typed(value):
if isinstance(value,dict): return {'type':'Dictionary','entries':[[typed(k),typed(v)] for k,v in value.items()]}
if isinstance(value,list): return {'type':'Array','element_type':0,'element_class':'','items':[typed(x) for x in value]}
kind = 'Nil' if value is None else 'bool' if type(value) is bool else 'int' if type(value) is int else 'String'
return {'type':kind,'text':json.dumps(value)}
class Proof:
def __init__(self, path):
self.path = path
runner = c.module('p08_synthetic_source',ROOT/'tools/run_all_tests.py')
self.runner = runner
source = {n:p['sha256'] for n,p in runner.source_refs(ROOT).items()}
self.index = {'schema':'p08-evidence-v1','commit':'e196578c1a4ad2fdf84642565bd102e524be386f',
'source_map':source,'spec_sha256':c.SPEC_SHA,'marker_contract_sha256':c.sha(ROOT/c.CONTRACT),
'inputs':{str(p.relative_to(ROOT)):c.sha(p) for p in (ROOT/'docs/spec/inputs/p08-02').iterdir() if p.is_file()},
'cases':[],'conditions':{k:[x for x in c.CASES if k!='unsolvable_preservation' or x.endswith('refusal')] for k in c.CONDITIONS},
'artifacts':{},'acceptance':'pending','collection_error':None,'phases':[],
'os_proof':'os.json','regression':'regression.json'}
for number,name in enumerate(c.CASES): self.case(name,number)
self.os(); self.regression(); self.save()
def put(self,name,value):
path = self.path/name; path.parent.mkdir(parents=True,exist_ok=True)
path.write_bytes(value if isinstance(value,bytes) else json.dumps(value).encode())
self.index['artifacts'][name] = {'bytes':path.stat().st_size,'sha256':c.sha(path)}
def variant(self,prefix,name,value):
node = typed(value)
self.put(prefix+name+'.json',node)
# Deliberately not a native Variant: this is an isolated Python proof fixture.
self.put(prefix+name+'.var',json.dumps(node).encode())
def save(self): (self.path/'index.json').write_text(json.dumps(self.index))
def state(self,operations):
jobs = [{'job_id':j,'state':'BUILDING','progress':466,'reserved_cells':[[x,2,0] for x in range(offset,offset+6)],'construction_cost':{'gold':1}}
for j,offset in [('j1',0),('j2',10)]]
facilities = [{'instance_id':'dorm','occupied_cells':[[30,0,1]]},{'instance_id':'clinic','occupied_cells':[[31,0,1]]}]
guests = [{'guest_id':g} for g in ['p08.walker']+['g'+str(n) for n in range(5)]]
staff = [{'staff_id':'patient','health':0},{'staff_id':'carrier'},{'staff_id':'staff_gina','carried_inventory_id':'cargo'}]
r = {'operations':{'enabled':operations},'guests':{'guests':guests},'staff':{'staff':staff,'tasks':[
{'task_id':'rescue','target_entity_id':'patient','rescue_facility_id':'clinic','state':'MOVING','assigned_staff_id':'carrier'}]},
'construction':{'jobs':jobs,'facilities':facilities},'facilities':{'instances':copy.deepcopy(facilities)},
'world':{'occupancies':[{'entity_id':j['job_id'],'cells':copy.deepcopy(j['reserved_cells'])} for j in jobs]+
[{'entity_id':f['instance_id'],'cells':copy.deepcopy(f['occupied_cells'])} for f in facilities]},
'inventory':{'inventories':[{'owner_inventory_id':x,'stacks':[{'item_id':'ingredient/raw_food','quantity':4}]} for x in ('source','dest','cargo')],
'reservations':[{'reservation_id':'reserve'}]},
'logistics_requests':[{'request_id':'food','source_inventory_id':'source','destination_inventory_id':'dest','assigned_carrier_id':'staff_gina','reservation_ids':['reserve'],'state':'IN_TRANSIT'}],
'movement_credits':[{'credit':1000}],
'elevators':{'trips':[{'request_id':'ev'+str(n),'actor_id':'g'+str(n)} for n in range(5)],'shafts':[{'boarded':['g0','g1','g2','g3']}]}}
return {'systems':{'runtime':r}}
def phase(self,name,role,pid):
folder = 'runs/'+name+'/'; test = 'tests/fixtures/p08-02/'+role+'.gd'
command = ['synthetic-native','--script',test]
start = {'actual_pid':pid,'command':command,'live_identity_exact':True,'event':'ENGINE_STARTED','owner_pid':1,'monotonic_ns':1}
end = dict(start,event='ENGINE_ENDED',monotonic_ns=2,exit_code=0,engine_ended=True)
self.put(folder+name+'/started.json',start); self.put(folder+name+'/ended.json',end)
self.put(folder+name+'/outer-owned-finally.json',{'actual_captured_pids':[pid],'all_captured_ended':True,'outer_cleanup_errors':[],'foreign_process_or_group_signals':0})
self.put(folder+'raw.log',(BANNER+'P08 mixed '+role+': 1 checks, 0 failures\n').encode())
receipt = {'phase':name,'actual_pid':pid,'command':command,'exit_code':0,'engine_ended':True,'test':test,
'source_sha256':self.index['source_map'][test],'log':'raw.log',
'native_process':{'exit_code':0,'helper_exit_code':0,'timed_out':False,'lifecycle_error':None},
'actual_private_profile':{'report':{'fresh_before_parent_initialize':True,'profile_writes_before_guard':0,'actual_user_data_dir':'diagnostic-private-'+name,'expected_user_data_dir':'diagnostic-private-'+name}}}
self.put(folder+name+'-receipt.json',receipt)
return folder+name+'-receipt.json'
def case(self,name,number):
operations = name.startswith('on-'); refusal = name.endswith('refusal'); prefix = 'pairs/'+name+'/'
phases = [self.phase(name+'-'+role,role,100+2*number+i) for i,role in enumerate(('writer','reader'))]
self.index['cases'].append({'id':name,'operations':'on' if operations else 'off','kind':'refusal' if refusal else 'positive','directory':prefix[:-1],'phases':phases})
self.index['phases'] += phases
identity = {'jobs':['j1','j2'],'dorm':'dorm','rescue_task':'rescue','logistics_requests':['food'],'ev_requests':['ev'+str(n) for n in range(5)]}
original = self.state(operations)
self.variant(prefix,'identity',identity); self.variant(prefix,'original',original); self.variant(prefix,'after-live',original)
before = {}; after = {}
for member in c.MEMBERS:
raw = ('synthetic '+member).encode()
for package in ('original','result','backup'): self.put(prefix+package+'/formal/'+member,raw)
before['/'+member] = after['/'+member] = hashlib.sha256(raw).hexdigest()
before['.pre_migration.bak/'+member] = after['.pre_migration.bak/'+member] = hashlib.sha256(raw).hexdigest()
self.variant(prefix,'before-members',before); self.variant(prefix,'after-members',after)
checkpoint = copy.deepcopy(original)
if not refusal:
r = checkpoint['systems']['runtime']
for job in r['construction']['jobs']: job['reserved_cells'] = job['reserved_cells'][:4]
for row in r['world']['occupancies'][:2]: row['cells'] = row['cells'][:4]
events = [{'event_type':'ELEVATOR_ARRIVED','payload':{'request_id':rid}} for rid in identity['ev_requests']]
for i,role in enumerate(('writer','reader')):
self.variant(prefix,role+'-process',{'pid':100+2*number+i,'failures':0,'checks':1,'operations':operations,'refusal':refusal})
for label in c.LABELS:
state = copy.deepcopy(checkpoint)
if label == 'minute-160':
state['systems']['runtime']['staff']['staff'][0]['health'] = 100
state['systems']['runtime']['logistics_requests'][0]['state'] = 'COMPLETED'
self.variant(prefix,role+'-'+label,events if label=='events' else state)
def os(self):
proof = {'commit':self.index['commit'],'source_map':self.index['source_map'],'input_method':'native_os','setup':'controlled_old_save','actions':[]}
for name in ('preview','cancel','confirm','changed_source_rejection'):
row = {'action':name,'input_receipt':'os/'+name+'/input.json'}
self.put(row['input_receipt'],{'method':'native_os','event_type':'mouse','target_pid':500,'timestamp_ns':1})
for field in ('screen_before','screen_after','model_before','model_after','formal_before','formal_after'):
row[field] = 'os/'+name+'/'+field+'.json'; self.put(row[field],{'synthetic':'diagnostic'})
proof['actions'].append(row)
self.put('os.json',proof)
def regression(self):
contracts = c.module('p08_synthetic_markers',ROOT/'tools/strict_test_markers.py').load_contracts(ROOT/c.CONTRACT)
roster = self.runner.roster(ROOT); rows = []
for i,test in enumerate(roster):
contract = contracts[test]
def marker(template):
for key in ('checks','passed'): template = template.replace('<'+key+'>','1')
for key in ('failures','failed','skipped'): template = template.replace('<'+key+'>','0')
return template+'\n'
raw = BANNER+marker(contract['template'])
for auxiliary in contract.get('auxiliary',[]): raw += marker(auxiliary['template'])
if contract.get('json_report'):
raw += contract['json_report']['prefix']+json.dumps({'check_summary':{'total':1,'passed':1,'failed':0,'skipped':0},'status':'PASS','runtime_errors':[],'unmet_fixture_fields':[]})+'\n'
log = str(i)+'.log'; self.put('regression/'+log,raw.encode())
rows.append({'test':test,'log':log,'exit_code':0,'engine_ended':True})
self.put('regression/input.json',{'selected_source_map':self.index['source_map'],'roster':roster})
self.put('regression/ended.json',{'error':None,'cleanup_errors':[],'guard_errors':[],'phases':rows})
self.put('regression.json',{'commit':self.index['commit'],'directory':'regression','input':'regression/input.json','ended':'regression/ended.json'})
class EvidenceTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.temp = tempfile.TemporaryDirectory(prefix='p08-checker-')
cls.proof = Proof(Path(cls.temp.name))
cls.baseline = copy.deepcopy(cls.proof.index)
@classmethod
def tearDownClass(cls): cls.temp.cleanup()
def test_positive_reaches_whole_check(self):
self.assertEqual(c.check(ROOT,self.proof.path),[])
def test_corruptions_cannot_be_accepted(self):
p = self.proof
mutations = {
'duplicate_actor':lambda r:r['guests']['guests'].append(copy.deepcopy(r['guests']['guests'][0])),
'orphan_occupancy':lambda r:r['world']['occupancies'][0].update(entity_id='orphan'),
'lost_patient':lambda r:r['staff']['staff'].pop(0),
'lost_cargo':lambda r:r['inventory']['inventories'].pop(),
'lost_ev':lambda r:r['elevators']['trips'].pop(),
}
path = 'pairs/off-positive/original.json'
original = (p.path/path).read_bytes()
for label,mutate in mutations.items():
with self.subTest(label=label):
node = c.decode(json.loads(original)); mutate(node['systems']['runtime'])
p.put(path,typed(node)); p.save()
self.assertTrue(c.check(ROOT,p.path),label)
p.put(path,original); p.index = copy.deepcopy(self.baseline); p.save()
paths = [('partial_batch','pairs/off-refusal/after-live.json'),('unequal_cross_pid','pairs/on-positive/reader-checkpoint.json'),
('overwritten_original','pairs/off-refusal/original/formal/world.bin'),('overwritten_backup','pairs/off-refusal/backup/formal/world.bin')]
for label,path in paths:
with self.subTest(label=label):
original = (p.path/path).read_bytes()
p.put(path,b'{}'); p.save(); self.assertTrue(c.check(ROOT,p.path),label)
p.put(path,original); p.index = copy.deepcopy(self.baseline); p.save()
for label in ('missing_on','wrong_source','stale_source','fake_os_signal','missing_phase','duplicate_pid'):
with self.subTest(label=label):
changed = None
if label=='missing_on': p.index['cases'] = p.index['cases'][:2]
elif label=='wrong_source': p.index['source_map']['autoload/simulation_runtime.gd'] = '0'*64
elif label=='stale_source': del p.index['source_map']['tools/check_p08_migration_evidence.py']
elif label=='missing_phase': p.index['cases'][0]['phases'].pop()
elif label=='fake_os_signal':
changed = 'os/preview/input.json'; original = (p.path/changed).read_bytes()
row = json.loads(original); row['method'] = 'signal_emitted'; p.put(changed,row)
else:
changed = p.index['cases'][0]['phases'][1]; original = (p.path/changed).read_bytes()
row = json.loads(original); row['actual_pid'] = 100; p.put(changed,row)
p.save(); self.assertTrue(c.check(ROOT,p.path),label)
if changed: p.put(changed,original)
p.index = copy.deepcopy(self.baseline); p.save()
if __name__ == '__main__': unittest.main()