@aiready/pattern-detect 0.16.19 → 0.16.20

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.
@@ -182,144 +182,27 @@ function calculateSeverity(file1, file2, code, similarity, linesOfCode) {
182
182
  }
183
183
  }
184
184
 
185
- // src/detector.ts
185
+ // src/core/normalizer.ts
186
186
  function normalizeCode(code, isPython = false) {
187
+ if (!code) return "";
187
188
  let normalized = code;
188
189
  if (isPython) {
189
190
  normalized = normalized.replace(/#.*/g, "");
190
191
  } else {
191
- normalized = normalized.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
192
+ normalized = normalized.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
192
193
  }
193
- return normalized.replace(/['"`]/g, '"').replace(/\s+/g, " ").trim().toLowerCase();
194
+ return normalized.replace(/"[^"]*"/g, '"STR"').replace(/'[^']*'/g, "'STR'").replace(/`[^`]*`/g, "`STR`").replace(/\b\d+\b/g, "NUM").replace(/\s+/g, " ").trim().toLowerCase();
194
195
  }
196
+
197
+ // src/detector.ts
195
198
  function extractBlocks(file, content) {
196
- const isPython = file.toLowerCase().endsWith(".py");
197
- if (isPython) {
198
- return extractBlocksPython(file, content);
199
- }
200
- const blocks = [];
201
- const lines = content.split("\n");
202
- const blockRegex = /^\s*(?:export\s+)?(?:async\s+)?(?:public\s+|private\s+|protected\s+|internal\s+|static\s+|readonly\s+|virtual\s+|abstract\s+|override\s+)*(function|class|interface|type|enum|record|struct|void|func|[a-zA-Z0-9_<>[]]+)\s+([a-zA-Z0-9_]+)(?:\s*\(|(?:\s+extends|\s+implements|\s+where)?\s*\{)|^\s*(?:export\s+)?const\s+([a-zA-Z0-9_]+)\s*=\s*[a-zA-Z0-9_.]+\.object\(|^\s*(app\.(?:get|post|put|delete|patch|use))\(/gm;
203
- let match;
204
- while ((match = blockRegex.exec(content)) !== null) {
205
- const startLine = content.substring(0, match.index).split("\n").length;
206
- let type;
207
- let name;
208
- if (match[1]) {
209
- type = match[1];
210
- name = match[2];
211
- } else if (match[3]) {
212
- type = "const";
213
- name = match[3];
214
- } else {
215
- type = "handler";
216
- name = match[4];
217
- }
218
- let endLine = -1;
219
- let openBraces = 0;
220
- let foundStart = false;
221
- for (let i = match.index; i < content.length; i++) {
222
- if (content[i] === "{") {
223
- openBraces++;
224
- foundStart = true;
225
- } else if (content[i] === "}") {
226
- openBraces--;
227
- }
228
- if (foundStart && openBraces === 0) {
229
- endLine = content.substring(0, i + 1).split("\n").length;
230
- break;
231
- }
232
- }
233
- if (endLine === -1) {
234
- const remaining = content.slice(match.index);
235
- const nextLineMatch = remaining.indexOf("\n");
236
- if (nextLineMatch !== -1) {
237
- endLine = startLine;
238
- } else {
239
- endLine = lines.length;
240
- }
241
- }
242
- endLine = Math.max(startLine, endLine);
243
- const blockCode = lines.slice(startLine - 1, endLine).join("\n");
244
- const tokens = (0, import_core2.estimateTokens)(blockCode);
245
- blocks.push({
246
- file,
247
- startLine,
248
- endLine,
249
- code: blockCode,
250
- tokens,
251
- patternType: inferPatternType(type, name)
252
- });
253
- }
254
- return blocks;
255
- }
256
- function extractBlocksPython(file, content) {
257
- const blocks = [];
258
- const lines = content.split("\n");
259
- const blockRegex = /^\s*(?:async\s+)?(def|class)\s+([a-zA-Z0-9_]+)/gm;
260
- let match;
261
- while ((match = blockRegex.exec(content)) !== null) {
262
- const startLinePos = content.substring(0, match.index).split("\n").length;
263
- const startLineIdx = startLinePos - 1;
264
- const initialIndent = lines[startLineIdx].search(/\S/);
265
- let endLineIdx = startLineIdx;
266
- for (let i = startLineIdx + 1; i < lines.length; i++) {
267
- const line = lines[i];
268
- if (line.trim().length === 0) {
269
- endLineIdx = i;
270
- continue;
271
- }
272
- const currentIndent = line.search(/\S/);
273
- if (currentIndent <= initialIndent) {
274
- break;
275
- }
276
- endLineIdx = i;
277
- }
278
- while (endLineIdx > startLineIdx && lines[endLineIdx].trim().length === 0) {
279
- endLineIdx--;
280
- }
281
- const blockCode = lines.slice(startLineIdx, endLineIdx + 1).join("\n");
282
- const tokens = (0, import_core2.estimateTokens)(blockCode);
283
- blocks.push({
284
- file,
285
- startLine: startLinePos,
286
- endLine: endLineIdx + 1,
287
- code: blockCode,
288
- tokens,
289
- patternType: inferPatternType(match[1], match[2])
290
- });
291
- }
292
- return blocks;
293
- }
294
- function inferPatternType(keyword, name) {
295
- const n = name.toLowerCase();
296
- if (keyword === "handler" || n.includes("handler") || n.includes("controller") || n.startsWith("app.")) {
297
- return "api-handler";
298
- }
299
- if (n.includes("validate") || n.includes("schema")) return "validator";
300
- if (n.includes("util") || n.includes("helper")) return "utility";
301
- if (keyword === "class") return "class-method";
302
- if (n.match(/^[A-Z]/)) return "component";
303
- if (keyword === "function") return "function";
304
- return "unknown";
199
+ return (0, import_core2.extractCodeBlocks)(file, content);
305
200
  }
306
201
  function calculateSimilarity(a, b) {
307
- if (a === b) return 1;
308
- const tokensA = a.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 0);
309
- const tokensB = b.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 0);
310
- if (tokensA.length === 0 || tokensB.length === 0) return 0;
311
- const setA = new Set(tokensA);
312
- const setB = new Set(tokensB);
313
- const intersection = new Set([...setA].filter((x) => setB.has(x)));
314
- const union = /* @__PURE__ */ new Set([...setA, ...setB]);
315
- return intersection.size / union.size;
202
+ return (0, import_core2.calculateStringSimilarity)(a, b);
316
203
  }
317
204
  function calculateConfidence(similarity, tokens, lines) {
318
- let confidence = similarity;
319
- if (lines > 20) confidence += 0.05;
320
- if (tokens > 200) confidence += 0.05;
321
- if (lines < 5) confidence -= 0.1;
322
- return Math.max(0, Math.min(1, confidence));
205
+ return (0, import_core2.calculateHeuristicConfidence)(similarity, tokens, lines);
323
206
  }
324
207
  async function detectDuplicatePatterns(fileContents, options) {
325
208
  const {
@@ -2,8 +2,8 @@ import {
2
2
  analyzePatterns,
3
3
  generateSummary,
4
4
  getSmartDefaults
5
- } from "./chunk-WMOGJFME.mjs";
6
- import "./chunk-THF4RW63.mjs";
5
+ } from "./chunk-DNZS4ESD.mjs";
6
+ import "./chunk-VGMM3L3O.mjs";
7
7
  import "./chunk-I6ETJC7L.mjs";
8
8
  export {
9
9
  analyzePatterns,
@@ -0,0 +1,64 @@
1
+ import {
2
+ analyzePatterns
3
+ } from "./chunk-JBUZ6YHE.mjs";
4
+ import {
5
+ calculatePatternScore
6
+ } from "./chunk-WBBO35SC.mjs";
7
+
8
+ // src/index.ts
9
+ import { ToolRegistry, Severity } from "@aiready/core";
10
+
11
+ // src/provider.ts
12
+ import {
13
+ ToolName,
14
+ SpokeOutputSchema,
15
+ GLOBAL_SCAN_OPTIONS
16
+ } from "@aiready/core";
17
+ var PatternDetectProvider = {
18
+ id: ToolName.PatternDetect,
19
+ alias: ["patterns", "duplicates", "duplication"],
20
+ async analyze(options) {
21
+ const results = await analyzePatterns(options);
22
+ return SpokeOutputSchema.parse({
23
+ results: results.results,
24
+ summary: {
25
+ totalFiles: results.files.length,
26
+ totalIssues: results.results.reduce(
27
+ (sum, r) => sum + r.issues.length,
28
+ 0
29
+ ),
30
+ duplicates: results.duplicates,
31
+ // Keep the raw duplicates for score calculation
32
+ clusters: results.clusters,
33
+ config: Object.fromEntries(
34
+ Object.entries(results.config).filter(
35
+ ([key]) => !GLOBAL_SCAN_OPTIONS.includes(key) || key === "rootDir"
36
+ )
37
+ )
38
+ },
39
+ metadata: {
40
+ toolName: ToolName.PatternDetect,
41
+ version: "0.12.5",
42
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
43
+ }
44
+ });
45
+ },
46
+ score(output, options) {
47
+ const duplicates = output.summary.duplicates || [];
48
+ const totalFiles = output.summary.totalFiles || output.results.length;
49
+ return calculatePatternScore(
50
+ duplicates,
51
+ totalFiles,
52
+ options.costConfig
53
+ );
54
+ },
55
+ defaultWeight: 22
56
+ };
57
+
58
+ // src/index.ts
59
+ ToolRegistry.register(PatternDetectProvider);
60
+
61
+ export {
62
+ PatternDetectProvider,
63
+ Severity
64
+ };
@@ -0,0 +1,64 @@
1
+ import {
2
+ analyzePatterns
3
+ } from "./chunk-DNZS4ESD.mjs";
4
+ import {
5
+ calculatePatternScore
6
+ } from "./chunk-WBBO35SC.mjs";
7
+
8
+ // src/index.ts
9
+ import { ToolRegistry, Severity } from "@aiready/core";
10
+
11
+ // src/provider.ts
12
+ import {
13
+ ToolName,
14
+ SpokeOutputSchema,
15
+ GLOBAL_SCAN_OPTIONS
16
+ } from "@aiready/core";
17
+ var PatternDetectProvider = {
18
+ id: ToolName.PatternDetect,
19
+ alias: ["patterns", "duplicates", "duplication"],
20
+ async analyze(options) {
21
+ const results = await analyzePatterns(options);
22
+ return SpokeOutputSchema.parse({
23
+ results: results.results,
24
+ summary: {
25
+ totalFiles: results.files.length,
26
+ totalIssues: results.results.reduce(
27
+ (sum, r) => sum + r.issues.length,
28
+ 0
29
+ ),
30
+ duplicates: results.duplicates,
31
+ // Keep the raw duplicates for score calculation
32
+ clusters: results.clusters,
33
+ config: Object.fromEntries(
34
+ Object.entries(results.config).filter(
35
+ ([key]) => !GLOBAL_SCAN_OPTIONS.includes(key) || key === "rootDir"
36
+ )
37
+ )
38
+ },
39
+ metadata: {
40
+ toolName: ToolName.PatternDetect,
41
+ version: "0.12.5",
42
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
43
+ }
44
+ });
45
+ },
46
+ score(output, options) {
47
+ const duplicates = output.summary.duplicates || [];
48
+ const totalFiles = output.summary.totalFiles || output.results.length;
49
+ return calculatePatternScore(
50
+ duplicates,
51
+ totalFiles,
52
+ options.costConfig
53
+ );
54
+ },
55
+ defaultWeight: 22
56
+ };
57
+
58
+ // src/index.ts
59
+ ToolRegistry.register(PatternDetectProvider);
60
+
61
+ export {
62
+ PatternDetectProvider,
63
+ Severity
64
+ };
@@ -0,0 +1,64 @@
1
+ import {
2
+ analyzePatterns
3
+ } from "./chunk-XNPID6FU.mjs";
4
+ import {
5
+ calculatePatternScore
6
+ } from "./chunk-WBBO35SC.mjs";
7
+
8
+ // src/index.ts
9
+ import { ToolRegistry, Severity } from "@aiready/core";
10
+
11
+ // src/provider.ts
12
+ import {
13
+ ToolName,
14
+ SpokeOutputSchema,
15
+ GLOBAL_SCAN_OPTIONS
16
+ } from "@aiready/core";
17
+ var PatternDetectProvider = {
18
+ id: ToolName.PatternDetect,
19
+ alias: ["patterns", "duplicates", "duplication"],
20
+ async analyze(options) {
21
+ const results = await analyzePatterns(options);
22
+ return SpokeOutputSchema.parse({
23
+ results: results.results,
24
+ summary: {
25
+ totalFiles: results.files.length,
26
+ totalIssues: results.results.reduce(
27
+ (sum, r) => sum + r.issues.length,
28
+ 0
29
+ ),
30
+ duplicates: results.duplicates,
31
+ // Keep the raw duplicates for score calculation
32
+ clusters: results.clusters,
33
+ config: Object.fromEntries(
34
+ Object.entries(results.config).filter(
35
+ ([key]) => !GLOBAL_SCAN_OPTIONS.includes(key) || key === "rootDir"
36
+ )
37
+ )
38
+ },
39
+ metadata: {
40
+ toolName: ToolName.PatternDetect,
41
+ version: "0.12.5",
42
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
43
+ }
44
+ });
45
+ },
46
+ score(output, options) {
47
+ const duplicates = output.summary.duplicates || [];
48
+ const totalFiles = output.summary.totalFiles || output.results.length;
49
+ return calculatePatternScore(
50
+ duplicates,
51
+ totalFiles,
52
+ options.costConfig
53
+ );
54
+ },
55
+ defaultWeight: 22
56
+ };
57
+
58
+ // src/index.ts
59
+ ToolRegistry.register(PatternDetectProvider);
60
+
61
+ export {
62
+ PatternDetectProvider,
63
+ Severity
64
+ };
@@ -0,0 +1,259 @@
1
+ import {
2
+ calculateSeverity
3
+ } from "./chunk-I6ETJC7L.mjs";
4
+
5
+ // src/detector.ts
6
+ import { estimateTokens } from "@aiready/core";
7
+
8
+ // src/core/normalizer.ts
9
+ function normalizeCode(code, isPython = false) {
10
+ if (!code) return "";
11
+ let normalized = code;
12
+ if (isPython) {
13
+ normalized = normalized.replace(/#.*/g, "");
14
+ } else {
15
+ normalized = normalized.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
16
+ }
17
+ return normalized.replace(/"[^"]*"/g, '"STR"').replace(/'[^']*'/g, "'STR'").replace(/`[^`]*`/g, "`STR`").replace(/\b\d+\b/g, "NUM").replace(/\s+/g, " ").trim();
18
+ }
19
+
20
+ // src/detector.ts
21
+ function extractBlocks(file, content) {
22
+ const isPython = file.toLowerCase().endsWith(".py");
23
+ if (isPython) {
24
+ return extractBlocksPython(file, content);
25
+ }
26
+ const blocks = [];
27
+ const lines = content.split("\n");
28
+ const blockRegex = /^\s*(?:export\s+)?(?:async\s+)?(?:public\s+|private\s+|protected\s+|internal\s+|static\s+|readonly\s+|virtual\s+|abstract\s+|override\s+)*(function|class|interface|type|enum|record|struct|void|func|[a-zA-Z0-9_<>[]]+)\s+([a-zA-Z0-9_]+)(?:\s*\(|(?:\s+extends|\s+implements|\s+where)?\s*\{)|^\s*(?:export\s+)?const\s+([a-zA-Z0-9_]+)\s*=\s*[a-zA-Z0-9_.]+\.object\(|^\s*(app\.(?:get|post|put|delete|patch|use))\(/gm;
29
+ let match;
30
+ while ((match = blockRegex.exec(content)) !== null) {
31
+ const startLine = content.substring(0, match.index).split("\n").length;
32
+ let type;
33
+ let name;
34
+ if (match[1]) {
35
+ type = match[1];
36
+ name = match[2];
37
+ } else if (match[3]) {
38
+ type = "const";
39
+ name = match[3];
40
+ } else {
41
+ type = "handler";
42
+ name = match[4];
43
+ }
44
+ let endLine = -1;
45
+ let openBraces = 0;
46
+ let foundStart = false;
47
+ for (let i = match.index; i < content.length; i++) {
48
+ if (content[i] === "{") {
49
+ openBraces++;
50
+ foundStart = true;
51
+ } else if (content[i] === "}") {
52
+ openBraces--;
53
+ }
54
+ if (foundStart && openBraces === 0) {
55
+ endLine = content.substring(0, i + 1).split("\n").length;
56
+ break;
57
+ }
58
+ }
59
+ if (endLine === -1) {
60
+ const remaining = content.slice(match.index);
61
+ const nextLineMatch = remaining.indexOf("\n");
62
+ if (nextLineMatch !== -1) {
63
+ endLine = startLine;
64
+ } else {
65
+ endLine = lines.length;
66
+ }
67
+ }
68
+ endLine = Math.max(startLine, endLine);
69
+ const blockCode = lines.slice(startLine - 1, endLine).join("\n");
70
+ const tokens = estimateTokens(blockCode);
71
+ blocks.push({
72
+ file,
73
+ startLine,
74
+ endLine,
75
+ code: blockCode,
76
+ tokens,
77
+ patternType: inferPatternType(type, name)
78
+ });
79
+ }
80
+ return blocks;
81
+ }
82
+ function extractBlocksPython(file, content) {
83
+ const blocks = [];
84
+ const lines = content.split("\n");
85
+ const blockRegex = /^\s*(?:async\s+)?(def|class)\s+([a-zA-Z0-9_]+)/gm;
86
+ let match;
87
+ while ((match = blockRegex.exec(content)) !== null) {
88
+ const startLinePos = content.substring(0, match.index).split("\n").length;
89
+ const startLineIdx = startLinePos - 1;
90
+ const initialIndent = lines[startLineIdx].search(/\S/);
91
+ let endLineIdx = startLineIdx;
92
+ for (let i = startLineIdx + 1; i < lines.length; i++) {
93
+ const line = lines[i];
94
+ if (line.trim().length === 0) {
95
+ endLineIdx = i;
96
+ continue;
97
+ }
98
+ const currentIndent = line.search(/\S/);
99
+ if (currentIndent <= initialIndent) {
100
+ break;
101
+ }
102
+ endLineIdx = i;
103
+ }
104
+ while (endLineIdx > startLineIdx && lines[endLineIdx].trim().length === 0) {
105
+ endLineIdx--;
106
+ }
107
+ const blockCode = lines.slice(startLineIdx, endLineIdx + 1).join("\n");
108
+ const tokens = estimateTokens(blockCode);
109
+ blocks.push({
110
+ file,
111
+ startLine: startLinePos,
112
+ endLine: endLineIdx + 1,
113
+ code: blockCode,
114
+ tokens,
115
+ patternType: inferPatternType(match[1], match[2])
116
+ });
117
+ }
118
+ return blocks;
119
+ }
120
+ function inferPatternType(keyword, name) {
121
+ const n = name.toLowerCase();
122
+ if (keyword === "handler" || n.includes("handler") || n.includes("controller") || n.startsWith("app.")) {
123
+ return "api-handler";
124
+ }
125
+ if (n.includes("validate") || n.includes("schema")) return "validator";
126
+ if (n.includes("util") || n.includes("helper")) return "utility";
127
+ if (keyword === "class") return "class-method";
128
+ if (n.match(/^[A-Z]/)) return "component";
129
+ if (keyword === "function") return "function";
130
+ return "unknown";
131
+ }
132
+ function calculateSimilarity(a, b) {
133
+ if (a === b) return 1;
134
+ const tokensA = a.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 0);
135
+ const tokensB = b.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 0);
136
+ if (tokensA.length === 0 || tokensB.length === 0) return 0;
137
+ const setA = new Set(tokensA);
138
+ const setB = new Set(tokensB);
139
+ const intersection = new Set([...setA].filter((x) => setB.has(x)));
140
+ const union = /* @__PURE__ */ new Set([...setA, ...setB]);
141
+ return intersection.size / union.size;
142
+ }
143
+ function calculateConfidence(similarity, tokens, lines) {
144
+ let confidence = similarity;
145
+ if (lines > 20) confidence += 0.05;
146
+ if (tokens > 200) confidence += 0.05;
147
+ if (lines < 5) confidence -= 0.1;
148
+ return Math.max(0, Math.min(1, confidence));
149
+ }
150
+ async function detectDuplicatePatterns(fileContents, options) {
151
+ const {
152
+ minSimilarity,
153
+ minLines,
154
+ streamResults,
155
+ onProgress,
156
+ excludePatterns = [],
157
+ confidenceThreshold = 0,
158
+ ignoreWhitelist = []
159
+ } = options;
160
+ const allBlocks = [];
161
+ const excludeRegexes = excludePatterns.map((p) => new RegExp(p, "i"));
162
+ for (const { file, content } of fileContents) {
163
+ const blocks = extractBlocks(file, content);
164
+ for (const b of blocks) {
165
+ if (b.endLine - b.startLine + 1 < minLines) continue;
166
+ const isExcluded = excludeRegexes.some((regex) => regex.test(b.code));
167
+ if (isExcluded) continue;
168
+ allBlocks.push(b);
169
+ }
170
+ }
171
+ const duplicates = [];
172
+ const totalBlocks = allBlocks.length;
173
+ let comparisons = 0;
174
+ const totalComparisons = totalBlocks * (totalBlocks - 1) / 2;
175
+ if (onProgress) {
176
+ onProgress(
177
+ 0,
178
+ totalComparisons,
179
+ `Starting duplicate detection on ${totalBlocks} blocks...`
180
+ );
181
+ }
182
+ for (let i = 0; i < allBlocks.length; i++) {
183
+ if (i % 50 === 0 && i > 0) {
184
+ await new Promise((resolve) => setImmediate(resolve));
185
+ if (onProgress) {
186
+ onProgress(
187
+ comparisons,
188
+ totalComparisons,
189
+ `Analyzing blocks (${i}/${totalBlocks})...`
190
+ );
191
+ }
192
+ }
193
+ const b1 = allBlocks[i];
194
+ const isPython1 = b1.file.toLowerCase().endsWith(".py");
195
+ const norm1 = normalizeCode(b1.code, isPython1);
196
+ for (let j = i + 1; j < allBlocks.length; j++) {
197
+ comparisons++;
198
+ const b2 = allBlocks[j];
199
+ if (b1.file === b2.file) continue;
200
+ const isWhitelisted = ignoreWhitelist.some((pattern) => {
201
+ return b1.file.includes(pattern) && b2.file.includes(pattern) || pattern === `${b1.file}::${b2.file}` || pattern === `${b2.file}::${b1.file}`;
202
+ });
203
+ if (isWhitelisted) continue;
204
+ const isPython2 = b2.file.toLowerCase().endsWith(".py");
205
+ const norm2 = normalizeCode(b2.code, isPython2);
206
+ const sim = calculateSimilarity(norm1, norm2);
207
+ if (sim >= minSimilarity) {
208
+ const confidence = calculateConfidence(
209
+ sim,
210
+ b1.tokens,
211
+ b1.endLine - b1.startLine + 1
212
+ );
213
+ if (confidence < confidenceThreshold) continue;
214
+ const { severity, reason, suggestion, matchedRule } = calculateSeverity(
215
+ b1.file,
216
+ b2.file,
217
+ b1.code,
218
+ sim,
219
+ b1.endLine - b1.startLine + 1
220
+ );
221
+ const dup = {
222
+ file1: b1.file,
223
+ line1: b1.startLine,
224
+ endLine1: b1.endLine,
225
+ file2: b2.file,
226
+ line2: b2.startLine,
227
+ endLine2: b2.endLine,
228
+ code1: b1.code,
229
+ code2: b2.code,
230
+ similarity: sim,
231
+ confidence,
232
+ patternType: b1.patternType,
233
+ tokenCost: b1.tokens + b2.tokens,
234
+ severity,
235
+ reason,
236
+ suggestion,
237
+ matchedRule
238
+ };
239
+ duplicates.push(dup);
240
+ if (streamResults)
241
+ console.log(
242
+ `[DUPLICATE] ${dup.file1}:${dup.line1} <-> ${dup.file2}:${dup.line2} (${Math.round(sim * 100)}%, conf: ${Math.round(confidence * 100)}%)`
243
+ );
244
+ }
245
+ }
246
+ }
247
+ if (onProgress) {
248
+ onProgress(
249
+ totalComparisons,
250
+ totalComparisons,
251
+ `Duplicate detection complete. Found ${duplicates.length} patterns.`
252
+ );
253
+ }
254
+ return duplicates.sort((a, b) => b.similarity - a.similarity);
255
+ }
256
+
257
+ export {
258
+ detectDuplicatePatterns
259
+ };