1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
|
<?php
require_once __DIR__ . '/config.php';
$errors = load_json(ERRORS_FILE);
$rebuildFlag = DEBUG_DATA_DIR . '/.rebuild_clusters';
// Check if cached clusters exist and are fresh
if (file_exists(CLUSTERS_FILE) && !file_exists($rebuildFlag)) {
$clusters = load_json(CLUSTERS_FILE);
json_response([
'success' => 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),
]);
|