@aiready/pattern-detect 0.16.19 → 0.16.21

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 (56) hide show
  1. package/dist/analyzer-entry/index.d.mts +3 -0
  2. package/dist/analyzer-entry/index.d.ts +3 -0
  3. package/dist/analyzer-entry/index.js +693 -0
  4. package/dist/analyzer-entry/index.mjs +12 -0
  5. package/dist/analyzer-entry.d.mts +100 -3
  6. package/dist/analyzer-entry.d.ts +100 -3
  7. package/dist/analyzer-entry.js +9 -126
  8. package/dist/analyzer-entry.mjs +2 -2
  9. package/dist/chunk-65UQ5J2J.mjs +64 -0
  10. package/dist/chunk-6JTVOBJX.mjs +64 -0
  11. package/dist/chunk-BKRPSTT2.mjs +64 -0
  12. package/dist/chunk-CMWW24HW.mjs +259 -0
  13. package/dist/chunk-DNZS4ESD.mjs +391 -0
  14. package/dist/chunk-GLKAGFKX.mjs +391 -0
  15. package/dist/chunk-GREN7X5H.mjs +143 -0
  16. package/dist/chunk-JBUZ6YHE.mjs +391 -0
  17. package/dist/chunk-KWMNN3TG.mjs +391 -0
  18. package/dist/chunk-LYKRYBSM.mjs +64 -0
  19. package/dist/chunk-MHU3CL4R.mjs +64 -0
  20. package/dist/chunk-RS73WLNI.mjs +251 -0
  21. package/dist/chunk-SVCSIZ2A.mjs +259 -0
  22. package/dist/chunk-VGMM3L3O.mjs +143 -0
  23. package/dist/chunk-XNPID6FU.mjs +391 -0
  24. package/dist/cli.js +29 -147
  25. package/dist/cli.mjs +27 -25
  26. package/dist/context-rules-entry/index.d.mts +2 -0
  27. package/dist/context-rules-entry/index.d.ts +2 -0
  28. package/dist/context-rules-entry/index.js +207 -0
  29. package/dist/context-rules-entry/index.mjs +12 -0
  30. package/dist/context-rules-entry.d.mts +55 -2
  31. package/dist/context-rules-entry.d.ts +55 -2
  32. package/dist/detector-entry/index.d.mts +14 -0
  33. package/dist/detector-entry/index.d.ts +14 -0
  34. package/dist/detector-entry/index.js +301 -0
  35. package/dist/detector-entry/index.mjs +7 -0
  36. package/dist/detector-entry.d.mts +2 -2
  37. package/dist/detector-entry.d.ts +2 -2
  38. package/dist/detector-entry.js +9 -126
  39. package/dist/detector-entry.mjs +1 -1
  40. package/dist/index-BVz-HnZd.d.mts +119 -0
  41. package/dist/index-BwuoiCNm.d.ts +119 -0
  42. package/dist/index-y2uJSngh.d.mts +60 -0
  43. package/dist/index-y2uJSngh.d.ts +60 -0
  44. package/dist/index.d.mts +4 -4
  45. package/dist/index.d.ts +4 -4
  46. package/dist/index.js +9 -126
  47. package/dist/index.mjs +3 -3
  48. package/dist/scoring-entry/index.d.mts +23 -0
  49. package/dist/scoring-entry/index.d.ts +23 -0
  50. package/dist/scoring-entry/index.js +133 -0
  51. package/dist/scoring-entry/index.mjs +6 -0
  52. package/dist/scoring-entry.d.mts +1 -1
  53. package/dist/scoring-entry.d.ts +1 -1
  54. package/dist/types-C4lmb2Yh.d.mts +36 -0
  55. package/dist/types-C4lmb2Yh.d.ts +36 -0
  56. package/package.json +16 -16
