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

This commit is contained in:
Agent Control Runner
2026-10-13 22:21:16 +09:00
parent 592809bfda
commit 1545bd42d6
3 changed files with 453 additions and 51 deletions
+74 -9
View File
@@ -4,6 +4,7 @@ import argparse
import hashlib
import importlib.util
import json
import math
import os
from pathlib import Path
import re
@@ -43,7 +44,16 @@ def decode(node):
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(',')]
match = re.fullmatch(re.escape(t)+r'\(([^()]*)\)',text)
parts = match.group(1).split(',') if match else []
integer = t.endswith('i')
number = r'[+-]?[0-9]+' if integer else r'[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?'
if len(parts) != int(t[6]) or any(not re.fullmatch(number,x.strip()) for x in parts):
raise ValueError('invalid typed vector '+t)
values = [(int if integer else float)(x.strip()) for x in parts]
if not integer and not all(math.isfinite(x) for x in values):
raise ValueError('invalid typed vector '+t)
return values
# Preserve unfamiliar scalar types verbatim; never silently drop them.
return (t,text)
@@ -101,13 +111,44 @@ def semantics(snapshot, identity, initial=False):
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')
for endpoint in ('source_inventory_id','destination_inventory_id'):
value = req[endpoint]
if not value.startswith('facility.inventory:') or value.removeprefix('facility.inventory:') not in operational:
raise ValueError('orphan cargo endpoint')
state = req['state']
carrier_id = req['assigned_carrier_id']
if state == 'RESERVED' and carrier_id:
raise ValueError('RESERVED carrier contradiction')
if state in ('RESERVED','DELIVERING'):
ids = req['reservation_ids']
if not ids or len(ids) != len(set(ids)) or any(x not in reservations for x in ids):
raise ValueError(state+' missing/duplicate reservation')
totals = {}
for reservation_id in ids:
row = reservations[reservation_id]
if (row['requester_id'] != rid or row['owner_inventory_id'] != req['source_inventory_id']
or row['stack_id'] not in req['reserved_stack_ids']
or row['state'] != ('ACTIVE' if state == 'RESERVED' else 'CONSUMED')
or type(row['quantity']) is not int or row['quantity'] <= 0):
raise ValueError(state+' reservation crosslink mismatch')
totals[row['item_id']] = totals.get(row['item_id'],0)+row['quantity']
if totals != req['item_requirements'] or set(req['reserved_stack_ids']) != {reservations[x]['stack_id'] for x in ids}:
raise ValueError(state+' reservation quantity mismatch')
if state == 'RESERVED':
source_stacks = indexed(inventories.get(req['source_inventory_id'],{}).get('stacks',[]),'stack_id')
for reservation_id in ids:
row = reservations[reservation_id]; stack = source_stacks.get(row['stack_id'],{})
if (stack.get('item_id') != row['item_id'] or stack.get('quantity',0) < row['quantity']
or stack.get('reserved_quantity',0) < row['quantity']):
raise ValueError('RESERVED cargo/reservation mismatch')
else:
carrier = staff.get(carrier_id,{})
cargo = inventories.get(carrier.get('carried_inventory_id'),{})
cargo_totals = {}
for stack in cargo.get('stacks',[]):
cargo_totals[stack['item_id']] = cargo_totals.get(stack['item_id'],0)+stack['quantity']
if not carrier or cargo_totals != totals:
raise ValueError('DELIVERING carrier/cargo mismatch')
trips = indexed(r['elevators']['trips'],'request_id')
actors = set(guests) | set(staff)
for trip in trips.values():
@@ -136,6 +177,27 @@ def typed_differences(a, b, 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]
def migration_revisions(before, after, refusal):
"""WorldService._mark_dirty assigns domain counters the global epoch.
r04: (world, occupancy, support) (144,144,142) -> (150,150,150).
Only an actual occupancy rewrite can justify the support epoch delta.
Whole typed comparison still rejects every other unapproved field.
"""
old, new = before['world']['revisions'], after['world']['revisions']
keys = ('world','occupancy','support')
if any(type(row[k]) is not int or row[k] < 0 for row in (old,new) for k in keys):
raise ValueError('invalid revision epoch')
if refusal:
if any(old[k] != new[k] for k in keys):
raise ValueError('refusal revision changed')
return
changed = before['world']['occupancies'] != after['world']['occupancies']
if (not changed or not old['support'] <= old['world'] or not old['occupancy'] <= old['world']
or new['world'] <= old['world'] or new['occupancy'] != new['world']
or new['support'] != new['world'] or new['support'] < old['support']):
raise ValueError('support epoch/occupancy mismatch')
# 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 = [
@@ -235,10 +297,13 @@ def check(project_root, evidence_dir):
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')
migrated = decode(checkpoint)['systems']['runtime']
migration_revisions(source,migrated,refusal)
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))
support_epoch = path == ('systems','runtime','world','revisions','support')
need(support_epoch or 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)