= self::MAX_ATTEMPTS) { return ABJ_404_Solution_DetachAbAttempt::MODE_DEFAULT; } $pairOrdinal = intdiv($attemptIndex, 2); $position = $attemptIndex % 2; $seedByte = hexdec(substr(md5($assignmentSeed), 0, 2)); $onFirst = (($seedByte + $pairOrdinal) % 2) === 0; if ($position === 1) { $onFirst = !$onFirst; } return $onFirst ? ABJ_404_Solution_DetachAbAttempt::MODE_ON : ABJ_404_Solution_DetachAbAttempt::MODE_OFF; } /** * Create a stable, privacy-safe fingerprint for request-shaping fields. * * @param array $payload */ public static function payloadKey(array $payload): string { ksort($payload); return sha1(serialize($payload)); } /** * Hash a browser session for joins without journaling its raw value. * * The name the diagnostics classes reach for. The formula itself belongs to * ABJ_404_Solution_DetachAbScope, which is what a hashed session identifies; * defining it there and forwarding from here keeps the dependency running * one way (policy depends on the scope, never the reverse) while leaving the * fourteen production callers that only want the hash where they are. */ public static function sessionKey(string $sessionId): string { return ABJ_404_Solution_DetachAbScope::sessionKeyFor($sessionId); } /** * Reserve this workload's next attempt slot and return its assignment. * * A COMMAND, not a query, and named accordingly since 2026-08-30: calling * this consumes one of the six slots the scope will ever have. It was * called resolve(), which reads as a question, and a reader checking "what * mode is this request in?" twice would have silently burned two thirds of * a counterbalanced pair sequence. * * `inert` is positive evidence that the experiment did not run. `default` * means the bounded experiment finished and the ordinary detach path was * restored. Both retain the same journal shape as an active assignment. * * Build channel travels with every decision because an inert stable build * is correct while an inert pre-release build means the experiment failed * to arm. Session key, part, payload key, ordinal, and pair coordinates also * travel with every result so a reader can join it to the exact counter that * produced the slot rather than infer scope from missing fields. * * @return array */ public static function assignNextAttempt(ABJ_404_Solution_DetachAbScope $scope): array { return ABJ_404_Solution_DetachAbResolutionTracer::traceResolution( $scope, static function () use ($scope): array { $buildChannel = ABJ_404_Solution_PluginReleaseChannel::currentChannel(); if (!self::isEnabled()) { return self::inertDecision(false, $buildChannel, $scope); } $attemptIndex = ABJ_404_Solution_DetachAbAttemptCounter::reserveNextSlot($scope); if ($attemptIndex === ABJ_404_Solution_DetachAbAttemptCounter::NO_SLOT) { return self::inertDecision(true, $buildChannel, $scope); } $assignmentSeed = $scope->assignmentSeed(); return array( 'mode' => self::modeForAttempt($attemptIndex, $assignmentSeed), 'attempt_index' => $attemptIndex, 'diagnostic_enabled' => true, 'build_channel' => $buildChannel, 'session_key' => $scope->sessionKey(), 'part' => $scope->part(), 'payload_key' => $scope->payloadKey(), 'ordinal' => $attemptIndex, 'pair_ordinal' => intdiv($attemptIndex, 2), 'pair_position' => $attemptIndex % 2, 'assignment_seed' => $assignmentSeed, ); } ); } /** * @return array */ private static function inertDecision( bool $diagnosticEnabled, string $buildChannel, ABJ_404_Solution_DetachAbScope $scope ): array { return array( 'mode' => ABJ_404_Solution_DetachAbAttempt::MODE_INERT, 'attempt_index' => ABJ_404_Solution_DetachAbAttemptCounter::NO_SLOT, 'diagnostic_enabled' => $diagnosticEnabled, 'build_channel' => $buildChannel, 'session_key' => $scope->sessionKey(), 'part' => $scope->part(), 'payload_key' => $scope->payloadKey(), 'ordinal' => ABJ_404_Solution_DetachAbAttemptCounter::NO_SLOT, 'pair_ordinal' => -1, 'pair_position' => -1, 'assignment_seed' => '', ); } }