true, 'cached' => true, 'clusters' => $clusters, 'total_errors' => count($errors), ]); } // Rebuild clusters $normalized = []; foreach ($errors as $e) { $msg = strtolower($e['message'] ?? ''); $msg = preg_replace('/\b0x[a-f0-9]+\b/', '0xHEX', $msg); $msg = preg_replace('/\b\d+\b/', 'N', $msg); $msg = preg_replace('/["\'].*?["\']/', '"STR"', $msg); $msg = preg_replace('/[^\w\s]/', ' ', $msg); $msg = preg_replace('/\s+/', ' ', $msg); $normalized[] = trim($msg); } $clusters = []; $assigned = []; $threshold = CLUSTER_SIMILARITY_THRESHOLD; for ($i = 0; $i < count($errors); $i++) { if (isset($assigned[$i])) continue; $cluster = [ 'errors' => [$i], 'messages' => [$errors[$i]['message']], 'count' => $errors[$i]['count'] ?? 1, 'types' => [$errors[$i]['type'] ?? 'unknown'], 'categories' => [$errors[$i]['category'] ?? 'runtime'], 'label' => $errors[$i]['message'] ?? '', ]; $assigned[$i] = true; for ($j = $i + 1; $j < count($errors); $j++) { if (isset($assigned[$j])) continue; // Jaccard similarity $tokensA = array_unique(array_filter(explode(' ', $normalized[$i]))); $tokensB = array_unique(array_filter(explode(' ', $normalized[$j]))); if (empty($tokensA) && empty($tokensB)) { $similarity = 1; } else { $intersection = count(array_intersect($tokensA, $tokensB)); $union = count($tokensA) + count($tokensB) - $intersection; $similarity = $union > 0 ? $intersection / $union : 0; } if ($similarity >= $threshold) { $cluster['errors'][] = $j; $cluster['messages'][] = $errors[$j]['message']; $cluster['count'] += $errors[$j]['count'] ?? 1; if (!in_array($errors[$j]['type'] ?? 'unknown', $cluster['types'])) { $cluster['types'][] = $errors[$j]['type'] ?? 'unknown'; } if (!in_array($errors[$j]['category'] ?? 'runtime', $cluster['categories'])) { $cluster['categories'][] = $errors[$j]['category'] ?? 'runtime'; } $assigned[$j] = true; } } // Most frequent message as label $freq = array_count_values($cluster['messages']); arsort($freq); $cluster['label'] = substr(key($freq) ?: $cluster['label'], 0, 200); $cluster['error_count'] = count($cluster['errors']); unset($cluster['errors']); unset($cluster['messages']); $clusters[] = $cluster; } usort($clusters, fn($a, $b) => $b['count'] - $a['count']); // Cache save_json(CLUSTERS_FILE, $clusters); if (file_exists($rebuildFlag)) unlink($rebuildFlag); json_response([ 'success' => true, 'cached' => false, 'clusters' => array_slice($clusters, 0, 50), 'total_errors' => count($errors), 'total_clusters' => count($clusters), ]);