utm->$key->weights))===array_keys(get_object_vars($after->utm->$key->weights)),'variant order and keys preserved '.$key);unset($before->utm->$key->weights,$after->utm->$key->weights);} migrationCheck($before==$after,'all remaining JSON values/default types/aliases/unknown groups preserved'); migrationCheck(PercentMigration::patch($migrated)===$migrated,'second run is byte-identical'); foreach(['{"x":0,"y":0}','{"x":100.0,"y":0e0}'] as $weights){$source='{"utm":{"bchoice":{"default":1,"weights":'.$weights.'}}}';migrationCheck(PercentMigration::patch($source)===$source,'all-zero and already-percent tokens preserved');} foreach([['1','3',25,75],['2','1',66.67,33.33],['1e308','1e308',50,50],['1e-300','3e-300',25,75]] as [$x,$y,$a,$b]){$source='{"utm":{"focus":{"weights":{"x":'.$x.',"y":'.$y.'}}}}';$out=Config::decode(PercentMigration::patch($source));migrationCheck((float)$out->utm->focus->weights->x===(float)$a&&(float)$out->utm->focus->weights->y===(float)$b,'safe normalization '.$x.':'.$y);} foreach([ '{"utm":{"reg":{"weights":{"1":1,"2":1,"3":1}}}}', '{"utm":{"reg":{"weights":{"1":"1","2":3}}}}', '{"utm":{"reg":{"weights":{"1":-1,"2":3}}}}', '{"utm":{"reg":{"weights":{"1":1e999,"2":3}}}}', '{"utm":{"reg":{"weights":{"1":1e-999,"2":3}}}}', '{"utm":{"reg":{"weights":{"1":1,"1":3}}}}', '{"utm":{"reg":{"weights":{"1":1,"\\u0031":3}}}}', '{"utm":{"reg":{"weights":{"1":1}}},"utm":{}}', '{"utm":{"reg":{"weights":{"1":1}}},"extra":{"x":1,"x":2}}', '{"utm":{"reg":{"weights":{"1":1}},"registration":{"weights":{"1":1}}}}', '{"utm":{"theme":{"weights":[1,3]}}}', '{"utm":null}', ] as $i=>$source)migrationReject($source,'unsafe/ambiguous input rejected '.$i); echo "Raw percent migration complete\n"; $partRaw='{"metadata":"keep","utm":{"part":{"default":"2","weights":{"1":1,"2":3}}}}'; migrationCheck(PercentMigration::patch($partRaw)===str_replace('{"1":1,"2":3}','{"1":25,"2":75}',$partRaw),'part relative weights normalize only explicit numeric spans');