@timmeck/brain 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/BRAIN_PLAN.md +3324 -3324
  2. package/LICENSE +21 -21
  3. package/README.md +194 -188
  4. package/dist/brain.js +2 -0
  5. package/dist/brain.js.map +1 -1
  6. package/dist/cli/colors.d.ts +50 -0
  7. package/dist/cli/colors.js +106 -0
  8. package/dist/cli/colors.js.map +1 -0
  9. package/dist/cli/commands/config.d.ts +2 -0
  10. package/dist/cli/commands/config.js +165 -0
  11. package/dist/cli/commands/config.js.map +1 -0
  12. package/dist/cli/commands/dashboard.js +222 -8
  13. package/dist/cli/commands/dashboard.js.map +1 -1
  14. package/dist/cli/commands/export.js +3 -0
  15. package/dist/cli/commands/export.js.map +1 -1
  16. package/dist/cli/commands/import.js +24 -15
  17. package/dist/cli/commands/import.js.map +1 -1
  18. package/dist/cli/commands/insights.js +33 -6
  19. package/dist/cli/commands/insights.js.map +1 -1
  20. package/dist/cli/commands/learn.d.ts +2 -0
  21. package/dist/cli/commands/learn.js +22 -0
  22. package/dist/cli/commands/learn.js.map +1 -0
  23. package/dist/cli/commands/modules.js +25 -6
  24. package/dist/cli/commands/modules.js.map +1 -1
  25. package/dist/cli/commands/network.js +15 -9
  26. package/dist/cli/commands/network.js.map +1 -1
  27. package/dist/cli/commands/query.js +92 -25
  28. package/dist/cli/commands/query.js.map +1 -1
  29. package/dist/cli/commands/start.js +8 -5
  30. package/dist/cli/commands/start.js.map +1 -1
  31. package/dist/cli/commands/status.js +21 -16
  32. package/dist/cli/commands/status.js.map +1 -1
  33. package/dist/cli/commands/stop.js +5 -4
  34. package/dist/cli/commands/stop.js.map +1 -1
  35. package/dist/cli/ipc-helper.js +4 -3
  36. package/dist/cli/ipc-helper.js.map +1 -1
  37. package/dist/cli/update-check.d.ts +2 -0
  38. package/dist/cli/update-check.js +58 -0
  39. package/dist/cli/update-check.js.map +1 -0
  40. package/dist/db/migrations/001_core_schema.js +115 -115
  41. package/dist/db/migrations/002_learning_schema.js +33 -33
  42. package/dist/db/migrations/003_code_schema.js +48 -48
  43. package/dist/db/migrations/004_synapses_schema.js +52 -52
  44. package/dist/db/migrations/005_fts_indexes.js +73 -73
  45. package/dist/db/migrations/index.js +6 -6
  46. package/dist/db/repositories/antipattern.repository.js +3 -3
  47. package/dist/db/repositories/code-module.repository.d.ts +1 -0
  48. package/dist/db/repositories/code-module.repository.js +8 -0
  49. package/dist/db/repositories/code-module.repository.js.map +1 -1
  50. package/dist/db/repositories/error.repository.js +46 -46
  51. package/dist/db/repositories/insight.repository.js +3 -3
  52. package/dist/db/repositories/notification.repository.js +3 -3
  53. package/dist/db/repositories/project.repository.js +21 -21
  54. package/dist/db/repositories/rule.repository.js +24 -24
  55. package/dist/db/repositories/solution.repository.js +50 -50
  56. package/dist/db/repositories/synapse.repository.js +18 -18
  57. package/dist/db/repositories/terminal.repository.js +24 -24
  58. package/dist/index.js +4 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/ipc/router.d.ts +2 -0
  61. package/dist/ipc/router.js +7 -1
  62. package/dist/ipc/router.js.map +1 -1
  63. package/dist/services/code.service.d.ts +1 -1
  64. package/dist/services/code.service.js +5 -2
  65. package/dist/services/code.service.js.map +1 -1
  66. package/package.json +5 -4
  67. package/src/brain.ts +3 -0
  68. package/src/cli/colors.ts +116 -0
  69. package/src/cli/commands/config.ts +169 -0
  70. package/src/cli/commands/dashboard.ts +231 -8
  71. package/src/cli/commands/export.ts +4 -0
  72. package/src/cli/commands/import.ts +24 -15
  73. package/src/cli/commands/insights.ts +37 -5
  74. package/src/cli/commands/learn.ts +24 -0
  75. package/src/cli/commands/modules.ts +28 -5
  76. package/src/cli/commands/network.ts +15 -9
  77. package/src/cli/commands/query.ts +103 -26
  78. package/src/cli/commands/start.ts +8 -5
  79. package/src/cli/commands/status.ts +22 -16
  80. package/src/cli/commands/stop.ts +5 -4
  81. package/src/cli/ipc-helper.ts +4 -3
  82. package/src/cli/update-check.ts +63 -0
  83. package/src/code/analyzer.ts +77 -77
  84. package/src/code/fingerprint.ts +87 -87
  85. package/src/code/matcher.ts +64 -64
  86. package/src/code/parsers/generic.ts +29 -29
  87. package/src/code/parsers/python.ts +54 -54
  88. package/src/code/parsers/typescript.ts +65 -65
  89. package/src/code/registry.ts +60 -60
  90. package/src/code/scorer.ts +108 -108
  91. package/src/config.ts +111 -111
  92. package/src/db/connection.ts +22 -22
  93. package/src/db/migrations/001_core_schema.ts +120 -120
  94. package/src/db/migrations/002_learning_schema.ts +38 -38
  95. package/src/db/migrations/003_code_schema.ts +53 -53
  96. package/src/db/migrations/004_synapses_schema.ts +57 -57
  97. package/src/db/migrations/005_fts_indexes.ts +78 -78
  98. package/src/db/migrations/006_synapses_phase3.ts +17 -17
  99. package/src/db/migrations/index.ts +64 -64
  100. package/src/db/repositories/antipattern.repository.ts +66 -66
  101. package/src/db/repositories/code-module.repository.ts +9 -0
  102. package/src/db/repositories/error.repository.ts +149 -149
  103. package/src/db/repositories/insight.repository.ts +78 -78
  104. package/src/db/repositories/notification.repository.ts +66 -66
  105. package/src/db/repositories/project.repository.ts +93 -93
  106. package/src/db/repositories/rule.repository.ts +108 -108
  107. package/src/db/repositories/solution.repository.ts +154 -154
  108. package/src/db/repositories/synapse.repository.ts +153 -153
  109. package/src/db/repositories/terminal.repository.ts +101 -101
  110. package/src/hooks/post-tool-use.ts +90 -90
  111. package/src/hooks/post-write.ts +117 -117
  112. package/src/index.ts +4 -0
  113. package/src/ipc/client.ts +118 -118
  114. package/src/ipc/protocol.ts +35 -35
  115. package/src/ipc/router.ts +9 -1
  116. package/src/ipc/server.ts +110 -110
  117. package/src/learning/confidence-scorer.ts +47 -47
  118. package/src/learning/decay.ts +46 -46
  119. package/src/learning/learning-engine.ts +162 -162
  120. package/src/learning/pattern-extractor.ts +90 -90
  121. package/src/learning/rule-generator.ts +74 -74
  122. package/src/matching/error-matcher.ts +115 -115
  123. package/src/matching/fingerprint.ts +29 -29
  124. package/src/matching/similarity.ts +61 -61
  125. package/src/matching/tfidf.ts +74 -74
  126. package/src/matching/tokenizer.ts +41 -41
  127. package/src/mcp/auto-detect.ts +93 -93
  128. package/src/mcp/server.ts +73 -73
  129. package/src/mcp/tools.ts +290 -290
  130. package/src/parsing/error-parser.ts +28 -28
  131. package/src/parsing/parsers/compiler.ts +93 -93
  132. package/src/parsing/parsers/generic.ts +28 -28
  133. package/src/parsing/parsers/go.ts +97 -97
  134. package/src/parsing/parsers/node.ts +69 -69
  135. package/src/parsing/parsers/python.ts +62 -62
  136. package/src/parsing/parsers/rust.ts +50 -50
  137. package/src/parsing/parsers/shell.ts +42 -42
  138. package/src/parsing/types.ts +47 -47
  139. package/src/research/gap-analyzer.ts +135 -135
  140. package/src/research/insight-generator.ts +123 -123
  141. package/src/research/research-engine.ts +116 -116
  142. package/src/research/synergy-detector.ts +126 -126
  143. package/src/research/template-extractor.ts +130 -130
  144. package/src/research/trend-analyzer.ts +127 -127
  145. package/src/services/analytics.service.ts +87 -87
  146. package/src/services/code.service.ts +5 -2
  147. package/src/services/error.service.ts +164 -164
  148. package/src/services/notification.service.ts +41 -41
  149. package/src/services/prevention.service.ts +119 -119
  150. package/src/services/research.service.ts +93 -93
  151. package/src/services/solution.service.ts +116 -116
  152. package/src/services/synapse.service.ts +59 -59
  153. package/src/services/terminal.service.ts +81 -81
  154. package/src/synapses/activation.ts +80 -80
  155. package/src/synapses/decay.ts +38 -38
  156. package/src/synapses/hebbian.ts +69 -69
  157. package/src/synapses/pathfinder.ts +81 -81
  158. package/src/synapses/synapse-manager.ts +109 -109
  159. package/src/types/code.types.ts +52 -52
  160. package/src/types/config.types.ts +79 -79
  161. package/src/types/error.types.ts +67 -67
  162. package/src/types/ipc.types.ts +8 -8
  163. package/src/types/mcp.types.ts +53 -53
  164. package/src/types/research.types.ts +28 -28
  165. package/src/types/solution.types.ts +30 -30
  166. package/src/types/synapse.types.ts +49 -49
  167. package/src/utils/events.ts +45 -45
  168. package/src/utils/hash.ts +5 -5
  169. package/src/utils/logger.ts +48 -48
  170. package/src/utils/paths.ts +19 -19
  171. package/tests/fixtures/code-modules/modules.ts +83 -83
  172. package/tests/fixtures/errors/go.ts +9 -9
  173. package/tests/fixtures/errors/node.ts +24 -24
  174. package/tests/fixtures/errors/python.ts +21 -21
  175. package/tests/fixtures/errors/rust.ts +25 -25
  176. package/tests/fixtures/errors/shell.ts +15 -15
  177. package/tests/fixtures/solutions/solutions.ts +27 -27
  178. package/tests/helpers/setup-db.ts +52 -52
  179. package/tests/integration/code-flow.test.ts +86 -86
  180. package/tests/integration/error-flow.test.ts +83 -83
  181. package/tests/integration/ipc-flow.test.ts +166 -166
  182. package/tests/integration/learning-cycle.test.ts +82 -82
  183. package/tests/integration/synapse-flow.test.ts +117 -117
  184. package/tests/unit/code/analyzer.test.ts +58 -58
  185. package/tests/unit/code/fingerprint.test.ts +51 -51
  186. package/tests/unit/code/scorer.test.ts +55 -55
  187. package/tests/unit/learning/confidence-scorer.test.ts +60 -60
  188. package/tests/unit/learning/decay.test.ts +45 -45
  189. package/tests/unit/learning/pattern-extractor.test.ts +50 -50
  190. package/tests/unit/matching/error-matcher.test.ts +69 -69
  191. package/tests/unit/matching/fingerprint.test.ts +47 -47
  192. package/tests/unit/matching/similarity.test.ts +65 -65
  193. package/tests/unit/matching/tfidf.test.ts +71 -71
  194. package/tests/unit/matching/tokenizer.test.ts +83 -83
  195. package/tests/unit/parsing/parsers.test.ts +113 -113
  196. package/tests/unit/research/gap-analyzer.test.ts +45 -45
  197. package/tests/unit/research/trend-analyzer.test.ts +45 -45
  198. package/tests/unit/synapses/activation.test.ts +80 -80
  199. package/tests/unit/synapses/decay.test.ts +27 -27
  200. package/tests/unit/synapses/hebbian.test.ts +96 -96
  201. package/tests/unit/synapses/pathfinder.test.ts +72 -72
  202. package/tsconfig.json +18 -18