@@ -0,0 +1,251 @@
1
+ import {
2
+ calculateSeverity
3
+ } from "./chunk-I6ETJC7L.mjs";
4
+
5
+ // src/detector.ts
6
+ import {
7
+ estimateTokens,
8
+ calculateStringSimilarity,
9
+ calculateHeuristicConfidence
10
+ } from "@aiready/core";
11
+
12
+ // src/core/normalizer.ts
13
+ function normalizeCode(code, isPython = false) {
14
+ if (!code) return "";
15
+ let normalized = code;
16
+ if (isPython) {
17
+ normalized = normalized.replace(/#.*/g, "");
18
+ } else {
19
+ normalized = normalized.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
20
+ }
21
+ return normalized.replace(/"[^"]*"/g, '"STR"').replace(/'[^']*'/g, "'STR'").replace(/`[^`]*`/g, "`STR`").replace(/\b\d+\b/g, "NUM").replace(/\s+/g, " ").trim();
22
+ }
23
+
24
+ // src/detector.ts
25
+ function extractBlocks(file, content) {
26
+ const isPython = file.toLowerCase().endsWith(".py");
27
+ if (isPython) {
28
+ return extractBlocksPython(file, content);
29
+ }
30
+ const blocks = [];
31
+ const lines = content.split("\n");
32
+ 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;
33
+ let match;
34
+ while ((match = blockRegex.exec(content)) !== null) {
35
+ const startLine = content.substring(0, match.index).split("\n").length;
36
+ let type;
37
+ let name;
38
+ if (match[1]) {
39
+ type = match[1];
40
+ name = match[2];
41
+ } else if (match[3]) {
42
+ type = "const";
43
+ name = match[3];
44
+ } else {
45
+ type = "handler";
46
+ name = match[4];
47
+ }
48
+ let endLine = -1;
49
+ let openBraces = 0;
50
+ let foundStart = false;
51
+ for (let i = match.index; i < content.length; i++) {
52
+ if (content[i] === "{") {
53
+ openBraces++;
54
+ foundStart = true;
55
+ } else if (content[i] === "}") {
56
+ openBraces--;
57
+ }
58
+ if (foundStart && openBraces === 0) {
59
+ endLine = content.substring(0, i + 1).split("\n").length;
60
+ break;
61
+ }
62
+ }
63
+ if (endLine === -1) {
64
+ const remaining = content.slice(match.index);
65
+ const nextLineMatch = remaining.indexOf("\n");
66
+ if (nextLineMatch !== -1) {
67
+ endLine = startLine;
68
+ } else {
69
+ endLine = lines.length;
70
+ }
71
+ }
72
+ endLine = Math.max(startLine, endLine);
73
+ const blockCode = lines.slice(startLine - 1, endLine).join("\n");
74
+ const tokens = estimateTokens(blockCode);
75
+ blocks.push({
76
+ file,
77
+ startLine,
78
+ endLine,
79
+ code: blockCode,
80
+ tokens,
81
+ patternType: inferPatternType(type, name)
82
+ });
83
+ }
84
+ return blocks;
85
+ }
86
+ function extractBlocksPython(file, content) {
87
+ const blocks = [];
88
+ const lines = content.split("\n");
89
+ const blockRegex = /^\s*(?:async\s+)?(def|class)\s+([a-zA-Z0-9_]+)/gm;
90
+ let match;
91
+ while ((match = blockRegex.exec(content)) !== null) {
92
+ const startLinePos = content.substring(0, match.index).split("\n").length;
93
+ const startLineIdx = startLinePos - 1;
94
+ const initialIndent = lines[startLineIdx].search(/\S/);
95
+ let endLineIdx = startLineIdx;
96
+ for (let i = startLineIdx + 1; i < lines.length; i++) {
97
+ const line = lines[i];
98
+ if (line.trim().length === 0) {
99
+ endLineIdx = i;
100
+ continue;
101
+ }
102
+ const currentIndent = line.search(/\S/);
103
+ if (currentIndent <= initialIndent) {
104
+ break;
105
+ }
106
+ endLineIdx = i;
107
+ }
108
+ while (endLineIdx > startLineIdx && lines[endLineIdx].trim().length === 0) {
109
+ endLineIdx--;
110
+ }
111
+ const blockCode = lines.slice(startLineIdx, endLineIdx + 1).join("\n");
112
+ const tokens = estimateTokens(blockCode);
113
+ blocks.push({
114
+ file,
115
+ startLine: startLinePos,
116
+ endLine: endLineIdx + 1,
117
+ code: blockCode,
118
+ tokens,
119
+ patternType: inferPatternType(match[1], match[2])
120
+ });
121
+ }
122
+ return blocks;
123
+ }
124
+ function inferPatternType(keyword, name) {
125
+ const n = name.toLowerCase();
126
+ if (keyword === "handler" || n.includes("handler") || n.includes("controller") || n.startsWith("app.")) {
127
+ return "api-handler";
128
+ }
129
+ if (n.includes("validate") || n.includes("schema")) return "validator";
130
+ if (n.includes("util") || n.includes("helper")) return "utility";
131
+ if (keyword === "class") return "class-method";
132
+ if (n.match(/^[A-Z]/)) return "component";
133
+ if (keyword === "function") return "function";
134
+ return "unknown";
135
+ }
136
+ function calculateSimilarity(a, b) {
137
+ return calculateStringSimilarity(a, b);
138
+ }
139
+ function calculateConfidence(similarity, tokens, lines) {
140
+ return calculateHeuristicConfidence(similarity, tokens, lines);
141
+ }
142
+ async function detectDuplicatePatterns(fileContents, options) {
143
+ const {
144
+ minSimilarity,
145
+ minLines,
146
+ streamResults,
147
+ onProgress,
148
+ excludePatterns = [],
149
+ confidenceThreshold = 0,
150
+ ignoreWhitelist = []
151
+ } = options;
152
+ const allBlocks = [];
153
+ const excludeRegexes = excludePatterns.map((p) => new RegExp(p, "i"));
154
+ for (const { file, content } of fileContents) {
155
+ const blocks = extractBlocks(file, content);
156
+ for (const b of blocks) {
157
+ if (b.endLine - b.startLine + 1 < minLines) continue;
158
+ const isExcluded = excludeRegexes.some((regex) => regex.test(b.code));
159
+ if (isExcluded) continue;
160
+ allBlocks.push(b);
161
+ }
162
+ }
163
+ const duplicates = [];
164
+ const totalBlocks = allBlocks.length;
165
+ let comparisons = 0;
166
+ const totalComparisons = totalBlocks * (totalBlocks - 1) / 2;
167
+ if (onProgress) {
168
+ onProgress(
169
+ 0,
170
+ totalComparisons,
171
+ `Starting duplicate detection on ${totalBlocks} blocks...`
172
+ );
173
+ }
174
+ for (let i = 0; i < allBlocks.length; i++) {
175
+ if (i % 50 === 0 && i > 0) {
176
+ await new Promise((resolve) => setImmediate(resolve));
177
+ if (onProgress) {
178
+ onProgress(
179
+ comparisons,
180
+ totalComparisons,
181
+ `Analyzing blocks (${i}/${totalBlocks})...`
182
+ );
183
+ }
184
+ }
185
+ const b1 = allBlocks[i];
186
+ const isPython1 = b1.file.toLowerCase().endsWith(".py");
187
+ const norm1 = normalizeCode(b1.code, isPython1);
188
+ for (let j = i + 1; j < allBlocks.length; j++) {
189
+ comparisons++;
190
+ const b2 = allBlocks[j];
191
+ if (b1.file === b2.file) continue;
192
+ const isWhitelisted = ignoreWhitelist.some((pattern) => {
193
+ return b1.file.includes(pattern) && b2.file.includes(pattern) || pattern === `${b1.file}::${b2.file}` || pattern === `${b2.file}::${b1.file}`;
194
+ });
195
+ if (isWhitelisted) continue;
196
+ const isPython2 = b2.file.toLowerCase().endsWith(".py");
197
+ const norm2 = normalizeCode(b2.code, isPython2);
198
+ const sim = calculateSimilarity(norm1, norm2);
199
+ if (sim >= minSimilarity) {
200
+ const confidence = calculateConfidence(
201
+ sim,
202
+ b1.tokens,
203
+ b1.endLine - b1.startLine + 1
204
+ );
205
+ if (confidence < confidenceThreshold) continue;
206
+ const { severity, reason, suggestion, matchedRule } = calculateSeverity(
207
+ b1.file,
208
+ b2.file,
209
+ b1.code,
210
+ sim,
211
+ b1.endLine - b1.startLine + 1
212
+ );
213
+ const dup = {
214
+ file1: b1.file,
215
+ line1: b1.startLine,
216
+ endLine1: b1.endLine,
217
+ file2: b2.file,
218
+ line2: b2.startLine,
219
+ endLine2: b2.endLine,
220
+ code1: b1.code,
221
+ code2: b2.code,
222
+ similarity: sim,
223
+ confidence,
224
+ patternType: b1.patternType,
225
+ tokenCost: b1.tokens + b2.tokens,
226
+ severity,
227
+ reason,
228
+ suggestion,
229
+ matchedRule
230
+ };
231
+ duplicates.push(dup);
232
+ if (streamResults)
233
+ console.log(
234
+ `[DUPLICATE] ${dup.file1}:${dup.line1} <-> ${dup.file2}:${dup.line2} (${Math.round(sim * 100)}%, conf: ${Math.round(confidence * 100)}%)`
235
+ );
236
+ }
237
+ }
238
+ }
239
+ if (onProgress) {
240
+ onProgress(
241
+ totalComparisons,
242
+ totalComparisons,
243
+ `Duplicate detection complete. Found ${duplicates.length} patterns.`
244
+ );
245
+ }
246
+ return duplicates.sort((a, b) => b.similarity - a.similarity);
247
+ }
248
+
249
+ export {
250
+ detectDuplicatePatterns
251
+ };
@@ -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().toLowerCase();
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
+ };
@@ -0,0 +1,143 @@
1
+ import {
2
+ calculateSeverity
3
+ } from "./chunk-I6ETJC7L.mjs";
4
+
5
+ // src/detector.ts
6
+ import {
7
+ calculateStringSimilarity,
8
+ calculateHeuristicConfidence,
9
+ extractCodeBlocks
10
+ } from "@aiready/core";
11
+
12
+ // src/core/normalizer.ts
13
+ function normalizeCode(code, isPython = false) {
14
+ if (!code) return "";
15
+ let normalized = code;
16
+ if (isPython) {
17
+ normalized = normalized.replace(/#.*/g, "");
18
+ } else {
19
+ normalized = normalized.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
20
+ }
21
+ return normalized.replace(/"[^"]*"/g, '"STR"').replace(/'[^']*'/g, "'STR'").replace(/`[^`]*`/g, "`STR`").replace(/\b\d+\b/g, "NUM").replace(/\s+/g, " ").trim().toLowerCase();
22
+ }
23
+
24
+ // src/detector.ts
25
+ function extractBlocks(file, content) {
26
+ return extractCodeBlocks(file, content);
27
+ }
28
+ function calculateSimilarity(a, b) {
29
+ return calculateStringSimilarity(a, b);
30
+ }
31
+ function calculateConfidence(similarity, tokens, lines) {
32
+ return calculateHeuristicConfidence(similarity, tokens, lines);
33
+ }
34
+ async function detectDuplicatePatterns(fileContents, options) {
35
+ const {
36
+ minSimilarity,
37
+ minLines,
38
+ streamResults,
39
+ onProgress,
40
+ excludePatterns = [],
41
+ confidenceThreshold = 0,
42
+ ignoreWhitelist = []
43
+ } = options;
44
+ const allBlocks = [];
45
+ const excludeRegexes = excludePatterns.map((p) => new RegExp(p, "i"));
46
+ for (const { file, content } of fileContents) {
47
+ const blocks = extractBlocks(file, content);
48
+ for (const b of blocks) {
49
+ if (b.endLine - b.startLine + 1 < minLines) continue;
50
+ const isExcluded = excludeRegexes.some((regex) => regex.test(b.code));
51
+ if (isExcluded) continue;
52
+ allBlocks.push(b);
53
+ }
54
+ }
55
+ const duplicates = [];
56
+ const totalBlocks = allBlocks.length;
57
+ let comparisons = 0;
58
+ const totalComparisons = totalBlocks * (totalBlocks - 1) / 2;
59
+ if (onProgress) {
60
+ onProgress(
61
+ 0,
62
+ totalComparisons,
63
+ `Starting duplicate detection on ${totalBlocks} blocks...`
64
+ );
65
+ }
66
+ for (let i = 0; i < allBlocks.length; i++) {
67
+ if (i % 50 === 0 && i > 0) {
68
+ await new Promise((resolve) => setImmediate(resolve));
69
+ if (onProgress) {
70
+ onProgress(
71
+ comparisons,
72
+ totalComparisons,
73
+ `Analyzing blocks (${i}/${totalBlocks})...`
74
+ );
75
+ }
76
+ }
77
+ const b1 = allBlocks[i];
78
+ const isPython1 = b1.file.toLowerCase().endsWith(".py");
79
+ const norm1 = normalizeCode(b1.code, isPython1);
80
+ for (let j = i + 1; j < allBlocks.length; j++) {
81
+ comparisons++;
82
+ const b2 = allBlocks[j];
83
+ if (b1.file === b2.file) continue;
84
+ const isWhitelisted = ignoreWhitelist.some((pattern) => {
85
+ return b1.file.includes(pattern) && b2.file.includes(pattern) || pattern === `${b1.file}::${b2.file}` || pattern === `${b2.file}::${b1.file}`;
86
+ });
87
+ if (isWhitelisted) continue;
88
+ const isPython2 = b2.file.toLowerCase().endsWith(".py");
89
+ const norm2 = normalizeCode(b2.code, isPython2);
90
+ const sim = calculateSimilarity(norm1, norm2);
91
+ if (sim >= minSimilarity) {
92
+ const confidence = calculateConfidence(
93
+ sim,
94
+ b1.tokens,
95
+ b1.endLine - b1.startLine + 1
96
+ );
97
+ if (confidence < confidenceThreshold) continue;
98
+ const { severity, reason, suggestion, matchedRule } = calculateSeverity(
99
+ b1.file,
100
+ b2.file,
101
+ b1.code,
102
+ sim,
103
+ b1.endLine - b1.startLine + 1
104
+ );
105
+ const dup = {
106
+ file1: b1.file,
107
+ line1: b1.startLine,
108
+ endLine1: b1.endLine,
109
+ file2: b2.file,
110
+ line2: b2.startLine,
111
+ endLine2: b2.endLine,
112
+ code1: b1.code,
113
+ code2: b2.code,
114
+ similarity: sim,
115
+ confidence,
116
+ patternType: b1.patternType,
117
+ tokenCost: b1.tokens + b2.tokens,
118
+ severity,
119
+ reason,
120
+ suggestion,
121
+ matchedRule
122
+ };
123
+ duplicates.push(dup);
124
+ if (streamResults)
125
+ console.log(
126
+ `[DUPLICATE] ${dup.file1}:${dup.line1} <-> ${dup.file2}:${dup.line2} (${Math.round(sim * 100)}%, conf: ${Math.round(confidence * 100)}%)`
127
+ );
128
+ }
129
+ }
130
+ }
131
+ if (onProgress) {
132
+ onProgress(
133
+ totalComparisons,
134
+ totalComparisons,
135
+ `Duplicate detection complete. Found ${duplicates.length} patterns.`
136
+ );
137
+ }
138
+ return duplicates.sort((a, b) => b.similarity - a.similarity);
139
+ }
140
+
141
+ export {
142
+ detectDuplicatePatterns
143
+ };