@aiready/pattern-detect 0.17.1 → 0.17.2
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.
- package/dist/analyzer-entry/index.js +3 -3
- package/dist/analyzer-entry/index.mjs +1 -1
- package/dist/chunk-K7BO57OO.mjs +391 -0
- package/dist/cli.js +39 -191
- package/dist/cli.mjs +6 -5
- package/dist/index.js +3 -3
- package/dist/index.mjs +56 -7
- package/package.json +2 -2
|
@@ -472,7 +472,7 @@ async function getSmartDefaults(directory, userOptions) {
|
|
|
472
472
|
6,
|
|
473
473
|
Math.min(20, 6 + Math.floor(estimatedBlocks / 1e3) * 2)
|
|
474
474
|
);
|
|
475
|
-
const minSimilarity = Math.min(0.
|
|
475
|
+
const minSimilarity = Math.min(0.75, 0.45 + estimatedBlocks / 1e4 * 0.3);
|
|
476
476
|
const batchSize = estimatedBlocks > 1e3 ? 200 : 100;
|
|
477
477
|
const severity = estimatedBlocks > 3e3 ? "high" : "all";
|
|
478
478
|
const maxCandidatesPerBlock = Math.max(
|
|
@@ -632,9 +632,9 @@ async function analyzePatterns(options) {
|
|
|
632
632
|
return { results, duplicates, files, groups, clusters, config: finalOptions };
|
|
633
633
|
}
|
|
634
634
|
function generateSummary(results) {
|
|
635
|
-
const allIssues = results.flatMap((r) => r.issues);
|
|
635
|
+
const allIssues = results.flatMap((r) => r.issues || []);
|
|
636
636
|
const totalTokenCost = results.reduce(
|
|
637
|
-
(sum, r) => sum + (r.metrics
|
|
637
|
+
(sum, r) => sum + (r.metrics?.tokenCost || 0),
|
|
638
638
|
0
|
|
639
639
|
);
|
|
640
640
|
const patternsByType = {
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectDuplicatePatterns
|
|
3
|
+
} from "./chunk-NQBYYWHJ.mjs";
|
|
4
|
+
import {
|
|
5
|
+
calculateSeverity
|
|
6
|
+
} from "./chunk-J2G742QF.mjs";
|
|
7
|
+
|
|
8
|
+
// src/grouping.ts
|
|
9
|
+
import { getSeverityLevel } from "@aiready/core";
|
|
10
|
+
import path from "path";
|
|
11
|
+
function groupDuplicatesByFilePair(duplicates) {
|
|
12
|
+
const groups = /* @__PURE__ */ new Map();
|
|
13
|
+
for (const dup of duplicates) {
|
|
14
|
+
const files = [dup.file1, dup.file2].sort();
|
|
15
|
+
const key = files.join("::");
|
|
16
|
+
if (!groups.has(key)) {
|
|
17
|
+
groups.set(key, {
|
|
18
|
+
filePair: key,
|
|
19
|
+
severity: dup.severity,
|
|
20
|
+
occurrences: 0,
|
|
21
|
+
totalTokenCost: 0,
|
|
22
|
+
averageSimilarity: 0,
|
|
23
|
+
patternTypes: /* @__PURE__ */ new Set(),
|
|
24
|
+
lineRanges: []
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const group = groups.get(key);
|
|
28
|
+
group.occurrences++;
|
|
29
|
+
group.totalTokenCost += dup.tokenCost;
|
|
30
|
+
group.averageSimilarity += dup.similarity;
|
|
31
|
+
group.patternTypes.add(dup.patternType);
|
|
32
|
+
group.lineRanges.push({
|
|
33
|
+
file1: { start: dup.line1, end: dup.endLine1 },
|
|
34
|
+
file2: { start: dup.line2, end: dup.endLine2 }
|
|
35
|
+
});
|
|
36
|
+
const currentSev = dup.severity;
|
|
37
|
+
if (getSeverityLevel(currentSev) > getSeverityLevel(group.severity)) {
|
|
38
|
+
group.severity = currentSev;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return Array.from(groups.values()).map((g) => ({
|
|
42
|
+
...g,
|
|
43
|
+
averageSimilarity: g.averageSimilarity / g.occurrences
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
function createRefactorClusters(duplicates) {
|
|
47
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
48
|
+
const visited = /* @__PURE__ */ new Set();
|
|
49
|
+
const components = [];
|
|
50
|
+
for (const dup of duplicates) {
|
|
51
|
+
if (!adjacency.has(dup.file1)) adjacency.set(dup.file1, /* @__PURE__ */ new Set());
|
|
52
|
+
if (!adjacency.has(dup.file2)) adjacency.set(dup.file2, /* @__PURE__ */ new Set());
|
|
53
|
+
adjacency.get(dup.file1).add(dup.file2);
|
|
54
|
+
adjacency.get(dup.file2).add(dup.file1);
|
|
55
|
+
}
|
|
56
|
+
for (const file of adjacency.keys()) {
|
|
57
|
+
if (visited.has(file)) continue;
|
|
58
|
+
const component = [];
|
|
59
|
+
const queue = [file];
|
|
60
|
+
visited.add(file);
|
|
61
|
+
while (queue.length > 0) {
|
|
62
|
+
const curr = queue.shift();
|
|
63
|
+
component.push(curr);
|
|
64
|
+
for (const neighbor of adjacency.get(curr) || []) {
|
|
65
|
+
if (!visited.has(neighbor)) {
|
|
66
|
+
visited.add(neighbor);
|
|
67
|
+
queue.push(neighbor);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
components.push(component);
|
|
72
|
+
}
|
|
73
|
+
const clusters = [];
|
|
74
|
+
for (const component of components) {
|
|
75
|
+
if (component.length < 2) continue;
|
|
76
|
+
const componentDups = duplicates.filter(
|
|
77
|
+
(d) => component.includes(d.file1) && component.includes(d.file2)
|
|
78
|
+
);
|
|
79
|
+
const totalTokenCost = componentDups.reduce(
|
|
80
|
+
(sum, d) => sum + d.tokenCost,
|
|
81
|
+
0
|
|
82
|
+
);
|
|
83
|
+
const avgSimilarity = componentDups.reduce((sum, d) => sum + d.similarity, 0) / Math.max(1, componentDups.length);
|
|
84
|
+
const name = determineClusterName(component);
|
|
85
|
+
const { severity, reason, suggestion } = calculateSeverity(
|
|
86
|
+
component[0],
|
|
87
|
+
component[1],
|
|
88
|
+
"",
|
|
89
|
+
// Code not available here
|
|
90
|
+
avgSimilarity,
|
|
91
|
+
30
|
|
92
|
+
// Assume substantial if clustered
|
|
93
|
+
);
|
|
94
|
+
clusters.push({
|
|
95
|
+
id: `cluster-${clusters.length}`,
|
|
96
|
+
name,
|
|
97
|
+
files: component,
|
|
98
|
+
severity,
|
|
99
|
+
duplicateCount: componentDups.length,
|
|
100
|
+
totalTokenCost,
|
|
101
|
+
averageSimilarity: avgSimilarity,
|
|
102
|
+
reason,
|
|
103
|
+
suggestion
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return clusters;
|
|
107
|
+
}
|
|
108
|
+
function determineClusterName(files) {
|
|
109
|
+
if (files.length === 0) return "Unknown Cluster";
|
|
110
|
+
if (files.some((f) => f.includes("blog"))) return "Blog SEO Boilerplate";
|
|
111
|
+
if (files.some((f) => f.includes("buttons")))
|
|
112
|
+
return "Button Component Variants";
|
|
113
|
+
if (files.some((f) => f.includes("cards"))) return "Card Component Variants";
|
|
114
|
+
if (files.some((f) => f.includes("login.test"))) return "E2E Test Patterns";
|
|
115
|
+
const first = files[0];
|
|
116
|
+
const dirName = path.dirname(first).split(path.sep).pop();
|
|
117
|
+
if (dirName && dirName !== "." && dirName !== "..") {
|
|
118
|
+
return `${dirName.charAt(0).toUpperCase() + dirName.slice(1)} Domain Group`;
|
|
119
|
+
}
|
|
120
|
+
return "Shared Pattern Group";
|
|
121
|
+
}
|
|
122
|
+
function filterClustersByImpact(clusters, minTokenCost = 1e3, minFiles = 3) {
|
|
123
|
+
return clusters.filter(
|
|
124
|
+
(c) => c.totalTokenCost >= minTokenCost && c.files.length >= minFiles
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/analyzer.ts
|
|
129
|
+
import { scanFiles, readFileContent, Severity as Severity2, IssueType } from "@aiready/core";
|
|
130
|
+
function getRefactoringSuggestion(patternType, similarity) {
|
|
131
|
+
const baseMessages = {
|
|
132
|
+
"api-handler": "Extract common middleware or create a base handler class",
|
|
133
|
+
validator: "Consolidate validation logic into shared schema validators (Zod/Yup)",
|
|
134
|
+
utility: "Move to a shared utilities file and reuse across modules",
|
|
135
|
+
"class-method": "Consider inheritance or composition to share behavior",
|
|
136
|
+
component: "Extract shared logic into a custom hook or HOC",
|
|
137
|
+
function: "Extract into a shared helper function",
|
|
138
|
+
unknown: "Extract common logic into a reusable module"
|
|
139
|
+
};
|
|
140
|
+
const urgency = similarity > 0.95 ? " (CRITICAL: Nearly identical code)" : similarity > 0.9 ? " (HIGH: Very similar, refactor soon)" : "";
|
|
141
|
+
return baseMessages[patternType] + urgency;
|
|
142
|
+
}
|
|
143
|
+
async function getSmartDefaults(directory, userOptions) {
|
|
144
|
+
if (userOptions.useSmartDefaults === false) {
|
|
145
|
+
return {
|
|
146
|
+
rootDir: directory,
|
|
147
|
+
minSimilarity: 0.6,
|
|
148
|
+
minLines: 8,
|
|
149
|
+
batchSize: 100,
|
|
150
|
+
approx: true,
|
|
151
|
+
minSharedTokens: 12,
|
|
152
|
+
maxCandidatesPerBlock: 5,
|
|
153
|
+
streamResults: false,
|
|
154
|
+
severity: "all",
|
|
155
|
+
includeTests: false
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const scanOptions = {
|
|
159
|
+
rootDir: directory,
|
|
160
|
+
include: userOptions.include || ["**/*.{ts,tsx,js,jsx,py,java}"],
|
|
161
|
+
exclude: userOptions.exclude
|
|
162
|
+
};
|
|
163
|
+
const files = await scanFiles(scanOptions);
|
|
164
|
+
const fileCount = files.length;
|
|
165
|
+
const estimatedBlocks = fileCount * 5;
|
|
166
|
+
const minLines = Math.max(
|
|
167
|
+
6,
|
|
168
|
+
Math.min(20, 6 + Math.floor(estimatedBlocks / 1e3) * 2)
|
|
169
|
+
);
|
|
170
|
+
const minSimilarity = Math.min(0.75, 0.45 + estimatedBlocks / 1e4 * 0.3);
|
|
171
|
+
const batchSize = estimatedBlocks > 1e3 ? 200 : 100;
|
|
172
|
+
const severity = estimatedBlocks > 3e3 ? "high" : "all";
|
|
173
|
+
const maxCandidatesPerBlock = Math.max(
|
|
174
|
+
5,
|
|
175
|
+
Math.min(100, Math.floor(1e6 / estimatedBlocks))
|
|
176
|
+
);
|
|
177
|
+
const defaults = {
|
|
178
|
+
rootDir: directory,
|
|
179
|
+
minSimilarity,
|
|
180
|
+
minLines,
|
|
181
|
+
batchSize,
|
|
182
|
+
approx: true,
|
|
183
|
+
minSharedTokens: 10,
|
|
184
|
+
maxCandidatesPerBlock,
|
|
185
|
+
streamResults: false,
|
|
186
|
+
severity,
|
|
187
|
+
includeTests: false
|
|
188
|
+
};
|
|
189
|
+
const result = { ...defaults };
|
|
190
|
+
for (const key of Object.keys(defaults)) {
|
|
191
|
+
if (key in userOptions && userOptions[key] !== void 0) {
|
|
192
|
+
result[key] = userOptions[key];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
function logConfiguration(config, estimatedBlocks) {
|
|
198
|
+
if (config.suppressToolConfig) return;
|
|
199
|
+
console.log("\u{1F4CB} Configuration:");
|
|
200
|
+
console.log(` Repository size: ~${estimatedBlocks} code blocks`);
|
|
201
|
+
console.log(` Similarity threshold: ${config.minSimilarity}`);
|
|
202
|
+
console.log(` Minimum lines: ${config.minLines}`);
|
|
203
|
+
console.log(` Approximate mode: ${config.approx ? "enabled" : "disabled"}`);
|
|
204
|
+
console.log(` Max candidates per block: ${config.maxCandidatesPerBlock}`);
|
|
205
|
+
console.log(` Min shared tokens: ${config.minSharedTokens}`);
|
|
206
|
+
console.log(` Severity filter: ${config.severity}`);
|
|
207
|
+
console.log(` Include tests: ${config.includeTests}`);
|
|
208
|
+
if (config.excludePatterns && config.excludePatterns.length > 0) {
|
|
209
|
+
console.log(` Exclude patterns: ${config.excludePatterns.length} active`);
|
|
210
|
+
}
|
|
211
|
+
if (config.confidenceThreshold && config.confidenceThreshold > 0) {
|
|
212
|
+
console.log(` Confidence threshold: ${config.confidenceThreshold}`);
|
|
213
|
+
}
|
|
214
|
+
if (config.ignoreWhitelist && config.ignoreWhitelist.length > 0) {
|
|
215
|
+
console.log(
|
|
216
|
+
` Ignore whitelist: ${config.ignoreWhitelist.length} entries`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
console.log("");
|
|
220
|
+
}
|
|
221
|
+
async function analyzePatterns(options) {
|
|
222
|
+
const smartDefaults = await getSmartDefaults(options.rootDir || ".", options);
|
|
223
|
+
const finalOptions = { ...smartDefaults, ...options };
|
|
224
|
+
const {
|
|
225
|
+
minSimilarity = 0.4,
|
|
226
|
+
minLines = 5,
|
|
227
|
+
batchSize = 100,
|
|
228
|
+
approx = true,
|
|
229
|
+
minSharedTokens = 8,
|
|
230
|
+
maxCandidatesPerBlock = 100,
|
|
231
|
+
streamResults = false,
|
|
232
|
+
severity = "all",
|
|
233
|
+
groupByFilePair = true,
|
|
234
|
+
createClusters = true,
|
|
235
|
+
minClusterTokenCost = 1e3,
|
|
236
|
+
minClusterFiles = 3,
|
|
237
|
+
excludePatterns = [],
|
|
238
|
+
confidenceThreshold = 0,
|
|
239
|
+
ignoreWhitelist = [],
|
|
240
|
+
...scanOptions
|
|
241
|
+
} = finalOptions;
|
|
242
|
+
const files = await scanFiles(scanOptions);
|
|
243
|
+
const estimatedBlocks = files.length * 3;
|
|
244
|
+
logConfiguration(finalOptions, estimatedBlocks);
|
|
245
|
+
const results = [];
|
|
246
|
+
const READ_BATCH_SIZE = 50;
|
|
247
|
+
const fileContents = [];
|
|
248
|
+
for (let i = 0; i < files.length; i += READ_BATCH_SIZE) {
|
|
249
|
+
const batch = files.slice(i, i + READ_BATCH_SIZE);
|
|
250
|
+
const batchContents = await Promise.all(
|
|
251
|
+
batch.map(async (file) => ({
|
|
252
|
+
file,
|
|
253
|
+
content: await readFileContent(file)
|
|
254
|
+
}))
|
|
255
|
+
);
|
|
256
|
+
fileContents.push(...batchContents);
|
|
257
|
+
}
|
|
258
|
+
const duplicates = await detectDuplicatePatterns(fileContents, {
|
|
259
|
+
minSimilarity,
|
|
260
|
+
minLines,
|
|
261
|
+
batchSize,
|
|
262
|
+
approx,
|
|
263
|
+
minSharedTokens,
|
|
264
|
+
maxCandidatesPerBlock,
|
|
265
|
+
streamResults,
|
|
266
|
+
excludePatterns,
|
|
267
|
+
confidenceThreshold,
|
|
268
|
+
ignoreWhitelist,
|
|
269
|
+
onProgress: options.onProgress
|
|
270
|
+
});
|
|
271
|
+
for (const file of files) {
|
|
272
|
+
const fileDuplicates = duplicates.filter(
|
|
273
|
+
(dup) => dup.file1 === file || dup.file2 === file
|
|
274
|
+
);
|
|
275
|
+
const issues = fileDuplicates.map((dup) => {
|
|
276
|
+
const otherFile = dup.file1 === file ? dup.file2 : dup.file1;
|
|
277
|
+
const severity2 = dup.similarity > 0.95 ? Severity2.Critical : dup.similarity > 0.9 ? Severity2.Major : Severity2.Minor;
|
|
278
|
+
return {
|
|
279
|
+
type: IssueType.DuplicatePattern,
|
|
280
|
+
severity: severity2,
|
|
281
|
+
message: `${dup.patternType} pattern ${Math.round(dup.similarity * 100)}% similar to ${otherFile} (${dup.tokenCost} tokens wasted)`,
|
|
282
|
+
location: {
|
|
283
|
+
file,
|
|
284
|
+
line: dup.file1 === file ? dup.line1 : dup.line2
|
|
285
|
+
},
|
|
286
|
+
suggestion: getRefactoringSuggestion(dup.patternType, dup.similarity)
|
|
287
|
+
};
|
|
288
|
+
});
|
|
289
|
+
let filteredIssues = issues;
|
|
290
|
+
if (severity !== "all") {
|
|
291
|
+
const severityMap = {
|
|
292
|
+
critical: [Severity2.Critical],
|
|
293
|
+
high: [Severity2.Critical, Severity2.Major],
|
|
294
|
+
medium: [Severity2.Critical, Severity2.Major, Severity2.Minor]
|
|
295
|
+
};
|
|
296
|
+
const allowedSeverities = severityMap[severity] || [Severity2.Critical, Severity2.Major, Severity2.Minor];
|
|
297
|
+
filteredIssues = issues.filter(
|
|
298
|
+
(issue) => allowedSeverities.includes(issue.severity)
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const totalTokenCost = fileDuplicates.reduce(
|
|
302
|
+
(sum, dup) => sum + dup.tokenCost,
|
|
303
|
+
0
|
|
304
|
+
);
|
|
305
|
+
results.push({
|
|
306
|
+
fileName: file,
|
|
307
|
+
issues: filteredIssues,
|
|
308
|
+
metrics: {
|
|
309
|
+
tokenCost: totalTokenCost,
|
|
310
|
+
consistencyScore: Math.max(0, 1 - fileDuplicates.length * 0.1)
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
let groups;
|
|
315
|
+
let clusters;
|
|
316
|
+
if (groupByFilePair) {
|
|
317
|
+
groups = groupDuplicatesByFilePair(duplicates);
|
|
318
|
+
}
|
|
319
|
+
if (createClusters) {
|
|
320
|
+
const allClusters = createRefactorClusters(duplicates);
|
|
321
|
+
clusters = filterClustersByImpact(
|
|
322
|
+
allClusters,
|
|
323
|
+
minClusterTokenCost,
|
|
324
|
+
minClusterFiles
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return { results, duplicates, files, groups, clusters, config: finalOptions };
|
|
328
|
+
}
|
|
329
|
+
function generateSummary(results) {
|
|
330
|
+
const allIssues = results.flatMap((r) => r.issues || []);
|
|
331
|
+
const totalTokenCost = results.reduce(
|
|
332
|
+
(sum, r) => sum + (r.metrics?.tokenCost || 0),
|
|
333
|
+
0
|
|
334
|
+
);
|
|
335
|
+
const patternsByType = {
|
|
336
|
+
"api-handler": 0,
|
|
337
|
+
validator: 0,
|
|
338
|
+
utility: 0,
|
|
339
|
+
"class-method": 0,
|
|
340
|
+
component: 0,
|
|
341
|
+
function: 0,
|
|
342
|
+
unknown: 0
|
|
343
|
+
};
|
|
344
|
+
allIssues.forEach((issue) => {
|
|
345
|
+
const match = issue.message.match(/^(\S+(?:-\S+)*) pattern/);
|
|
346
|
+
if (match) {
|
|
347
|
+
const type = match[1];
|
|
348
|
+
patternsByType[type] = (patternsByType[type] || 0) + 1;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
const topDuplicates = allIssues.slice(0, 10).map((issue) => {
|
|
352
|
+
const similarityMatch = issue.message.match(/(\d+)% similar/);
|
|
353
|
+
const tokenMatch = issue.message.match(/\((\d+) tokens/);
|
|
354
|
+
const typeMatch = issue.message.match(/^(\S+(?:-\S+)*) pattern/);
|
|
355
|
+
const fileMatch = issue.message.match(/similar to (.+?) \(/);
|
|
356
|
+
return {
|
|
357
|
+
files: [
|
|
358
|
+
{
|
|
359
|
+
path: issue.location.file,
|
|
360
|
+
startLine: issue.location.line,
|
|
361
|
+
endLine: 0
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
path: fileMatch?.[1] || "unknown",
|
|
365
|
+
startLine: 0,
|
|
366
|
+
endLine: 0
|
|
367
|
+
}
|
|
368
|
+
],
|
|
369
|
+
similarity: similarityMatch ? parseInt(similarityMatch[1]) / 100 : 0,
|
|
370
|
+
confidence: similarityMatch ? parseInt(similarityMatch[1]) / 100 : 0,
|
|
371
|
+
// Fallback for summary
|
|
372
|
+
patternType: typeMatch?.[1] || "unknown",
|
|
373
|
+
tokenCost: tokenMatch ? parseInt(tokenMatch[1]) : 0
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
return {
|
|
377
|
+
totalPatterns: allIssues.length,
|
|
378
|
+
totalTokenCost,
|
|
379
|
+
patternsByType,
|
|
380
|
+
topDuplicates
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export {
|
|
385
|
+
groupDuplicatesByFilePair,
|
|
386
|
+
createRefactorClusters,
|
|
387
|
+
filterClustersByImpact,
|
|
388
|
+
getSmartDefaults,
|
|
389
|
+
analyzePatterns,
|
|
390
|
+
generateSummary
|
|
391
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -26,12 +26,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
// src/cli.ts
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
|
|
29
|
-
// src/index.ts
|
|
30
|
-
var import_core7 = require("@aiready/core");
|
|
31
|
-
|
|
32
|
-
// src/provider.ts
|
|
33
|
-
var import_core6 = require("@aiready/core");
|
|
34
|
-
|
|
35
29
|
// src/analyzer.ts
|
|
36
30
|
var import_core4 = require("@aiready/core");
|
|
37
31
|
|
|
@@ -468,7 +462,7 @@ async function getSmartDefaults(directory, userOptions) {
|
|
|
468
462
|
6,
|
|
469
463
|
Math.min(20, 6 + Math.floor(estimatedBlocks / 1e3) * 2)
|
|
470
464
|
);
|
|
471
|
-
const minSimilarity = Math.min(0.
|
|
465
|
+
const minSimilarity = Math.min(0.75, 0.45 + estimatedBlocks / 1e4 * 0.3);
|
|
472
466
|
const batchSize = estimatedBlocks > 1e3 ? 200 : 100;
|
|
473
467
|
const severity = estimatedBlocks > 3e3 ? "high" : "all";
|
|
474
468
|
const maxCandidatesPerBlock = Math.max(
|
|
@@ -628,9 +622,9 @@ async function analyzePatterns(options) {
|
|
|
628
622
|
return { results, duplicates, files, groups, clusters, config: finalOptions };
|
|
629
623
|
}
|
|
630
624
|
function generateSummary(results) {
|
|
631
|
-
const allIssues = results.flatMap((r) => r.issues);
|
|
625
|
+
const allIssues = results.flatMap((r) => r.issues || []);
|
|
632
626
|
const totalTokenCost = results.reduce(
|
|
633
|
-
(sum, r) => sum + (r.metrics
|
|
627
|
+
(sum, r) => sum + (r.metrics?.tokenCost || 0),
|
|
634
628
|
0
|
|
635
629
|
);
|
|
636
630
|
const patternsByType = {
|
|
@@ -682,163 +676,14 @@ function generateSummary(results) {
|
|
|
682
676
|
};
|
|
683
677
|
}
|
|
684
678
|
|
|
685
|
-
// src/scoring.ts
|
|
686
|
-
var import_core5 = require("@aiready/core");
|
|
687
|
-
function calculatePatternScore(duplicates, totalFilesAnalyzed, costConfig) {
|
|
688
|
-
const totalDuplicates = duplicates.length;
|
|
689
|
-
const totalTokenCost = duplicates.reduce((sum, d) => sum + d.tokenCost, 0);
|
|
690
|
-
const highImpactDuplicates = duplicates.filter(
|
|
691
|
-
(d) => d.tokenCost > 1e3 || d.similarity > 0.7
|
|
692
|
-
).length;
|
|
693
|
-
if (totalFilesAnalyzed === 0) {
|
|
694
|
-
return {
|
|
695
|
-
toolName: import_core5.ToolName.PatternDetect,
|
|
696
|
-
score: 100,
|
|
697
|
-
rawMetrics: {
|
|
698
|
-
totalDuplicates: 0,
|
|
699
|
-
totalTokenCost: 0,
|
|
700
|
-
highImpactDuplicates: 0,
|
|
701
|
-
totalFilesAnalyzed: 0
|
|
702
|
-
},
|
|
703
|
-
factors: [],
|
|
704
|
-
recommendations: []
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
const duplicatesPerFile = totalDuplicates / totalFilesAnalyzed * 100;
|
|
708
|
-
const tokenWastePerFile = totalTokenCost / totalFilesAnalyzed;
|
|
709
|
-
const duplicatesPenalty = Math.min(60, duplicatesPerFile * 0.6);
|
|
710
|
-
const tokenPenalty = Math.min(40, tokenWastePerFile / 125);
|
|
711
|
-
const highImpactPenalty = highImpactDuplicates > 0 ? Math.min(15, highImpactDuplicates * 2 - 5) : -5;
|
|
712
|
-
const score = 100 - duplicatesPenalty - tokenPenalty - highImpactPenalty;
|
|
713
|
-
const finalScore = Math.max(0, Math.min(100, Math.round(score)));
|
|
714
|
-
const factors = [
|
|
715
|
-
{
|
|
716
|
-
name: "Duplication Density",
|
|
717
|
-
impact: -Math.round(duplicatesPenalty),
|
|
718
|
-
description: `${duplicatesPerFile.toFixed(1)} duplicates per 100 files`
|
|
719
|
-
},
|
|
720
|
-
{
|
|
721
|
-
name: "Token Waste",
|
|
722
|
-
impact: -Math.round(tokenPenalty),
|
|
723
|
-
description: `${Math.round(tokenWastePerFile)} tokens wasted per file`
|
|
724
|
-
}
|
|
725
|
-
];
|
|
726
|
-
if (highImpactDuplicates > 0) {
|
|
727
|
-
factors.push({
|
|
728
|
-
name: "High-Impact Patterns",
|
|
729
|
-
impact: -Math.round(highImpactPenalty),
|
|
730
|
-
description: `${highImpactDuplicates} high-impact duplicates (>1000 tokens or >70% similar)`
|
|
731
|
-
});
|
|
732
|
-
} else {
|
|
733
|
-
factors.push({
|
|
734
|
-
name: "No High-Impact Patterns",
|
|
735
|
-
impact: 5,
|
|
736
|
-
description: "No severe duplicates detected"
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
const recommendations = [];
|
|
740
|
-
if (highImpactDuplicates > 0) {
|
|
741
|
-
const estimatedImpact = Math.min(15, highImpactDuplicates * 3);
|
|
742
|
-
recommendations.push({
|
|
743
|
-
action: `Deduplicate ${highImpactDuplicates} high-impact pattern${highImpactDuplicates > 1 ? "s" : ""}`,
|
|
744
|
-
estimatedImpact,
|
|
745
|
-
priority: "high"
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
if (totalDuplicates > 10 && duplicatesPerFile > 20) {
|
|
749
|
-
const estimatedImpact = Math.min(10, Math.round(duplicatesPenalty * 0.3));
|
|
750
|
-
recommendations.push({
|
|
751
|
-
action: "Extract common patterns into shared utilities",
|
|
752
|
-
estimatedImpact,
|
|
753
|
-
priority: "medium"
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
if (tokenWastePerFile > 2e3) {
|
|
757
|
-
const estimatedImpact = Math.min(8, Math.round(tokenPenalty * 0.4));
|
|
758
|
-
recommendations.push({
|
|
759
|
-
action: "Consolidate duplicated logic to reduce AI context waste",
|
|
760
|
-
estimatedImpact,
|
|
761
|
-
priority: totalTokenCost > 1e4 ? "high" : "medium"
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
const cfg = { ...import_core5.DEFAULT_COST_CONFIG, ...costConfig };
|
|
765
|
-
const estimatedMonthlyCost = (0, import_core5.calculateMonthlyCost)(totalTokenCost, cfg);
|
|
766
|
-
const issues = duplicates.map((d) => ({
|
|
767
|
-
severity: d.severity === "critical" ? "critical" : d.severity === "major" ? "major" : "minor"
|
|
768
|
-
}));
|
|
769
|
-
const productivityImpact = (0, import_core5.calculateProductivityImpact)(issues);
|
|
770
|
-
return {
|
|
771
|
-
toolName: "pattern-detect",
|
|
772
|
-
score: finalScore,
|
|
773
|
-
rawMetrics: {
|
|
774
|
-
totalDuplicates,
|
|
775
|
-
totalTokenCost,
|
|
776
|
-
highImpactDuplicates,
|
|
777
|
-
totalFilesAnalyzed,
|
|
778
|
-
duplicatesPerFile: Math.round(duplicatesPerFile * 10) / 10,
|
|
779
|
-
tokenWastePerFile: Math.round(tokenWastePerFile),
|
|
780
|
-
// Business value metrics
|
|
781
|
-
estimatedMonthlyCost,
|
|
782
|
-
estimatedDeveloperHours: productivityImpact.totalHours
|
|
783
|
-
},
|
|
784
|
-
factors,
|
|
785
|
-
recommendations
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// src/provider.ts
|
|
790
|
-
var PatternDetectProvider = {
|
|
791
|
-
id: import_core6.ToolName.PatternDetect,
|
|
792
|
-
alias: ["patterns", "duplicates", "duplication"],
|
|
793
|
-
async analyze(options) {
|
|
794
|
-
const results = await analyzePatterns(options);
|
|
795
|
-
return import_core6.SpokeOutputSchema.parse({
|
|
796
|
-
results: results.results,
|
|
797
|
-
summary: {
|
|
798
|
-
totalFiles: results.files.length,
|
|
799
|
-
totalIssues: results.results.reduce(
|
|
800
|
-
(sum, r) => sum + r.issues.length,
|
|
801
|
-
0
|
|
802
|
-
),
|
|
803
|
-
duplicates: results.duplicates,
|
|
804
|
-
// Keep the raw duplicates for score calculation
|
|
805
|
-
clusters: results.clusters,
|
|
806
|
-
config: Object.fromEntries(
|
|
807
|
-
Object.entries(results.config).filter(
|
|
808
|
-
([key]) => !import_core6.GLOBAL_SCAN_OPTIONS.includes(key) || key === "rootDir"
|
|
809
|
-
)
|
|
810
|
-
)
|
|
811
|
-
},
|
|
812
|
-
metadata: {
|
|
813
|
-
toolName: import_core6.ToolName.PatternDetect,
|
|
814
|
-
version: "0.12.5",
|
|
815
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
816
|
-
}
|
|
817
|
-
});
|
|
818
|
-
},
|
|
819
|
-
score(output, options) {
|
|
820
|
-
const duplicates = output.summary.duplicates || [];
|
|
821
|
-
const totalFiles = output.summary.totalFiles || output.results.length;
|
|
822
|
-
return calculatePatternScore(
|
|
823
|
-
duplicates,
|
|
824
|
-
totalFiles,
|
|
825
|
-
options.costConfig
|
|
826
|
-
);
|
|
827
|
-
},
|
|
828
|
-
defaultWeight: 22
|
|
829
|
-
};
|
|
830
|
-
|
|
831
|
-
// src/index.ts
|
|
832
|
-
import_core7.ToolRegistry.register(PatternDetectProvider);
|
|
833
|
-
|
|
834
679
|
// src/cli-action.ts
|
|
835
680
|
var import_chalk2 = __toESM(require("chalk"));
|
|
836
681
|
var import_fs = require("fs");
|
|
837
682
|
var import_path2 = require("path");
|
|
838
|
-
var
|
|
683
|
+
var import_core8 = require("@aiready/core");
|
|
839
684
|
|
|
840
685
|
// src/config-resolver.ts
|
|
841
|
-
var
|
|
686
|
+
var import_core5 = require("@aiready/core");
|
|
842
687
|
|
|
843
688
|
// src/constants.ts
|
|
844
689
|
var DEFAULT_MIN_SIMILARITY = 0.4;
|
|
@@ -870,7 +715,7 @@ EXAMPLES:
|
|
|
870
715
|
|
|
871
716
|
// src/config-resolver.ts
|
|
872
717
|
async function resolvePatternConfig(directory, options) {
|
|
873
|
-
const fileConfig = await (0,
|
|
718
|
+
const fileConfig = await (0, import_core5.loadConfig)(directory);
|
|
874
719
|
const defaults = {
|
|
875
720
|
minSimilarity: DEFAULT_MIN_SIMILARITY,
|
|
876
721
|
minLines: DEFAULT_MIN_LINES,
|
|
@@ -884,7 +729,7 @@ async function resolvePatternConfig(directory, options) {
|
|
|
884
729
|
excludePatterns: void 0,
|
|
885
730
|
confidenceThreshold: 0,
|
|
886
731
|
ignoreWhitelist: void 0,
|
|
887
|
-
minSeverity:
|
|
732
|
+
minSeverity: import_core5.Severity.Minor,
|
|
888
733
|
excludeTestFixtures: false,
|
|
889
734
|
excludeTemplates: false,
|
|
890
735
|
includeTests: false,
|
|
@@ -895,7 +740,7 @@ async function resolvePatternConfig(directory, options) {
|
|
|
895
740
|
minClusterFiles: DEFAULT_MIN_CLUSTER_FILES,
|
|
896
741
|
showRawDuplicates: false
|
|
897
742
|
};
|
|
898
|
-
const mergedConfig = (0,
|
|
743
|
+
const mergedConfig = (0, import_core5.mergeConfigWithDefaults)(fileConfig, defaults);
|
|
899
744
|
const finalOptions = {
|
|
900
745
|
rootDir: directory,
|
|
901
746
|
minSimilarity: options.similarity ? parseFloat(options.similarity) : mergedConfig.minSimilarity,
|
|
@@ -937,7 +782,7 @@ async function resolvePatternConfig(directory, options) {
|
|
|
937
782
|
}
|
|
938
783
|
|
|
939
784
|
// src/cli-output.ts
|
|
940
|
-
var
|
|
785
|
+
var import_core6 = require("@aiready/core");
|
|
941
786
|
function getPatternIcon(type) {
|
|
942
787
|
const icons = {
|
|
943
788
|
"api-handler": "\u{1F50C}",
|
|
@@ -963,7 +808,7 @@ function generateHTMLReport(results, summary) {
|
|
|
963
808
|
dup.files.map((f) => `<code>${f.path}:${f.startLine}-${f.endLine}</code>`).join("<br>\u2194<br>"),
|
|
964
809
|
dup.tokenCost.toLocaleString()
|
|
965
810
|
]);
|
|
966
|
-
return (0,
|
|
811
|
+
return (0, import_core6.generateStandardHtmlReport)(
|
|
967
812
|
{
|
|
968
813
|
title: "Pattern Detection Report",
|
|
969
814
|
packageName: "pattern-detect",
|
|
@@ -980,7 +825,7 @@ function generateHTMLReport(results, summary) {
|
|
|
980
825
|
[
|
|
981
826
|
{
|
|
982
827
|
title: "Duplicate Patterns",
|
|
983
|
-
content: (0,
|
|
828
|
+
content: (0, import_core6.generateTable)({
|
|
984
829
|
headers: ["Similarity", "Type", "Locations", "Tokens Wasted"],
|
|
985
830
|
rows: tableRows
|
|
986
831
|
})
|
|
@@ -992,9 +837,9 @@ function generateHTMLReport(results, summary) {
|
|
|
992
837
|
|
|
993
838
|
// src/terminal-output.ts
|
|
994
839
|
var import_chalk = __toESM(require("chalk"));
|
|
995
|
-
var
|
|
840
|
+
var import_core7 = require("@aiready/core");
|
|
996
841
|
function printAnalysisSummary(resultsLength, totalIssues, totalTokenCost, elapsedTime) {
|
|
997
|
-
(0,
|
|
842
|
+
(0, import_core7.printTerminalHeader)("PATTERN ANALYSIS SUMMARY");
|
|
998
843
|
console.log(import_chalk.default.white(`\u{1F4C1} Files analyzed: ${import_chalk.default.bold(resultsLength)}`));
|
|
999
844
|
console.log(
|
|
1000
845
|
import_chalk.default.yellow(
|
|
@@ -1011,9 +856,9 @@ function printAnalysisSummary(resultsLength, totalIssues, totalTokenCost, elapse
|
|
|
1011
856
|
function printPatternBreakdown(patternsByType) {
|
|
1012
857
|
const sortedTypes = Object.entries(patternsByType).filter(([, count]) => count > 0).sort(([, a], [, b]) => b - a);
|
|
1013
858
|
if (sortedTypes.length > 0) {
|
|
1014
|
-
console.log("\n" + (0,
|
|
859
|
+
console.log("\n" + (0, import_core7.getTerminalDivider)());
|
|
1015
860
|
console.log(import_chalk.default.bold.white(" PATTERNS BY TYPE"));
|
|
1016
|
-
console.log((0,
|
|
861
|
+
console.log((0, import_core7.getTerminalDivider)() + "\n");
|
|
1017
862
|
sortedTypes.forEach(([type, count]) => {
|
|
1018
863
|
const icon = getPatternIcon(type);
|
|
1019
864
|
console.log(
|
|
@@ -1024,20 +869,20 @@ function printPatternBreakdown(patternsByType) {
|
|
|
1024
869
|
}
|
|
1025
870
|
function printDuplicateGroups(groups, maxResults) {
|
|
1026
871
|
if (groups.length === 0) return;
|
|
1027
|
-
console.log("\n" + (0,
|
|
872
|
+
console.log("\n" + (0, import_core7.getTerminalDivider)());
|
|
1028
873
|
console.log(
|
|
1029
874
|
import_chalk.default.bold.white(` \u{1F4E6} DUPLICATE GROUPS (${groups.length} file pairs)`)
|
|
1030
875
|
);
|
|
1031
|
-
console.log((0,
|
|
876
|
+
console.log((0, import_core7.getTerminalDivider)() + "\n");
|
|
1032
877
|
const topGroups = groups.sort((a, b) => {
|
|
1033
|
-
const bVal = (0,
|
|
1034
|
-
const aVal = (0,
|
|
878
|
+
const bVal = (0, import_core7.getSeverityValue)(b.severity);
|
|
879
|
+
const aVal = (0, import_core7.getSeverityValue)(a.severity);
|
|
1035
880
|
const severityDiff = bVal - aVal;
|
|
1036
881
|
if (severityDiff !== 0) return severityDiff;
|
|
1037
882
|
return b.totalTokenCost - a.totalTokenCost;
|
|
1038
883
|
}).slice(0, maxResults);
|
|
1039
884
|
topGroups.forEach((group, idx) => {
|
|
1040
|
-
const severityBadge = (0,
|
|
885
|
+
const severityBadge = (0, import_core7.getSeverityBadge)(group.severity);
|
|
1041
886
|
const [file1, file2] = group.filePair.split("::");
|
|
1042
887
|
const file1Name = file1.split("/").pop() || file1;
|
|
1043
888
|
const file2Name = file2.split("/").pop() || file2;
|
|
@@ -1070,13 +915,13 @@ function printDuplicateGroups(groups, maxResults) {
|
|
|
1070
915
|
}
|
|
1071
916
|
function printRefactorClusters(clusters) {
|
|
1072
917
|
if (clusters.length === 0) return;
|
|
1073
|
-
console.log("\n" + (0,
|
|
918
|
+
console.log("\n" + (0, import_core7.getTerminalDivider)());
|
|
1074
919
|
console.log(
|
|
1075
920
|
import_chalk.default.bold.white(` \u{1F3AF} REFACTOR CLUSTERS (${clusters.length} patterns)`)
|
|
1076
921
|
);
|
|
1077
|
-
console.log((0,
|
|
922
|
+
console.log((0, import_core7.getTerminalDivider)() + "\n");
|
|
1078
923
|
clusters.sort((a, b) => b.totalTokenCost - a.totalTokenCost).forEach((cluster, idx) => {
|
|
1079
|
-
const severityBadge = (0,
|
|
924
|
+
const severityBadge = (0, import_core7.getSeverityBadge)(cluster.severity);
|
|
1080
925
|
console.log(`${idx + 1}. ${severityBadge} ${import_chalk.default.bold(cluster.name)}`);
|
|
1081
926
|
console.log(
|
|
1082
927
|
` Total tokens: ${import_chalk.default.bold(cluster.totalTokenCost.toLocaleString())} | Avg similarity: ${import_chalk.default.bold(Math.round(cluster.averageSimilarity * 100) + "%")} | Duplicates: ${import_chalk.default.bold(cluster.duplicateCount)}`
|
|
@@ -1103,18 +948,18 @@ function printRefactorClusters(clusters) {
|
|
|
1103
948
|
}
|
|
1104
949
|
function printRawDuplicates(duplicates, maxResults) {
|
|
1105
950
|
if (duplicates.length === 0) return;
|
|
1106
|
-
console.log("\n" + (0,
|
|
951
|
+
console.log("\n" + (0, import_core7.getTerminalDivider)());
|
|
1107
952
|
console.log(import_chalk.default.bold.white(" TOP DUPLICATE PATTERNS"));
|
|
1108
|
-
console.log((0,
|
|
953
|
+
console.log((0, import_core7.getTerminalDivider)() + "\n");
|
|
1109
954
|
const topDuplicates = duplicates.sort((a, b) => {
|
|
1110
|
-
const bVal = (0,
|
|
1111
|
-
const aVal = (0,
|
|
955
|
+
const bVal = (0, import_core7.getSeverityValue)(b.severity);
|
|
956
|
+
const aVal = (0, import_core7.getSeverityValue)(a.severity);
|
|
1112
957
|
const severityDiff = bVal - aVal;
|
|
1113
958
|
if (severityDiff !== 0) return severityDiff;
|
|
1114
959
|
return b.similarity - a.similarity;
|
|
1115
960
|
}).slice(0, maxResults);
|
|
1116
961
|
topDuplicates.forEach((dup) => {
|
|
1117
|
-
const severityBadge = (0,
|
|
962
|
+
const severityBadge = (0, import_core7.getSeverityBadge)(dup.severity);
|
|
1118
963
|
const file1Name = dup.file1.split("/").pop() || dup.file1;
|
|
1119
964
|
const file2Name = dup.file2.split("/").pop() || dup.file2;
|
|
1120
965
|
console.log(
|
|
@@ -1147,9 +992,9 @@ function printRawDuplicates(duplicates, maxResults) {
|
|
|
1147
992
|
}
|
|
1148
993
|
function printCriticalIssues(issues) {
|
|
1149
994
|
if (issues.length === 0) return;
|
|
1150
|
-
console.log((0,
|
|
995
|
+
console.log((0, import_core7.getTerminalDivider)());
|
|
1151
996
|
console.log(import_chalk.default.bold.white(" CRITICAL ISSUES (>95% similar)"));
|
|
1152
|
-
console.log((0,
|
|
997
|
+
console.log((0, import_core7.getTerminalDivider)() + "\n");
|
|
1153
998
|
issues.slice(0, 5).forEach((issue) => {
|
|
1154
999
|
console.log(
|
|
1155
1000
|
import_chalk.default.red("\u25CF ") + import_chalk.default.white(`${issue.file}:${issue.location.line}`)
|
|
@@ -1189,8 +1034,11 @@ async function patternActionHandler(directory, options) {
|
|
|
1189
1034
|
);
|
|
1190
1035
|
}
|
|
1191
1036
|
const elapsedTime = ((Date.now() - startTime) / 1e3).toFixed(2);
|
|
1192
|
-
const summary = generateSummary(results);
|
|
1193
|
-
const totalIssues = results.reduce(
|
|
1037
|
+
const summary = generateSummary(results || []);
|
|
1038
|
+
const totalIssues = (results || []).reduce(
|
|
1039
|
+
(sum, r) => sum + (r.issues?.length || 0),
|
|
1040
|
+
0
|
|
1041
|
+
);
|
|
1194
1042
|
if (options.output === "json") {
|
|
1195
1043
|
handleJsonOutput(options.outputFile, directory, {
|
|
1196
1044
|
summary,
|
|
@@ -1218,7 +1066,7 @@ async function patternActionHandler(directory, options) {
|
|
|
1218
1066
|
);
|
|
1219
1067
|
}
|
|
1220
1068
|
function handleJsonOutput(outputFile, directory, data) {
|
|
1221
|
-
const outputPath = (0,
|
|
1069
|
+
const outputPath = (0, import_core8.resolveOutputPath)(
|
|
1222
1070
|
outputFile,
|
|
1223
1071
|
`pattern-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
|
|
1224
1072
|
directory
|
|
@@ -1231,7 +1079,7 @@ function handleJsonOutput(outputFile, directory, data) {
|
|
|
1231
1079
|
}
|
|
1232
1080
|
function handleHtmlOutput(outputFile, directory, summary, results) {
|
|
1233
1081
|
const html = generateHTMLReport(summary, results);
|
|
1234
|
-
const outputPath = (0,
|
|
1082
|
+
const outputPath = (0, import_core8.resolveOutputPath)(
|
|
1235
1083
|
outputFile,
|
|
1236
1084
|
`pattern-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.html`,
|
|
1237
1085
|
directory
|
|
@@ -1266,7 +1114,7 @@ function renderTerminalOutput(fileCount, totalIssues, summary, elapsedTime, opti
|
|
|
1266
1114
|
} else if (totalIssues < 5) {
|
|
1267
1115
|
printGuidance();
|
|
1268
1116
|
}
|
|
1269
|
-
console.log((0,
|
|
1117
|
+
console.log((0, import_core8.getTerminalDivider)());
|
|
1270
1118
|
if (totalIssues > 0) {
|
|
1271
1119
|
console.log(
|
|
1272
1120
|
import_chalk2.default.white(
|
|
@@ -1278,7 +1126,7 @@ function renderTerminalOutput(fileCount, totalIssues, summary, elapsedTime, opti
|
|
|
1278
1126
|
printFooter();
|
|
1279
1127
|
}
|
|
1280
1128
|
function resultsToCriticalIssues(summary, duplicates) {
|
|
1281
|
-
return duplicates.filter((d) => (0,
|
|
1129
|
+
return duplicates.filter((d) => (0, import_core8.getSeverityValue)(d.severity) === 4).map((d) => ({
|
|
1282
1130
|
file: d.file1,
|
|
1283
1131
|
location: { line: d.line1 },
|
|
1284
1132
|
message: `${d.patternType} pattern highly similar to ${d.file2}`,
|
package/dist/cli.mjs
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./chunk-J5CW6NYY.mjs";
|
|
3
2
|
import {
|
|
4
3
|
analyzePatterns,
|
|
5
4
|
generateSummary
|
|
6
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-K7BO57OO.mjs";
|
|
7
6
|
import "./chunk-NQBYYWHJ.mjs";
|
|
8
7
|
import {
|
|
9
8
|
filterBySeverity
|
|
10
9
|
} from "./chunk-J2G742QF.mjs";
|
|
11
|
-
import "./chunk-WBBO35SC.mjs";
|
|
12
10
|
|
|
13
11
|
// src/cli.ts
|
|
14
12
|
import { Command } from "commander";
|
|
@@ -385,8 +383,11 @@ async function patternActionHandler(directory, options) {
|
|
|
385
383
|
);
|
|
386
384
|
}
|
|
387
385
|
const elapsedTime = ((Date.now() - startTime) / 1e3).toFixed(2);
|
|
388
|
-
const summary = generateSummary(results);
|
|
389
|
-
const totalIssues = results.reduce(
|
|
386
|
+
const summary = generateSummary(results || []);
|
|
387
|
+
const totalIssues = (results || []).reduce(
|
|
388
|
+
(sum, r) => sum + (r.issues?.length || 0),
|
|
389
|
+
0
|
|
390
|
+
);
|
|
390
391
|
if (options.output === "json") {
|
|
391
392
|
handleJsonOutput(options.outputFile, directory, {
|
|
392
393
|
summary,
|
package/dist/index.js
CHANGED
|
@@ -498,7 +498,7 @@ async function getSmartDefaults(directory, userOptions) {
|
|
|
498
498
|
6,
|
|
499
499
|
Math.min(20, 6 + Math.floor(estimatedBlocks / 1e3) * 2)
|
|
500
500
|
);
|
|
501
|
-
const minSimilarity = Math.min(0.
|
|
501
|
+
const minSimilarity = Math.min(0.75, 0.45 + estimatedBlocks / 1e4 * 0.3);
|
|
502
502
|
const batchSize = estimatedBlocks > 1e3 ? 200 : 100;
|
|
503
503
|
const severity = estimatedBlocks > 3e3 ? "high" : "all";
|
|
504
504
|
const maxCandidatesPerBlock = Math.max(
|
|
@@ -658,9 +658,9 @@ async function analyzePatterns(options) {
|
|
|
658
658
|
return { results, duplicates, files, groups, clusters, config: finalOptions };
|
|
659
659
|
}
|
|
660
660
|
function generateSummary(results) {
|
|
661
|
-
const allIssues = results.flatMap((r) => r.issues);
|
|
661
|
+
const allIssues = results.flatMap((r) => r.issues || []);
|
|
662
662
|
const totalTokenCost = results.reduce(
|
|
663
|
-
(sum, r) => sum + (r.metrics
|
|
663
|
+
(sum, r) => sum + (r.metrics?.tokenCost || 0),
|
|
664
664
|
0
|
|
665
665
|
);
|
|
666
666
|
const patternsByType = {
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PatternDetectProvider,
|
|
3
|
-
Severity
|
|
4
|
-
} from "./chunk-J5CW6NYY.mjs";
|
|
5
1
|
import {
|
|
6
2
|
analyzePatterns,
|
|
7
3
|
createRefactorClusters,
|
|
@@ -9,10 +5,13 @@ import {
|
|
|
9
5
|
generateSummary,
|
|
10
6
|
getSmartDefaults,
|
|
11
7
|
groupDuplicatesByFilePair
|
|
12
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-K7BO57OO.mjs";
|
|
13
9
|
import {
|
|
14
10
|
detectDuplicatePatterns
|
|
15
11
|
} from "./chunk-NQBYYWHJ.mjs";
|
|
12
|
+
import {
|
|
13
|
+
calculatePatternScore
|
|
14
|
+
} from "./chunk-WBBO35SC.mjs";
|
|
16
15
|
import {
|
|
17
16
|
CONTEXT_RULES,
|
|
18
17
|
IssueType,
|
|
@@ -21,9 +20,59 @@ import {
|
|
|
21
20
|
getSeverityLabel,
|
|
22
21
|
getSeverityThreshold
|
|
23
22
|
} from "./chunk-J2G742QF.mjs";
|
|
23
|
+
|
|
24
|
+
// src/index.ts
|
|
25
|
+
import { ToolRegistry, Severity } from "@aiready/core";
|
|
26
|
+
|
|
27
|
+
// src/provider.ts
|
|
24
28
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
ToolName,
|
|
30
|
+
SpokeOutputSchema,
|
|
31
|
+
GLOBAL_SCAN_OPTIONS
|
|
32
|
+
} from "@aiready/core";
|
|
33
|
+
var PatternDetectProvider = {
|
|
34
|
+
id: ToolName.PatternDetect,
|
|
35
|
+
alias: ["patterns", "duplicates", "duplication"],
|
|
36
|
+
async analyze(options) {
|
|
37
|
+
const results = await analyzePatterns(options);
|
|
38
|
+
return SpokeOutputSchema.parse({
|
|
39
|
+
results: results.results,
|
|
40
|
+
summary: {
|
|
41
|
+
totalFiles: results.files.length,
|
|
42
|
+
totalIssues: results.results.reduce(
|
|
43
|
+
(sum, r) => sum + r.issues.length,
|
|
44
|
+
0
|
|
45
|
+
),
|
|
46
|
+
duplicates: results.duplicates,
|
|
47
|
+
// Keep the raw duplicates for score calculation
|
|
48
|
+
clusters: results.clusters,
|
|
49
|
+
config: Object.fromEntries(
|
|
50
|
+
Object.entries(results.config).filter(
|
|
51
|
+
([key]) => !GLOBAL_SCAN_OPTIONS.includes(key) || key === "rootDir"
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
},
|
|
55
|
+
metadata: {
|
|
56
|
+
toolName: ToolName.PatternDetect,
|
|
57
|
+
version: "0.12.5",
|
|
58
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
score(output, options) {
|
|
63
|
+
const duplicates = output.summary.duplicates || [];
|
|
64
|
+
const totalFiles = output.summary.totalFiles || output.results.length;
|
|
65
|
+
return calculatePatternScore(
|
|
66
|
+
duplicates,
|
|
67
|
+
totalFiles,
|
|
68
|
+
options.costConfig
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
defaultWeight: 22
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/index.ts
|
|
75
|
+
ToolRegistry.register(PatternDetectProvider);
|
|
27
76
|
export {
|
|
28
77
|
CONTEXT_RULES,
|
|
29
78
|
IssueType,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/pattern-detect",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"description": "Semantic duplicate pattern detection for AI-generated code - finds similar implementations that waste AI context tokens",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"commander": "^14.0.0",
|
|
67
67
|
"chalk": "^5.3.0",
|
|
68
|
-
"@aiready/core": "0.24.
|
|
68
|
+
"@aiready/core": "0.24.2"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"tsup": "^8.3.5",
|