> Normalized receipts, in the * order the browser reported them. */ public static function parseStepReceipts($raw): array { return ABJ_404_Solution_AjaxCanaryReceiptParser::parse($raw); } /** * A JSON-safe filler payload whose ENCODED size lands as close to * $targetBytes as the envelope overhead allows. The filler is inert, * repeated content -- the ladder measures size and transport behavior, * never anything resembling real redirect/URL data. * * @return array{requestId: string, canaryStep: string, filler: string} */ public static function buildFillerPayload(string $requestId, string $step, int $targetBytes): array { return ABJ_404_Solution_AjaxCanaryPayloadFactory::buildFiller($requestId, $step, $targetBytes); } /** * A matched-size compressible or high-entropy JSON payload for the * geometric size ladder. * * The incompressible body is deterministic, printable SHA-256 output: * no random source can fail, no invalid UTF-8 can break json_encode, and * unlike one repeated digest it does not introduce a short repeating * period that gzip can collapse. Metadata is part of the envelope before * filler length is calculated, so paired variants differ in * compressibility rather than decoded response size. * * @param array{request_id: string, target_bytes: int, variant: string, * rung_percent: int, target_source: string} $options * @return array */ public static function buildPayloadVariant(array $options): array { return ABJ_404_Solution_AjaxCanaryPayloadFactory::buildVariant($options); } /** * The interpretation matrix (matrix coverage req. 7): which single cause * space the ladder's outcomes point to. Pure and side-effect free so the * comparison logic can be tested directly; the AJAX handler journals the * result. * * @param array $observations Client-reported per-step * outcomes, keyed by step id: {ok: bool, bytes?: int, ms?: int, gapMs?: int}. * @param bool $realRequestFailed Whether the real table request that * triggered this ladder run actually failed. Always true in production * (the ladder only ever runs after a real failure); kept as an * explicit parameter rather than a hard-coded assumption so the * content-inspection rule stays honestly conditional and testable. * @return array */ public static function interpretResults(array $observations, bool $realRequestFailed = true): array { $entry = static function (array $obs, string $step): array { $found = $obs[$step] ?? null; return is_array($found) ? $found : array(); }; $ok = static function (array $found): bool { return !empty($found['ok']); }; $staticAsset = $entry($observations, 'static_asset'); $authOnly = $entry($observations, self::STEP_AUTH_ONLY); $postLimiter = $entry($observations, self::STEP_POST_LIMITER); $summary = $entry($observations, self::STEP_SUMMARY); $inert = $entry($observations, self::STEP_INERT); $compressOn = $entry($observations, self::STEP_COMPRESS_ON); $compressOff = $entry($observations, self::STEP_COMPRESS_OFF); $stream = $entry($observations, self::STEP_STREAM); $streamGapMs = isset($stream['gapMs']) && is_numeric($stream['gapMs']) ? (int)$stream['gapMs'] : 0; $concurrent = $entry($observations, self::STEP_CONCURRENT_CONTROL); $samePhaseControlFailed = self::samePhaseControlFailed($concurrent, $realRequestFailed); return array_merge(array( 'browserOrNetworkCausal' => !$ok($staticAsset), 'bootAuthOrDeliveryCausal' => $ok($staticAsset) && !$ok($authOnly), 'limiterCausal' => $ok($authOnly) && !$ok($postLimiter), // req. 7 matrix rule: all server work completes (summary-ok) but // an inert response of the SAME size still fails => the failure // tracks response size/bandwidth/buffering, not query cost. 'sizeOrDeliveryCausal' => !$samePhaseControlFailed && $ok($summary) && !$ok($inert), // Mirror rule: a same-size inert filler succeeds while the real, // content-bearing request failed => something inspects or mangles // the CONTENT (redirect URLs/HTML), not merely its size. 'contentInspectionCausal' => !$samePhaseControlFailed && $ok($inert) && $realRequestFailed, 'samePhaseControlFailed' => $samePhaseControlFailed, 'compressionCausal' => $ok($compressOff) && !$ok($compressOn), 'streamingBufferCausal' => !$ok($stream) && $streamGapMs > 2000, ), self::baselineTrend($observations)); } /** * A positive overlap is required before one failed control can veto later * causal claims. Missing or malformed browser evidence remains unknown. * * @param array $concurrent */ private static function samePhaseControlFailed(array $concurrent, bool $realRequestFailed): bool { $receipt = is_array($concurrent['receipt'] ?? null) ? $concurrent['receipt'] : array(); $overlap = is_array($concurrent['overlap'] ?? null) ? $concurrent['overlap'] : array(); $tableOutcome = is_scalar($concurrent['tableOutcome'] ?? null) ? (string)$concurrent['tableOutcome'] : ''; $overlapState = is_scalar($overlap['state'] ?? null) ? (string)$overlap['state'] : ''; return $realRequestFailed && $tableOutcome !== 'success' && empty($receipt['ok']) && $overlapState === 'computed' && is_numeric($overlap['durationMs'] ?? null) && (int)$overlap['durationMs'] > 0; } /** * Summarize the repeated fixed-size controls in chronological order. * A changing control is reported as drift, never reinterpreted as a step effect. * @param array $observations * @return array */ private static function baselineTrend(array $observations): array { $raw = $observations[self::STEP_BASELINE_CONTROL] ?? array(); $baselines = is_array($raw) ? $raw : array(); $count = 0; $okCount = 0; $firstMs = null; $lastMs = null; foreach ($baselines as $baseline) { if (!is_array($baseline)) { continue; } $count++; if (!empty($baseline['ok'])) { $okCount++; } if (isset($baseline['ms']) && is_numeric($baseline['ms'])) { $ms = (int)$baseline['ms']; if ($firstMs === null) { $firstMs = $ms; } $lastMs = $ms; } } return array( 'baselineControlCount' => $count, 'baselineControlOkCount' => $okCount, 'baselineControlFirstMs' => $firstMs ?? -1, 'baselineControlLastMs' => $lastMs ?? -1, 'baselineControlTrendMs' => $firstMs !== null && $lastMs !== null ? $lastMs - $firstMs : 0, ); } /** * The decisive-measurement rule for the detach A/B experiment (Bruno * timeout cause matrix, gap G9 / c434; * ABJ_404_Solution_AjaxRequestLedger::resolveDetachAbMode() picks the * mode, ABJ_404_Solution_AjaxAdminEndpointSupport::checkpointedFlushAndFinish() * records it per request ID). Kept as its own pure function rather than * folded into interpretResults(): two independent verdicts computed from * disjoint inputs -- the ladder's canary observations vs. the * real table endpoint's own A/B attempts -- can never confound each * other, whereas merging them into one matrix would let an ambiguous * quadrant in one leak into the other's conclusion. * * If every 'on' attempt completed and every 'off' attempt did not, the * detach fix is causal. If both modes completed uniformly, detach was * never the cause and a transient (or one of the other three things * beta.2 also ships) is the better explanation. If neither mode ever * completed, something else dominates regardless of detach. Anything * else -- mixed outcomes within a mode, or fewer than one full pair * observed -- is honestly inconclusive rather than forced into one of * the three clean verdicts. * * @param array> $attempts * Chronological per-request outcomes for the real table endpoint's own * workload-matched A/B attempts (mode 'on'/'off', workload scope, and * ordinal as journaled by detach_ab_mode; ok = whether that attempt * completed from the client's own point of view). * @return array */ public static function interpretDetachAbResults(array $attempts): array { $tally = self::tallyDetachAbAttempts($attempts); $onCount = $tally['on']; $onOkCount = $tally['onOk']; $offCount = $tally['off']; $offOkCount = $tally['offOk']; $pairs = self::matchedDetachAbPairs($attempts); $pairCount = count($pairs); $onFirstPairs = 0; $offFirstPairs = 0; $detachPairs = 0; $transientPairs = 0; $neitherPairs = 0; foreach ($pairs as $pair) { $pair['on_first'] ? $onFirstPairs++ : $offFirstPairs++; if ($pair['on_ok'] && !$pair['off_ok']) { $detachPairs++; } else if ($pair['on_ok'] && $pair['off_ok']) { $transientPairs++; } else if (!$pair['on_ok'] && !$pair['off_ok']) { $neitherPairs++; } } $orderCounterbalanced = $onFirstPairs > 0 && $offFirstPairs > 0; $detachCausal = $pairCount >= 2 && $orderCounterbalanced && $detachPairs === $pairCount; $transientCausal = $pairCount > 0 && $transientPairs === $pairCount; $neitherModeHelps = $pairCount > 0 && $neitherPairs === $pairCount; return array( 'detachCausal' => $detachCausal, 'transientCausal' => $transientCausal, 'neitherModeHelps' => $neitherModeHelps, 'inconclusive' => !$detachCausal && !$transientCausal && !$neitherModeHelps, 'onCount' => $onCount, 'onOkCount' => $onOkCount, 'offCount' => $offCount, 'offOkCount' => $offOkCount, 'matchedPairCount' => $pairCount, 'onFirstPairCount' => $onFirstPairs, 'offFirstPairCount' => $offFirstPairs, 'orderCounterbalanced' => $orderCounterbalanced, ); } /** * Complete, workload-matched pairs only. Missing partners, legacy records * without scope fields, duplicated positions, and malformed mode pairs * remain visible in raw attempt accounting but cannot decide causality. * * @param array> $attempts * @return array */ private static function matchedDetachAbPairs(array $attempts): array { $grouped = array(); $duplicates = array(); foreach ($attempts as $attempt) { $slot = self::detachAbPairSlot($attempt); if ($slot === null) { continue; } if (isset($grouped[$slot['key']][$slot['position']])) { $duplicates[$slot['key']] = true; continue; } $grouped[$slot['key']][$slot['position']] = array( 'mode' => $slot['mode'], 'ok' => $slot['ok'], ); } $pairs = array(); foreach ($grouped as $key => $positions) { if (isset($duplicates[$key]) || !isset($positions[0], $positions[1]) || $positions[0]['mode'] === $positions[1]['mode']) { continue; } $on = $positions[0]['mode'] === 'on' ? $positions[0] : $positions[1]; $off = $positions[0]['mode'] === 'off' ? $positions[0] : $positions[1]; $pairs[] = array( 'on_ok' => $on['ok'], 'off_ok' => $off['ok'], 'on_first' => $positions[0]['mode'] === 'on', ); } return $pairs; } /** * Validate one evidence record and derive pair coordinates from ordinal. * Supplemental pair metadata is journaled but never overrides the ordinal. * @param mixed $attempt * @return array{key: string, position: int, mode: string, ok: bool}|null */ private static function detachAbPairSlot($attempt): ?array { if (!is_array($attempt)) { return null; } $part = is_scalar($attempt['part'] ?? null) ? (string)$attempt['part'] : ''; $payloadKey = is_scalar($attempt['payload_key'] ?? null) ? (string)$attempt['payload_key'] : ''; $ordinal = isset($attempt['ordinal']) && is_numeric($attempt['ordinal']) ? (int)$attempt['ordinal'] : -1; $mode = is_scalar($attempt['mode'] ?? null) ? (string)$attempt['mode'] : ''; if ($part === '' || $payloadKey === '' || $ordinal < 0 || ($mode !== 'on' && $mode !== 'off')) { return null; } return array( 'key' => $part . '|' . $payloadKey . '|' . intdiv($ordinal, 2), 'position' => $ordinal % 2, 'mode' => $mode, 'ok' => !empty($attempt['ok']), ); } /** * Count per-mode attempts and completions, split out of * interpretDetachAbResults() purely to keep that method's cyclomatic * complexity within the project's ceiling -- this loop is one * self-contained tally, not logic that needs to be inlined at the call * site. * * @param array $attempts * @return array{on: int, onOk: int, off: int, offOk: int} */ private static function tallyDetachAbAttempts(array $attempts): array { $tally = array('on' => 0, 'onOk' => 0, 'off' => 0, 'offOk' => 0); foreach ($attempts as $attempt) { if (!is_array($attempt)) { continue; } $mode = is_scalar($attempt['mode'] ?? null) ? (string)$attempt['mode'] : ''; if ($mode !== 'on' && $mode !== 'off') { continue; } $tally[$mode]++; if (!empty($attempt['ok'])) { $tally[$mode . 'Ok']++; } } return $tally; } }