@@ -1,74 +1,74 @@
1
- import type { LearningConfig } from '../types/config.types.js';
2
- import type { RuleRepository } from '../db/repositories/rule.repository.js';
3
- import type { ErrorPattern } from './pattern-extractor.js';
4
- import { getLogger } from '../utils/logger.js';
5
-
6
- export interface GeneratedRule {
7
- pattern: string;
8
- action: string;
9
- description: string;
10
- confidence: number;
11
- sourceErrorIds: number[];
12
- }
13
-
14
- /**
15
- * Generate prevention rules from extracted patterns.
16
- */
17
- export function generateRules(
18
- patterns: ErrorPattern[],
19
- config: LearningConfig,
20
- ): GeneratedRule[] {
21
- return patterns
22
- .filter(p =>
23
- p.occurrences >= config.minOccurrences &&
24
- p.confidence >= config.minConfidence,
25
- )
26
- .map(pattern => ({
27
- pattern: pattern.messageRegex,
28
- action: pattern.confidence >= 0.90
29
- ? `Auto-fix available for ${pattern.errorType}`
30
- : `Suggestion: check ${pattern.errorType} pattern (${pattern.occurrences} occurrences)`,
31
- description: `Auto-generated from ${pattern.occurrences} occurrences of ${pattern.errorType}`,
32
- confidence: pattern.confidence,
33
- sourceErrorIds: pattern.errorIds,
34
- }));
35
- }
36
-
37
- /**
38
- * Persist generated rules to the database.
39
- */
40
- export function persistRules(
41
- rules: GeneratedRule[],
42
- ruleRepo: RuleRepository,
43
- projectId?: number,
44
- ): number {
45
- const logger = getLogger();
46
- let created = 0;
47
-
48
- for (const rule of rules) {
49
- // Check if similar rule already exists
50
- const existing = ruleRepo.findByPattern(rule.pattern);
51
- if (existing.length > 0) {
52
- // Update confidence of existing rule
53
- const best = existing[0]!;
54
- if (rule.confidence > best.confidence) {
55
- ruleRepo.update(best.id, { confidence: rule.confidence });
56
- }
57
- continue;
58
- }
59
-
60
- ruleRepo.create({
61
- pattern: rule.pattern,
62
- action: rule.action,
63
- description: rule.description,
64
- confidence: rule.confidence,
65
- occurrences: 0,
66
- active: 1,
67
- project_id: projectId ?? null,
68
- });
69
- created++;
70
- logger.info(`New rule generated: ${rule.pattern.substring(0, 50)}...`);
71
- }
72
-
73
- return created;
74
- }
1
+ import type { LearningConfig } from '../types/config.types.js';
2
+ import type { RuleRepository } from '../db/repositories/rule.repository.js';
3
+ import type { ErrorPattern } from './pattern-extractor.js';
4
+ import { getLogger } from '../utils/logger.js';
5
+
6
+ export interface GeneratedRule {
7
+ pattern: string;
8
+ action: string;
9
+ description: string;
10
+ confidence: number;
11
+ sourceErrorIds: number[];
12
+ }
13
+
14
+ /**
15
+ * Generate prevention rules from extracted patterns.
16
+ */
17
+ export function generateRules(
18
+ patterns: ErrorPattern[],
19
+ config: LearningConfig,
20
+ ): GeneratedRule[] {
21
+ return patterns
22
+ .filter(p =>
23
+ p.occurrences >= config.minOccurrences &&
24
+ p.confidence >= config.minConfidence,
25
+ )
26
+ .map(pattern => ({
27
+ pattern: pattern.messageRegex,
28
+ action: pattern.confidence >= 0.90
29
+ ? `Auto-fix available for ${pattern.errorType}`
30
+ : `Suggestion: check ${pattern.errorType} pattern (${pattern.occurrences} occurrences)`,
31
+ description: `Auto-generated from ${pattern.occurrences} occurrences of ${pattern.errorType}`,
32
+ confidence: pattern.confidence,
33
+ sourceErrorIds: pattern.errorIds,
34
+ }));
35
+ }
36
+
37
+ /**
38
+ * Persist generated rules to the database.
39
+ */
40
+ export function persistRules(
41
+ rules: GeneratedRule[],
42
+ ruleRepo: RuleRepository,
43
+ projectId?: number,
44
+ ): number {
45
+ const logger = getLogger();
46
+ let created = 0;
47
+
48
+ for (const rule of rules) {
49
+ // Check if similar rule already exists
50
+ const existing = ruleRepo.findByPattern(rule.pattern);
51
+ if (existing.length > 0) {
52
+ // Update confidence of existing rule
53
+ const best = existing[0]!;
54
+ if (rule.confidence > best.confidence) {
55
+ ruleRepo.update(best.id, { confidence: rule.confidence });
56
+ }
57
+ continue;
58
+ }
59
+
60
+ ruleRepo.create({
61
+ pattern: rule.pattern,
62
+ action: rule.action,
63
+ description: rule.description,
64
+ confidence: rule.confidence,
65
+ occurrences: 0,
66
+ active: 1,
67
+ project_id: projectId ?? null,
68
+ });
69
+ created++;
70
+ logger.info(`New rule generated: ${rule.pattern.substring(0, 50)}...`);
71
+ }
72
+
73
+ return created;
74
+ }
@@ -1,115 +1,115 @@
1
- import type { ErrorRecord } from '../types/error.types.js';
2
- import { tokenize } from './tokenizer.js';
3
- import { cosineSimilarity, jaccardSimilarity, levenshteinDistance } from './similarity.js';
4
-
5
- export interface SignalScore {
6
- signal: string;
7
- score: number;
8
- weighted: number;
9
- }
10
-
11
- export interface MatchResult {
12
- errorId: number;
13
- score: number;
14
- signals: SignalScore[];
15
- isStrong: boolean;
16
- }
17
-
18
- interface MatchSignal {
19
- name: string;
20
- weight: number;
21
- compute: (a: ErrorRecord, b: ErrorRecord) => number;
22
- }
23
-
24
- const SIGNALS: MatchSignal[] = [
25
- { name: 'fingerprint', weight: 0.30, compute: fingerprintMatch },
26
- { name: 'message_similarity', weight: 0.20, compute: messageSimilarity },
27
- { name: 'type_match', weight: 0.15, compute: typeMatch },
28
- { name: 'stack_similarity', weight: 0.15, compute: stackSimilarity },
29
- { name: 'file_similarity', weight: 0.10, compute: fileSimilarity },
30
- { name: 'context_similarity', weight: 0.10, compute: contextSimilarity },
31
- ];
32
-
33
- const MATCH_THRESHOLD = 0.70;
34
- const STRONG_MATCH_THRESHOLD = 0.90;
35
-
36
- export function matchError(
37
- incoming: ErrorRecord,
38
- candidates: ErrorRecord[],
39
- ): MatchResult[] {
40
- return candidates
41
- .map(candidate => {
42
- const signals = SIGNALS.map(signal => {
43
- const score = signal.compute(incoming, candidate);
44
- return {
45
- signal: signal.name,
46
- score,
47
- weighted: score * signal.weight,
48
- };
49
- });
50
-
51
- const totalScore = signals.reduce((sum, s) => sum + s.weighted, 0);
52
-
53
- return {
54
- errorId: candidate.id,
55
- score: totalScore,
56
- signals,
57
- isStrong: totalScore >= STRONG_MATCH_THRESHOLD,
58
- };
59
- })
60
- .filter(result => result.score >= MATCH_THRESHOLD)
61
- .sort((a, b) => b.score - a.score);
62
- }
63
-
64
- function fingerprintMatch(a: ErrorRecord, b: ErrorRecord): number {
65
- return a.fingerprint === b.fingerprint ? 1.0 : 0.0;
66
- }
67
-
68
- function messageSimilarity(a: ErrorRecord, b: ErrorRecord): number {
69
- const tokensA = tokenize(a.message);
70
- const tokensB = tokenize(b.message);
71
- return cosineSimilarity(tokensA, tokensB);
72
- }
73
-
74
- function typeMatch(a: ErrorRecord, b: ErrorRecord): number {
75
- return a.type === b.type ? 1.0 : 0.0;
76
- }
77
-
78
- function stackSimilarity(a: ErrorRecord, b: ErrorRecord): number {
79
- const rawA = a.raw_output ?? '';
80
- const rawB = b.raw_output ?? '';
81
-
82
- const frameRe = /at (?:(.+?) )?\(/g;
83
- const extractFuncs = (raw: string) => {
84
- const funcs: string[] = [];
85
- let m: RegExpExecArray | null;
86
- const re = new RegExp(frameRe.source, 'g');
87
- while ((m = re.exec(raw)) !== null) {
88
- if (m[1]) funcs.push(m[1]);
89
- }
90
- return funcs;
91
- };
92
-
93
- const funcsA = extractFuncs(rawA);
94
- const funcsB = extractFuncs(rawB);
95
-
96
- if (funcsA.length === 0 && funcsB.length === 0) return 0.5;
97
- return jaccardSimilarity(funcsA, funcsB);
98
- }
99
-
100
- function fileSimilarity(a: ErrorRecord, b: ErrorRecord): number {
101
- const pathA = a.file_path ?? '';
102
- const pathB = b.file_path ?? '';
103
- if (!pathA || !pathB) return 0.0;
104
- if (pathA === pathB) return 1.0;
105
- return levenshteinDistance(pathA, pathB);
106
- }
107
-
108
- function contextSimilarity(a: ErrorRecord, b: ErrorRecord): number {
109
- const ctxA = a.context ?? '';
110
- const ctxB = b.context ?? '';
111
- if (!ctxA || !ctxB) return 0.0;
112
- const tokensA = tokenize(ctxA);
113
- const tokensB = tokenize(ctxB);
114
- return cosineSimilarity(tokensA, tokensB);
115
- }
1
+ import type { ErrorRecord } from '../types/error.types.js';
2
+ import { tokenize } from './tokenizer.js';
3
+ import { cosineSimilarity, jaccardSimilarity, levenshteinDistance } from './similarity.js';
4
+
5
+ export interface SignalScore {
6
+ signal: string;
7
+ score: number;
8
+ weighted: number;
9
+ }
10
+
11
+ export interface MatchResult {
12
+ errorId: number;
13
+ score: number;
14
+ signals: SignalScore[];
15
+ isStrong: boolean;
16
+ }
17
+
18
+ interface MatchSignal {
19
+ name: string;
20
+ weight: number;
21
+ compute: (a: ErrorRecord, b: ErrorRecord) => number;
22
+ }
23
+
24
+ const SIGNALS: MatchSignal[] = [
25
+ { name: 'fingerprint', weight: 0.30, compute: fingerprintMatch },
26
+ { name: 'message_similarity', weight: 0.20, compute: messageSimilarity },
27
+ { name: 'type_match', weight: 0.15, compute: typeMatch },
28
+ { name: 'stack_similarity', weight: 0.15, compute: stackSimilarity },
29
+ { name: 'file_similarity', weight: 0.10, compute: fileSimilarity },
30
+ { name: 'context_similarity', weight: 0.10, compute: contextSimilarity },
31
+ ];
32
+
33
+ const MATCH_THRESHOLD = 0.70;
34
+ const STRONG_MATCH_THRESHOLD = 0.90;
35
+
36
+ export function matchError(
37
+ incoming: ErrorRecord,
38
+ candidates: ErrorRecord[],
39
+ ): MatchResult[] {
40
+ return candidates
41
+ .map(candidate => {
42
+ const signals = SIGNALS.map(signal => {
43
+ const score = signal.compute(incoming, candidate);
44
+ return {
45
+ signal: signal.name,
46
+ score,
47
+ weighted: score * signal.weight,
48
+ };
49
+ });
50
+
51
+ const totalScore = signals.reduce((sum, s) => sum + s.weighted, 0);
52
+
53
+ return {
54
+ errorId: candidate.id,
55
+ score: totalScore,
56
+ signals,
57
+ isStrong: totalScore >= STRONG_MATCH_THRESHOLD,
58
+ };
59
+ })
60
+ .filter(result => result.score >= MATCH_THRESHOLD)
61
+ .sort((a, b) => b.score - a.score);
62
+ }
63
+
64
+ function fingerprintMatch(a: ErrorRecord, b: ErrorRecord): number {
65
+ return a.fingerprint === b.fingerprint ? 1.0 : 0.0;
66
+ }
67
+
68
+ function messageSimilarity(a: ErrorRecord, b: ErrorRecord): number {
69
+ const tokensA = tokenize(a.message);
70
+ const tokensB = tokenize(b.message);
71
+ return cosineSimilarity(tokensA, tokensB);
72
+ }
73
+
74
+ function typeMatch(a: ErrorRecord, b: ErrorRecord): number {
75
+ return a.type === b.type ? 1.0 : 0.0;
76
+ }
77
+
78
+ function stackSimilarity(a: ErrorRecord, b: ErrorRecord): number {
79
+ const rawA = a.raw_output ?? '';
80
+ const rawB = b.raw_output ?? '';
81
+
82
+ const frameRe = /at (?:(.+?) )?\(/g;
83
+ const extractFuncs = (raw: string) => {
84
+ const funcs: string[] = [];
85
+ let m: RegExpExecArray | null;
86
+ const re = new RegExp(frameRe.source, 'g');
87
+ while ((m = re.exec(raw)) !== null) {
88
+ if (m[1]) funcs.push(m[1]);
89
+ }
90
+ return funcs;
91
+ };
92
+
93
+ const funcsA = extractFuncs(rawA);
94
+ const funcsB = extractFuncs(rawB);
95
+
96
+ if (funcsA.length === 0 && funcsB.length === 0) return 0.5;
97
+ return jaccardSimilarity(funcsA, funcsB);
98
+ }
99
+
100
+ function fileSimilarity(a: ErrorRecord, b: ErrorRecord): number {
101
+ const pathA = a.file_path ?? '';
102
+ const pathB = b.file_path ?? '';
103
+ if (!pathA || !pathB) return 0.0;
104
+ if (pathA === pathB) return 1.0;
105
+ return levenshteinDistance(pathA, pathB);
106
+ }
107
+
108
+ function contextSimilarity(a: ErrorRecord, b: ErrorRecord): number {
109
+ const ctxA = a.context ?? '';
110
+ const ctxB = b.context ?? '';
111
+ if (!ctxA || !ctxB) return 0.0;
112
+ const tokensA = tokenize(ctxA);
113
+ const tokensB = tokenize(ctxB);
114
+ return cosineSimilarity(tokensA, tokensB);
115
+ }
@@ -1,29 +1,29 @@
1
- import path from 'node:path';
2
- import { sha256 } from '../utils/hash.js';
3
- import type { StackFrame } from '../parsing/types.js';
4
-
5
- export function templateMessage(msg: string): string {
6
- return msg
7
- .replace(/[A-Z]:\\[\w\-.\\ ]+\.\w+/g, '<PATH>')
8
- .replace(/\/[\w\-./ ]+\.\w+/g, '<PATH>')
9
- .replace(/:(\d+):(\d+)/g, ':<LINE>:<COL>')
10
- .replace(/line \d+/gi, 'line <LINE>')
11
- .replace(/0x[0-9a-fA-F]+/g, '<ADDR>')
12
- .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<UUID>')
13
- .replace(/https?:\/\/[^\s]+/g, '<URL>')
14
- .replace(/\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}/g, '<TIMESTAMP>');
15
- }
16
-
17
- export function generateFingerprint(
18
- errorType: string,
19
- message: string,
20
- frames: StackFrame[],
21
- ): string {
22
- const template = templateMessage(message);
23
- const topFrames = frames
24
- .slice(0, 3)
25
- .map(f => `${f.function_name || '<anon>'}@${path.basename(f.file_path || '<unknown>')}`)
26
- .join('|');
27
- const input = `${errorType}::${template}::${topFrames}`;
28
- return sha256(input);
29
- }
1
+ import path from 'node:path';
2
+ import { sha256 } from '../utils/hash.js';
3
+ import type { StackFrame } from '../parsing/types.js';
4
+
5
+ export function templateMessage(msg: string): string {
6
+ return msg
7
+ .replace(/[A-Z]:\\[\w\-.\\ ]+\.\w+/g, '<PATH>')
8
+ .replace(/\/[\w\-./ ]+\.\w+/g, '<PATH>')
9
+ .replace(/:(\d+):(\d+)/g, ':<LINE>:<COL>')
10
+ .replace(/line \d+/gi, 'line <LINE>')
11
+ .replace(/0x[0-9a-fA-F]+/g, '<ADDR>')
12
+ .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<UUID>')
13
+ .replace(/https?:\/\/[^\s]+/g, '<URL>')
14
+ .replace(/\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}/g, '<TIMESTAMP>');
15
+ }
16
+
17
+ export function generateFingerprint(
18
+ errorType: string,
19
+ message: string,
20
+ frames: StackFrame[],
21
+ ): string {
22
+ const template = templateMessage(message);
23
+ const topFrames = frames
24
+ .slice(0, 3)
25
+ .map(f => `${f.function_name || '<anon>'}@${path.basename(f.file_path || '<unknown>')}`)
26
+ .join('|');
27
+ const input = `${errorType}::${template}::${topFrames}`;
28
+ return sha256(input);
29
+ }
@@ -1,61 +1,61 @@
1
- export function levenshteinDistance(a: string, b: string): number {
2
- if (a === b) return 1.0;
3
- if (a.length === 0 || b.length === 0) return 0.0;
4
-
5
- const dp: number[][] = Array(b.length + 1)
6
- .fill(0)
7
- .map(() => Array(a.length + 1).fill(0) as number[]);
8
-
9
- for (let i = 0; i <= a.length; i++) dp[0]![i] = i;
10
- for (let j = 0; j <= b.length; j++) dp[j]![0] = j;
11
-
12
- for (let i = 1; i <= b.length; i++) {
13
- for (let j = 1; j <= a.length; j++) {
14
- const cost = a[j - 1] === b[i - 1] ? 0 : 1;
15
- dp[i]![j] = Math.min(
16
- dp[i - 1]![j]! + 1,
17
- dp[i]![j - 1]! + 1,
18
- dp[i - 1]![j - 1]! + cost,
19
- );
20
- }
21
- }
22
-
23
- return 1 - dp[b.length]![a.length]! / Math.max(a.length, b.length);
24
- }
25
-
26
- export function cosineSimilarity(tokensA: string[], tokensB: string[]): number {
27
- if (tokensA.length === 0 || tokensB.length === 0) return 0.0;
28
-
29
- const vocab = new Set([...tokensA, ...tokensB]);
30
- const vecA = new Map<string, number>();
31
- const vecB = new Map<string, number>();
32
-
33
- for (const t of tokensA) vecA.set(t, (vecA.get(t) ?? 0) + 1);
34
- for (const t of tokensB) vecB.set(t, (vecB.get(t) ?? 0) + 1);
35
-
36
- let dot = 0;
37
- let magA = 0;
38
- let magB = 0;
39
-
40
- for (const word of vocab) {
41
- const a = vecA.get(word) ?? 0;
42
- const b = vecB.get(word) ?? 0;
43
- dot += a * b;
44
- magA += a * a;
45
- magB += b * b;
46
- }
47
-
48
- const denom = Math.sqrt(magA) * Math.sqrt(magB);
49
- return denom === 0 ? 0 : dot / denom;
50
- }
51
-
52
- export function jaccardSimilarity(tokensA: string[], tokensB: string[]): number {
53
- if (tokensA.length === 0 && tokensB.length === 0) return 0.0;
54
-
55
- const setA = new Set(tokensA);
56
- const setB = new Set(tokensB);
57
- const intersection = new Set([...setA].filter(x => setB.has(x)));
58
- const union = new Set([...setA, ...setB]);
59
-
60
- return union.size === 0 ? 0 : intersection.size / union.size;
61
- }
1
+ export function levenshteinDistance(a: string, b: string): number {
2
+ if (a === b) return 1.0;
3
+ if (a.length === 0 || b.length === 0) return 0.0;
4
+
5
+ const dp: number[][] = Array(b.length + 1)
6
+ .fill(0)
7
+ .map(() => Array(a.length + 1).fill(0) as number[]);
8
+
9
+ for (let i = 0; i <= a.length; i++) dp[0]![i] = i;
10
+ for (let j = 0; j <= b.length; j++) dp[j]![0] = j;
11
+
12
+ for (let i = 1; i <= b.length; i++) {
13
+ for (let j = 1; j <= a.length; j++) {
14
+ const cost = a[j - 1] === b[i - 1] ? 0 : 1;
15
+ dp[i]![j] = Math.min(
16
+ dp[i - 1]![j]! + 1,
17
+ dp[i]![j - 1]! + 1,
18
+ dp[i - 1]![j - 1]! + cost,
19
+ );
20
+ }
21
+ }
22
+
23
+ return 1 - dp[b.length]![a.length]! / Math.max(a.length, b.length);
24
+ }
25
+
26
+ export function cosineSimilarity(tokensA: string[], tokensB: string[]): number {
27
+ if (tokensA.length === 0 || tokensB.length === 0) return 0.0;
28
+
29
+ const vocab = new Set([...tokensA, ...tokensB]);
30
+ const vecA = new Map<string, number>();
31
+ const vecB = new Map<string, number>();
32
+
33
+ for (const t of tokensA) vecA.set(t, (vecA.get(t) ?? 0) + 1);
34
+ for (const t of tokensB) vecB.set(t, (vecB.get(t) ?? 0) + 1);
35
+
36
+ let dot = 0;
37
+ let magA = 0;
38
+ let magB = 0;
39
+
40
+ for (const word of vocab) {
41
+ const a = vecA.get(word) ?? 0;
42
+ const b = vecB.get(word) ?? 0;
43
+ dot += a * b;
44
+ magA += a * a;
45
+ magB += b * b;
46
+ }
47
+
48
+ const denom = Math.sqrt(magA) * Math.sqrt(magB);
49
+ return denom === 0 ? 0 : dot / denom;
50
+ }
51
+
52
+ export function jaccardSimilarity(tokensA: string[], tokensB: string[]): number {
53
+ if (tokensA.length === 0 && tokensB.length === 0) return 0.0;
54
+
55
+ const setA = new Set(tokensA);
56
+ const setB = new Set(tokensB);
57
+ const intersection = new Set([...setA].filter(x => setB.has(x)));
58
+ const union = new Set([...setA, ...setB]);
59
+
60
+ return union.size === 0 ? 0 : intersection.size / union.size;
61
+ }