@aiready/pattern-detect 0.16.22 → 0.17.0
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.mjs +3 -3
- package/dist/chunk-J2G742QF.mjs +162 -0
- package/dist/chunk-J5CW6NYY.mjs +64 -0
- package/dist/chunk-NQBYYWHJ.mjs +143 -0
- package/dist/chunk-SUUZMLPS.mjs +391 -0
- package/dist/cli.js +336 -303
- package/dist/cli.mjs +347 -303
- package/dist/context-rules-entry/index.d.mts +2 -2
- package/dist/context-rules-entry/index.d.ts +2 -2
- package/dist/context-rules-entry/index.js +2 -25
- package/dist/context-rules-entry/index.mjs +1 -1
- package/dist/detector-entry/index.mjs +2 -2
- package/dist/index-szjQDBsm.d.mts +49 -0
- package/dist/index-szjQDBsm.d.ts +49 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -25
- package/dist/index.mjs +6 -4
- package/package.json +2 -2
- package/dist/__tests__/context-rules.test.d.ts +0 -2
- package/dist/__tests__/context-rules.test.d.ts.map +0 -1
- package/dist/__tests__/context-rules.test.js +0 -189
- package/dist/__tests__/context-rules.test.js.map +0 -1
- package/dist/__tests__/detector.test.d.ts +0 -2
- package/dist/__tests__/detector.test.d.ts.map +0 -1
- package/dist/__tests__/detector.test.js +0 -259
- package/dist/__tests__/detector.test.js.map +0 -1
- package/dist/__tests__/grouping.test.d.ts +0 -2
- package/dist/__tests__/grouping.test.d.ts.map +0 -1
- package/dist/__tests__/grouping.test.js +0 -443
- package/dist/__tests__/grouping.test.js.map +0 -1
- package/dist/__tests__/scoring.test.d.ts +0 -2
- package/dist/__tests__/scoring.test.d.ts.map +0 -1
- package/dist/__tests__/scoring.test.js +0 -102
- package/dist/__tests__/scoring.test.js.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,104 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./chunk-
|
|
2
|
+
import "./chunk-J5CW6NYY.mjs";
|
|
3
3
|
import {
|
|
4
4
|
analyzePatterns,
|
|
5
5
|
generateSummary
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-SUUZMLPS.mjs";
|
|
7
|
+
import "./chunk-NQBYYWHJ.mjs";
|
|
8
8
|
import {
|
|
9
9
|
filterBySeverity
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-J2G742QF.mjs";
|
|
11
11
|
import "./chunk-WBBO35SC.mjs";
|
|
12
12
|
|
|
13
13
|
// src/cli.ts
|
|
14
14
|
import { Command } from "commander";
|
|
15
15
|
|
|
16
16
|
// src/cli-action.ts
|
|
17
|
-
import
|
|
17
|
+
import chalk2 from "chalk";
|
|
18
18
|
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
19
19
|
import { dirname } from "path";
|
|
20
20
|
import {
|
|
21
|
-
loadConfig,
|
|
22
|
-
mergeConfigWithDefaults,
|
|
23
21
|
resolveOutputPath,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
getSeverityValue as getSeverityValue2,
|
|
27
|
-
printTerminalHeader,
|
|
28
|
-
getTerminalDivider
|
|
22
|
+
getSeverityValue as getSeverityValue3,
|
|
23
|
+
getTerminalDivider as getTerminalDivider2
|
|
29
24
|
} from "@aiready/core";
|
|
30
25
|
|
|
31
|
-
// src/
|
|
32
|
-
import {
|
|
33
|
-
getSeverityBadge,
|
|
34
|
-
getSeverityValue,
|
|
35
|
-
generateReportHead,
|
|
36
|
-
generateStatCards,
|
|
37
|
-
generateScoreCard,
|
|
38
|
-
generateTable,
|
|
39
|
-
generateReportFooter
|
|
40
|
-
} from "@aiready/core";
|
|
41
|
-
function getPatternIcon(type) {
|
|
42
|
-
const icons = {
|
|
43
|
-
"api-handler": "\u{1F50C}",
|
|
44
|
-
validator: "\u{1F6E1}\uFE0F",
|
|
45
|
-
utility: "\u2699\uFE0F",
|
|
46
|
-
"class-method": "\u{1F3DB}\uFE0F",
|
|
47
|
-
component: "\u{1F9E9}",
|
|
48
|
-
function: "\u{1D453}",
|
|
49
|
-
unknown: "\u2753"
|
|
50
|
-
};
|
|
51
|
-
return icons[type] || icons.unknown;
|
|
52
|
-
}
|
|
53
|
-
function generateHTMLReport(results, summary) {
|
|
54
|
-
const data = summary ? { results, summary, metadata: { version: "0.11.22" } } : results;
|
|
55
|
-
const { metadata } = data;
|
|
56
|
-
const s = data.summary;
|
|
57
|
-
const head = generateReportHead("AIReady - Pattern Detection Report");
|
|
58
|
-
const score = Math.max(
|
|
59
|
-
0,
|
|
60
|
-
100 - Math.round((s.duplicates?.length || 0) / (s.totalFiles || 1) * 20)
|
|
61
|
-
);
|
|
62
|
-
const scoreCard = generateScoreCard(
|
|
63
|
-
`${score}%`,
|
|
64
|
-
"AI Ready Score (Deduplication)"
|
|
65
|
-
);
|
|
66
|
-
const stats = generateStatCards([
|
|
67
|
-
{ value: s.totalFiles, label: "Files Analyzed" },
|
|
68
|
-
{ value: s.duplicates?.length || 0, label: "Duplicate Clusters" },
|
|
69
|
-
{ value: s.totalIssues, label: "Total Issues" }
|
|
70
|
-
]);
|
|
71
|
-
const tableRows = (s.duplicates || []).map((dup) => [
|
|
72
|
-
`<span class="${dup.similarity > 0.95 ? "critical" : dup.similarity > 0.9 ? "major" : "minor"}">${Math.round(dup.similarity * 100)}%</span>`,
|
|
73
|
-
dup.patternType,
|
|
74
|
-
dup.files.map((f) => `<code>${f.path}:${f.startLine}-${f.endLine}</code>`).join("<br>\u2194<br>"),
|
|
75
|
-
dup.tokenCost.toLocaleString()
|
|
76
|
-
]);
|
|
77
|
-
const table = generateTable({
|
|
78
|
-
headers: ["Similarity", "Type", "Locations", "Tokens Wasted"],
|
|
79
|
-
rows: tableRows
|
|
80
|
-
});
|
|
81
|
-
const body = `${scoreCard}
|
|
82
|
-
${stats}
|
|
83
|
-
<div class="card">
|
|
84
|
-
<h2>Duplicate Patterns</h2>
|
|
85
|
-
${table}
|
|
86
|
-
</div>`;
|
|
87
|
-
const footer = generateReportFooter({
|
|
88
|
-
title: "Pattern Detection Report",
|
|
89
|
-
packageName: "pattern-detect",
|
|
90
|
-
packageUrl: "https://github.com/caopengau/aiready-pattern-detect",
|
|
91
|
-
bugUrl: "https://github.com/caopengau/aiready-pattern-detect/issues",
|
|
92
|
-
version: metadata.version
|
|
93
|
-
});
|
|
94
|
-
return `${head}
|
|
95
|
-
<body>
|
|
96
|
-
<h1>Pattern Detection Report</h1>
|
|
97
|
-
${body}
|
|
98
|
-
${footer}
|
|
99
|
-
</body>
|
|
100
|
-
</html>`;
|
|
101
|
-
}
|
|
26
|
+
// src/config-resolver.ts
|
|
27
|
+
import { loadConfig, mergeConfigWithDefaults, Severity } from "@aiready/core";
|
|
102
28
|
|
|
103
29
|
// src/constants.ts
|
|
104
30
|
var DEFAULT_MIN_SIMILARITY = 0.4;
|
|
@@ -128,11 +54,9 @@ EXAMPLES:
|
|
|
128
54
|
aiready-patterns . --max-candidates 50 --no-approx # Slower but more thorough
|
|
129
55
|
aiready-patterns . --output json > report.json # JSON export`;
|
|
130
56
|
|
|
131
|
-
// src/
|
|
132
|
-
async function
|
|
133
|
-
|
|
134
|
-
const startTime = Date.now();
|
|
135
|
-
const config = await loadConfig(directory);
|
|
57
|
+
// src/config-resolver.ts
|
|
58
|
+
async function resolvePatternConfig(directory, options) {
|
|
59
|
+
const fileConfig = await loadConfig(directory);
|
|
136
60
|
const defaults = {
|
|
137
61
|
minSimilarity: DEFAULT_MIN_SIMILARITY,
|
|
138
62
|
minLines: DEFAULT_MIN_LINES,
|
|
@@ -157,14 +81,13 @@ async function patternActionHandler(directory, options) {
|
|
|
157
81
|
minClusterFiles: DEFAULT_MIN_CLUSTER_FILES,
|
|
158
82
|
showRawDuplicates: false
|
|
159
83
|
};
|
|
160
|
-
const mergedConfig = mergeConfigWithDefaults(
|
|
84
|
+
const mergedConfig = mergeConfigWithDefaults(fileConfig, defaults);
|
|
161
85
|
const finalOptions = {
|
|
162
86
|
rootDir: directory,
|
|
163
87
|
minSimilarity: options.similarity ? parseFloat(options.similarity) : mergedConfig.minSimilarity,
|
|
164
88
|
minLines: options.minLines ? parseInt(options.minLines) : mergedConfig.minLines,
|
|
165
89
|
batchSize: options.batchSize ? parseInt(options.batchSize) : mergedConfig.batchSize,
|
|
166
90
|
approx: options.approx !== false && mergedConfig.approx,
|
|
167
|
-
// CLI --no-approx takes precedence
|
|
168
91
|
minSharedTokens: options.minSharedTokens ? parseInt(options.minSharedTokens) : mergedConfig.minSharedTokens,
|
|
169
92
|
maxCandidatesPerBlock: options.maxCandidates ? parseInt(options.maxCandidates) : mergedConfig.maxCandidatesPerBlock,
|
|
170
93
|
streamResults: options.streamResults !== false && mergedConfig.streamResults,
|
|
@@ -196,73 +119,79 @@ async function patternActionHandler(directory, options) {
|
|
|
196
119
|
(pattern) => !testPatterns.includes(pattern)
|
|
197
120
|
);
|
|
198
121
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
const summary =
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
122
|
+
return finalOptions;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/cli-output.ts
|
|
126
|
+
import {
|
|
127
|
+
getSeverityBadge,
|
|
128
|
+
getSeverityValue,
|
|
129
|
+
generateTable,
|
|
130
|
+
generateStandardHtmlReport
|
|
131
|
+
} from "@aiready/core";
|
|
132
|
+
function getPatternIcon(type) {
|
|
133
|
+
const icons = {
|
|
134
|
+
"api-handler": "\u{1F50C}",
|
|
135
|
+
validator: "\u{1F6E1}\uFE0F",
|
|
136
|
+
utility: "\u2699\uFE0F",
|
|
137
|
+
"class-method": "\u{1F3DB}\uFE0F",
|
|
138
|
+
component: "\u{1F9E9}",
|
|
139
|
+
function: "\u{1D453}",
|
|
140
|
+
unknown: "\u2753"
|
|
141
|
+
};
|
|
142
|
+
return icons[type] || icons.unknown;
|
|
143
|
+
}
|
|
144
|
+
function generateHTMLReport(results, summary) {
|
|
145
|
+
const data = summary ? { results, summary, metadata: { version: "0.11.22" } } : results;
|
|
146
|
+
const { metadata, summary: s } = data;
|
|
147
|
+
const scoreValue = Math.max(
|
|
148
|
+
0,
|
|
149
|
+
100 - Math.round((s.duplicates?.length || 0) / (s.totalFiles || 1) * 20)
|
|
150
|
+
);
|
|
151
|
+
const tableRows = (s.duplicates || []).map((dup) => [
|
|
152
|
+
`<span class="${dup.similarity > 0.95 ? "critical" : dup.similarity > 0.9 ? "major" : "minor"}">${Math.round(dup.similarity * 100)}%</span>`,
|
|
153
|
+
dup.patternType,
|
|
154
|
+
dup.files.map((f) => `<code>${f.path}:${f.startLine}-${f.endLine}</code>`).join("<br>\u2194<br>"),
|
|
155
|
+
dup.tokenCost.toLocaleString()
|
|
156
|
+
]);
|
|
157
|
+
return generateStandardHtmlReport(
|
|
158
|
+
{
|
|
159
|
+
title: "Pattern Detection Report",
|
|
160
|
+
packageName: "pattern-detect",
|
|
161
|
+
packageUrl: "https://github.com/caopengau/aiready-pattern-detect",
|
|
162
|
+
bugUrl: "https://github.com/caopengau/aiready-pattern-detect/issues",
|
|
163
|
+
version: metadata.version,
|
|
164
|
+
emoji: "\u{1F50D}"
|
|
165
|
+
},
|
|
166
|
+
[
|
|
167
|
+
{ value: s.totalFiles, label: "Files Analyzed" },
|
|
168
|
+
{ value: s.duplicates?.length || 0, label: "Duplicate Clusters" },
|
|
169
|
+
{ value: s.totalIssues, label: "Total Issues" }
|
|
170
|
+
],
|
|
171
|
+
[
|
|
172
|
+
{
|
|
173
|
+
title: "Duplicate Patterns",
|
|
174
|
+
content: generateTable({
|
|
175
|
+
headers: ["Similarity", "Type", "Locations", "Tokens Wasted"],
|
|
176
|
+
rows: tableRows
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
],
|
|
180
|
+
{ value: `${scoreValue}%`, label: "AI Ready Score (Deduplication)" }
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/terminal-output.ts
|
|
185
|
+
import chalk from "chalk";
|
|
186
|
+
import {
|
|
187
|
+
getSeverityBadge as getSeverityBadge2,
|
|
188
|
+
getSeverityValue as getSeverityValue2,
|
|
189
|
+
printTerminalHeader,
|
|
190
|
+
getTerminalDivider
|
|
191
|
+
} from "@aiready/core";
|
|
192
|
+
function printAnalysisSummary(resultsLength, totalIssues, totalTokenCost, elapsedTime) {
|
|
264
193
|
printTerminalHeader("PATTERN ANALYSIS SUMMARY");
|
|
265
|
-
console.log(chalk.white(`\u{1F4C1} Files analyzed: ${chalk.bold(
|
|
194
|
+
console.log(chalk.white(`\u{1F4C1} Files analyzed: ${chalk.bold(resultsLength)}`));
|
|
266
195
|
console.log(
|
|
267
196
|
chalk.yellow(
|
|
268
197
|
`\u26A0 AI confusion patterns detected: ${chalk.bold(totalIssues)}`
|
|
@@ -270,11 +199,13 @@ async function patternActionHandler(directory, options) {
|
|
|
270
199
|
);
|
|
271
200
|
console.log(
|
|
272
201
|
chalk.red(
|
|
273
|
-
`\u{1F4B0} Token cost (wasted): ${chalk.bold(
|
|
202
|
+
`\u{1F4B0} Token cost (wasted): ${chalk.bold(totalTokenCost.toLocaleString())}`
|
|
274
203
|
)
|
|
275
204
|
);
|
|
276
205
|
console.log(chalk.gray(`\u23F1 Analysis time: ${chalk.bold(elapsedTime + "s")}`));
|
|
277
|
-
|
|
206
|
+
}
|
|
207
|
+
function printPatternBreakdown(patternsByType) {
|
|
208
|
+
const sortedTypes = Object.entries(patternsByType).filter(([, count]) => count > 0).sort(([, a], [, b]) => b - a);
|
|
278
209
|
if (sortedTypes.length > 0) {
|
|
279
210
|
console.log("\n" + getTerminalDivider());
|
|
280
211
|
console.log(chalk.bold.white(" PATTERNS BY TYPE"));
|
|
@@ -286,188 +217,301 @@ async function patternActionHandler(directory, options) {
|
|
|
286
217
|
);
|
|
287
218
|
});
|
|
288
219
|
}
|
|
289
|
-
|
|
290
|
-
|
|
220
|
+
}
|
|
221
|
+
function printDuplicateGroups(groups, maxResults) {
|
|
222
|
+
if (groups.length === 0) return;
|
|
223
|
+
console.log("\n" + getTerminalDivider());
|
|
224
|
+
console.log(
|
|
225
|
+
chalk.bold.white(` \u{1F4E6} DUPLICATE GROUPS (${groups.length} file pairs)`)
|
|
226
|
+
);
|
|
227
|
+
console.log(getTerminalDivider() + "\n");
|
|
228
|
+
const topGroups = groups.sort((a, b) => {
|
|
229
|
+
const bVal = getSeverityValue2(b.severity);
|
|
230
|
+
const aVal = getSeverityValue2(a.severity);
|
|
231
|
+
const severityDiff = bVal - aVal;
|
|
232
|
+
if (severityDiff !== 0) return severityDiff;
|
|
233
|
+
return b.totalTokenCost - a.totalTokenCost;
|
|
234
|
+
}).slice(0, maxResults);
|
|
235
|
+
topGroups.forEach((group, idx) => {
|
|
236
|
+
const severityBadge = getSeverityBadge2(group.severity);
|
|
237
|
+
const [file1, file2] = group.filePair.split("::");
|
|
238
|
+
const file1Name = file1.split("/").pop() || file1;
|
|
239
|
+
const file2Name = file2.split("/").pop() || file2;
|
|
291
240
|
console.log(
|
|
292
|
-
chalk.bold
|
|
241
|
+
`${idx + 1}. ${severityBadge} ${chalk.bold(file1Name)} \u2194 ${chalk.bold(file2Name)}`
|
|
293
242
|
);
|
|
294
|
-
console.log(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (severityDiff !== 0) return severityDiff;
|
|
300
|
-
return b.totalTokenCost - a.totalTokenCost;
|
|
301
|
-
}).slice(0, finalOptions.maxResults);
|
|
302
|
-
topGroups.forEach((group, idx) => {
|
|
303
|
-
const severityBadge = getSeverityBadge2(group.severity);
|
|
304
|
-
const [file1, file2] = group.filePair.split("::");
|
|
305
|
-
const file1Name = file1.split("/").pop() || file1;
|
|
306
|
-
const file2Name = file2.split("/").pop() || file2;
|
|
307
|
-
console.log(
|
|
308
|
-
`${idx + 1}. ${severityBadge} ${chalk.bold(file1Name)} \u2194 ${chalk.bold(file2Name)}`
|
|
309
|
-
);
|
|
243
|
+
console.log(
|
|
244
|
+
` Occurrences: ${chalk.bold(group.occurrences)} | Total tokens: ${chalk.bold(group.totalTokenCost.toLocaleString())} | Avg similarity: ${chalk.bold(Math.round(group.averageSimilarity * 100) + "%")}`
|
|
245
|
+
);
|
|
246
|
+
const displayRanges = group.lineRanges.slice(0, 3);
|
|
247
|
+
displayRanges.forEach((range) => {
|
|
310
248
|
console.log(
|
|
311
|
-
`
|
|
249
|
+
` ${chalk.gray(file1)}:${chalk.cyan(`${range.file1.start}-${range.file1.end}`)} \u2194 ${chalk.gray(file2)}:${chalk.cyan(`${range.file2.start}-${range.file2.end}`)}`
|
|
312
250
|
);
|
|
313
|
-
const displayRanges = group.lineRanges.slice(0, 3);
|
|
314
|
-
displayRanges.forEach((range) => {
|
|
315
|
-
console.log(
|
|
316
|
-
` ${chalk.gray(file1)}:${chalk.cyan(`${range.file1.start}-${range.file1.end}`)} \u2194 ${chalk.gray(file2)}:${chalk.cyan(`${range.file2.start}-${range.file2.end}`)}`
|
|
317
|
-
);
|
|
318
|
-
});
|
|
319
|
-
if (group.lineRanges.length > 3) {
|
|
320
|
-
console.log(
|
|
321
|
-
` ${chalk.gray(`... and ${group.lineRanges.length - 3} more ranges`)}`
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
console.log();
|
|
325
251
|
});
|
|
326
|
-
if (
|
|
252
|
+
if (group.lineRanges.length > 3) {
|
|
327
253
|
console.log(
|
|
328
|
-
chalk.gray(
|
|
329
|
-
` ... and ${groups.length - topGroups.length} more file pairs`
|
|
330
|
-
)
|
|
254
|
+
` ${chalk.gray(`... and ${group.lineRanges.length - 3} more ranges`)}`
|
|
331
255
|
);
|
|
332
256
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
257
|
+
console.log();
|
|
258
|
+
});
|
|
259
|
+
if (groups.length > topGroups.length) {
|
|
336
260
|
console.log(
|
|
337
|
-
chalk.
|
|
261
|
+
chalk.gray(
|
|
262
|
+
` ... and ${groups.length - topGroups.length} more file pairs`
|
|
263
|
+
)
|
|
338
264
|
);
|
|
339
|
-
console.log(getTerminalDivider() + "\n");
|
|
340
|
-
clusters.sort((a, b) => b.totalTokenCost - a.totalTokenCost).forEach((cluster, idx) => {
|
|
341
|
-
const severityBadge = getSeverityBadge2(cluster.severity);
|
|
342
|
-
console.log(`${idx + 1}. ${severityBadge} ${chalk.bold(cluster.name)}`);
|
|
343
|
-
console.log(
|
|
344
|
-
` Total tokens: ${chalk.bold(cluster.totalTokenCost.toLocaleString())} | Avg similarity: ${chalk.bold(Math.round(cluster.averageSimilarity * 100) + "%")} | Duplicates: ${chalk.bold(cluster.duplicateCount)}`
|
|
345
|
-
);
|
|
346
|
-
const displayFiles = cluster.files.slice(0, 5);
|
|
347
|
-
console.log(
|
|
348
|
-
` Files (${cluster.files.length}): ${displayFiles.map((f) => chalk.gray(f.split("/").pop() || f)).join(", ")}`
|
|
349
|
-
);
|
|
350
|
-
if (cluster.files.length > 5) {
|
|
351
|
-
console.log(
|
|
352
|
-
` ${chalk.gray(`... and ${cluster.files.length - 5} more files`)}`
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
if (cluster.reason) {
|
|
356
|
-
console.log(` ${chalk.italic.gray(cluster.reason)}`);
|
|
357
|
-
}
|
|
358
|
-
if (cluster.suggestion) {
|
|
359
|
-
console.log(
|
|
360
|
-
` ${chalk.cyan("\u2192")} ${chalk.italic(cluster.suggestion)}`
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
console.log();
|
|
364
|
-
});
|
|
365
265
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
` Similarity: ${chalk.bold(Math.round(dup.similarity * 100) + "%")} | Pattern: ${dup.patternType} | Tokens: ${chalk.bold(dup.tokenCost.toLocaleString())}`
|
|
386
|
-
);
|
|
387
|
-
console.log(
|
|
388
|
-
` ${chalk.gray(dup.file1)}:${chalk.cyan(dup.line1 + "-" + dup.endLine1)}`
|
|
389
|
-
);
|
|
390
|
-
console.log(
|
|
391
|
-
` ${chalk.gray(dup.file2)}:${chalk.cyan(dup.line2 + "-" + dup.endLine2)}`
|
|
392
|
-
);
|
|
393
|
-
if (dup.reason) {
|
|
394
|
-
console.log(` ${chalk.italic.gray(dup.reason)}`);
|
|
395
|
-
}
|
|
396
|
-
if (dup.suggestion) {
|
|
397
|
-
console.log(` ${chalk.cyan("\u2192")} ${chalk.italic(dup.suggestion)}`);
|
|
398
|
-
}
|
|
399
|
-
console.log();
|
|
400
|
-
});
|
|
401
|
-
if (filteredDuplicates.length > topDuplicates.length) {
|
|
266
|
+
}
|
|
267
|
+
function printRefactorClusters(clusters) {
|
|
268
|
+
if (clusters.length === 0) return;
|
|
269
|
+
console.log("\n" + getTerminalDivider());
|
|
270
|
+
console.log(
|
|
271
|
+
chalk.bold.white(` \u{1F3AF} REFACTOR CLUSTERS (${clusters.length} patterns)`)
|
|
272
|
+
);
|
|
273
|
+
console.log(getTerminalDivider() + "\n");
|
|
274
|
+
clusters.sort((a, b) => b.totalTokenCost - a.totalTokenCost).forEach((cluster, idx) => {
|
|
275
|
+
const severityBadge = getSeverityBadge2(cluster.severity);
|
|
276
|
+
console.log(`${idx + 1}. ${severityBadge} ${chalk.bold(cluster.name)}`);
|
|
277
|
+
console.log(
|
|
278
|
+
` Total tokens: ${chalk.bold(cluster.totalTokenCost.toLocaleString())} | Avg similarity: ${chalk.bold(Math.round(cluster.averageSimilarity * 100) + "%")} | Duplicates: ${chalk.bold(cluster.duplicateCount)}`
|
|
279
|
+
);
|
|
280
|
+
const displayFiles = cluster.files.slice(0, 5);
|
|
281
|
+
console.log(
|
|
282
|
+
` Files (${cluster.files.length}): ${displayFiles.map((f) => chalk.gray(f.split("/").pop() || f)).join(", ")}`
|
|
283
|
+
);
|
|
284
|
+
if (cluster.files.length > 5) {
|
|
402
285
|
console.log(
|
|
403
|
-
chalk.gray(
|
|
404
|
-
` ... and ${filteredDuplicates.length - topDuplicates.length} more duplicates`
|
|
405
|
-
)
|
|
286
|
+
` ${chalk.gray(`... and ${cluster.files.length - 5} more files`)}`
|
|
406
287
|
);
|
|
407
288
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const criticalIssues = allIssues.filter(
|
|
413
|
-
(issue) => getSeverityValue2(issue.severity) === 4
|
|
414
|
-
);
|
|
415
|
-
if (criticalIssues.length > 0) {
|
|
416
|
-
console.log(getTerminalDivider());
|
|
417
|
-
console.log(chalk.bold.white(" CRITICAL ISSUES (>95% similar)"));
|
|
418
|
-
console.log(getTerminalDivider() + "\n");
|
|
419
|
-
criticalIssues.slice(0, 5).forEach((issue) => {
|
|
289
|
+
if (cluster.reason) {
|
|
290
|
+
console.log(` ${chalk.italic.gray(cluster.reason)}`);
|
|
291
|
+
}
|
|
292
|
+
if (cluster.suggestion) {
|
|
420
293
|
console.log(
|
|
421
|
-
chalk.
|
|
294
|
+
` ${chalk.cyan("\u2192")} ${chalk.italic(cluster.suggestion)}`
|
|
422
295
|
);
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (
|
|
429
|
-
|
|
296
|
+
}
|
|
297
|
+
console.log();
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function printRawDuplicates(duplicates, maxResults) {
|
|
301
|
+
if (duplicates.length === 0) return;
|
|
302
|
+
console.log("\n" + getTerminalDivider());
|
|
303
|
+
console.log(chalk.bold.white(" TOP DUPLICATE PATTERNS"));
|
|
304
|
+
console.log(getTerminalDivider() + "\n");
|
|
305
|
+
const topDuplicates = duplicates.sort((a, b) => {
|
|
306
|
+
const bVal = getSeverityValue2(b.severity);
|
|
307
|
+
const aVal = getSeverityValue2(a.severity);
|
|
308
|
+
const severityDiff = bVal - aVal;
|
|
309
|
+
if (severityDiff !== 0) return severityDiff;
|
|
310
|
+
return b.similarity - a.similarity;
|
|
311
|
+
}).slice(0, maxResults);
|
|
312
|
+
topDuplicates.forEach((dup) => {
|
|
313
|
+
const severityBadge = getSeverityBadge2(dup.severity);
|
|
314
|
+
const file1Name = dup.file1.split("/").pop() || dup.file1;
|
|
315
|
+
const file2Name = dup.file2.split("/").pop() || dup.file2;
|
|
430
316
|
console.log(
|
|
431
|
-
chalk.
|
|
432
|
-
"\u{1F4A1} If you expected to find duplicates, try adjusting parameters:"
|
|
433
|
-
)
|
|
317
|
+
`${severityBadge} ${chalk.bold(file1Name)} \u2194 ${chalk.bold(file2Name)}`
|
|
434
318
|
);
|
|
435
|
-
console.log(chalk.dim(" \u2022 Lower similarity threshold: --similarity 0.3"));
|
|
436
|
-
console.log(chalk.dim(" \u2022 Reduce minimum lines: --min-lines 3"));
|
|
437
|
-
console.log(chalk.dim(" \u2022 Include test files: --include-tests"));
|
|
438
319
|
console.log(
|
|
439
|
-
chalk.
|
|
320
|
+
` Similarity: ${chalk.bold(Math.round(dup.similarity * 100) + "%")} | Pattern: ${dup.patternType} | Tokens: ${chalk.bold(dup.tokenCost.toLocaleString())}`
|
|
321
|
+
);
|
|
322
|
+
console.log(
|
|
323
|
+
` ${chalk.gray(dup.file1)}:${chalk.cyan(dup.line1 + "-" + dup.endLine1)}`
|
|
440
324
|
);
|
|
441
|
-
console.log("");
|
|
442
|
-
}
|
|
443
|
-
if (totalIssues > 0 && totalIssues < 5) {
|
|
444
325
|
console.log(
|
|
445
|
-
chalk.
|
|
326
|
+
` ${chalk.gray(dup.file2)}:${chalk.cyan(dup.line2 + "-" + dup.endLine2)}`
|
|
446
327
|
);
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
328
|
+
if (dup.reason) {
|
|
329
|
+
console.log(` ${chalk.italic.gray(dup.reason)}`);
|
|
330
|
+
}
|
|
331
|
+
if (dup.suggestion) {
|
|
332
|
+
console.log(` ${chalk.cyan("\u2192")} ${chalk.italic(dup.suggestion)}`);
|
|
333
|
+
}
|
|
334
|
+
console.log();
|
|
335
|
+
});
|
|
336
|
+
if (duplicates.length > topDuplicates.length) {
|
|
450
337
|
console.log(
|
|
451
|
-
chalk.
|
|
338
|
+
chalk.gray(
|
|
339
|
+
` ... and ${duplicates.length - topDuplicates.length} more duplicates`
|
|
340
|
+
)
|
|
452
341
|
);
|
|
453
|
-
console.log("");
|
|
454
342
|
}
|
|
343
|
+
}
|
|
344
|
+
function printCriticalIssues(issues) {
|
|
345
|
+
if (issues.length === 0) return;
|
|
455
346
|
console.log(getTerminalDivider());
|
|
347
|
+
console.log(chalk.bold.white(" CRITICAL ISSUES (>95% similar)"));
|
|
348
|
+
console.log(getTerminalDivider() + "\n");
|
|
349
|
+
issues.slice(0, 5).forEach((issue) => {
|
|
350
|
+
console.log(
|
|
351
|
+
chalk.red("\u25CF ") + chalk.white(`${issue.file}:${issue.location.line}`)
|
|
352
|
+
);
|
|
353
|
+
console.log(` ${chalk.dim(issue.message)}`);
|
|
354
|
+
console.log(` ${chalk.green("\u2192")} ${chalk.italic(issue.suggestion)}
|
|
355
|
+
`);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/cli-action.ts
|
|
360
|
+
async function patternActionHandler(directory, options) {
|
|
361
|
+
console.log(chalk2.blue("\u{1F50D} Analyzing patterns...\n"));
|
|
362
|
+
const startTime = Date.now();
|
|
363
|
+
const finalOptions = await resolvePatternConfig(directory, options);
|
|
364
|
+
const {
|
|
365
|
+
results,
|
|
366
|
+
duplicates: rawDuplicates,
|
|
367
|
+
groups,
|
|
368
|
+
clusters
|
|
369
|
+
} = await analyzePatterns(finalOptions);
|
|
370
|
+
let filteredDuplicates = rawDuplicates;
|
|
371
|
+
if (finalOptions.minSeverity) {
|
|
372
|
+
filteredDuplicates = filterBySeverity(
|
|
373
|
+
filteredDuplicates,
|
|
374
|
+
finalOptions.minSeverity
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
if (finalOptions.excludeTestFixtures) {
|
|
378
|
+
filteredDuplicates = filteredDuplicates.filter(
|
|
379
|
+
(d) => d.matchedRule !== "test-fixtures"
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
if (finalOptions.excludeTemplates) {
|
|
383
|
+
filteredDuplicates = filteredDuplicates.filter(
|
|
384
|
+
(d) => d.matchedRule !== "templates"
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const elapsedTime = ((Date.now() - startTime) / 1e3).toFixed(2);
|
|
388
|
+
const summary = generateSummary(results);
|
|
389
|
+
const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0);
|
|
390
|
+
if (options.output === "json") {
|
|
391
|
+
handleJsonOutput(options.outputFile, directory, {
|
|
392
|
+
summary,
|
|
393
|
+
results,
|
|
394
|
+
duplicates: rawDuplicates,
|
|
395
|
+
groups: groups || [],
|
|
396
|
+
clusters: clusters || [],
|
|
397
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (options.output === "html") {
|
|
402
|
+
handleHtmlOutput(options.outputFile, directory, summary, results);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
renderTerminalOutput(
|
|
406
|
+
results.length,
|
|
407
|
+
totalIssues,
|
|
408
|
+
summary,
|
|
409
|
+
elapsedTime,
|
|
410
|
+
finalOptions,
|
|
411
|
+
groups,
|
|
412
|
+
clusters,
|
|
413
|
+
filteredDuplicates
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
function handleJsonOutput(outputFile, directory, data) {
|
|
417
|
+
const outputPath = resolveOutputPath(
|
|
418
|
+
outputFile,
|
|
419
|
+
`pattern-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
|
|
420
|
+
directory
|
|
421
|
+
);
|
|
422
|
+
const dir = dirname(outputPath);
|
|
423
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
424
|
+
writeFileSync(outputPath, JSON.stringify(data, null, 2));
|
|
425
|
+
console.log(chalk2.green(`
|
|
426
|
+
\u2713 JSON report saved to ${outputPath}`));
|
|
427
|
+
}
|
|
428
|
+
function handleHtmlOutput(outputFile, directory, summary, results) {
|
|
429
|
+
const html = generateHTMLReport(summary, results);
|
|
430
|
+
const outputPath = resolveOutputPath(
|
|
431
|
+
outputFile,
|
|
432
|
+
`pattern-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.html`,
|
|
433
|
+
directory
|
|
434
|
+
);
|
|
435
|
+
const dir = dirname(outputPath);
|
|
436
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
437
|
+
writeFileSync(outputPath, html);
|
|
438
|
+
console.log(chalk2.green(`
|
|
439
|
+
\u2713 HTML report saved to ${outputPath}`));
|
|
440
|
+
}
|
|
441
|
+
function renderTerminalOutput(fileCount, totalIssues, summary, elapsedTime, options, groups, clusters, filteredDuplicates) {
|
|
442
|
+
printAnalysisSummary(
|
|
443
|
+
fileCount,
|
|
444
|
+
totalIssues,
|
|
445
|
+
summary.totalTokenCost,
|
|
446
|
+
elapsedTime
|
|
447
|
+
);
|
|
448
|
+
printPatternBreakdown(summary.patternsByType);
|
|
449
|
+
if (!options.showRawDuplicates && groups && groups.length > 0) {
|
|
450
|
+
printDuplicateGroups(groups, options.maxResults);
|
|
451
|
+
}
|
|
452
|
+
if (!options.showRawDuplicates && clusters && clusters.length > 0) {
|
|
453
|
+
printRefactorClusters(clusters);
|
|
454
|
+
}
|
|
455
|
+
if (totalIssues > 0 && (options.showRawDuplicates || !groups || groups.length === 0)) {
|
|
456
|
+
printRawDuplicates(filteredDuplicates, options.maxResults);
|
|
457
|
+
}
|
|
458
|
+
const criticalIssues = resultsToCriticalIssues(summary, filteredDuplicates);
|
|
459
|
+
printCriticalIssues(criticalIssues);
|
|
460
|
+
if (totalIssues === 0) {
|
|
461
|
+
printSuccessMessage();
|
|
462
|
+
} else if (totalIssues < 5) {
|
|
463
|
+
printGuidance();
|
|
464
|
+
}
|
|
465
|
+
console.log(getTerminalDivider2());
|
|
456
466
|
if (totalIssues > 0) {
|
|
457
467
|
console.log(
|
|
458
|
-
|
|
468
|
+
chalk2.white(
|
|
459
469
|
`
|
|
460
|
-
\u{1F4A1} Run with ${
|
|
470
|
+
\u{1F4A1} Run with ${chalk2.bold("--output json")} or ${chalk2.bold("--output html")} for detailed reports`
|
|
461
471
|
)
|
|
462
472
|
);
|
|
463
473
|
}
|
|
474
|
+
printFooter();
|
|
475
|
+
}
|
|
476
|
+
function resultsToCriticalIssues(summary, duplicates) {
|
|
477
|
+
return duplicates.filter((d) => getSeverityValue3(d.severity) === 4).map((d) => ({
|
|
478
|
+
file: d.file1,
|
|
479
|
+
location: { line: d.line1 },
|
|
480
|
+
message: `${d.patternType} pattern highly similar to ${d.file2}`,
|
|
481
|
+
suggestion: d.suggestion,
|
|
482
|
+
severity: d.severity
|
|
483
|
+
}));
|
|
484
|
+
}
|
|
485
|
+
function printSuccessMessage() {
|
|
486
|
+
console.log(chalk2.green("\n\u2728 Great! No duplicate patterns detected.\n"));
|
|
487
|
+
console.log(
|
|
488
|
+
chalk2.yellow(
|
|
489
|
+
"\u{1F4A1} If you expected to find duplicates, try adjusting parameters:"
|
|
490
|
+
)
|
|
491
|
+
);
|
|
492
|
+
console.log(chalk2.dim(" \u2022 Lower similarity threshold: --similarity 0.3"));
|
|
493
|
+
console.log(chalk2.dim(" \u2022 Reduce minimum lines: --min-lines 3"));
|
|
494
|
+
console.log(chalk2.dim(" \u2022 Include test files: --include-tests"));
|
|
495
|
+
console.log(
|
|
496
|
+
chalk2.dim(" \u2022 Lower shared tokens threshold: --min-shared-tokens 5\n")
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
function printGuidance() {
|
|
500
|
+
console.log(
|
|
501
|
+
chalk2.yellow("\n\u{1F4A1} Few results found. To find more duplicates, try:")
|
|
502
|
+
);
|
|
503
|
+
console.log(chalk2.dim(" \u2022 Lower similarity threshold: --similarity 0.3"));
|
|
504
|
+
console.log(chalk2.dim(" \u2022 Reduce minimum lines: --min-lines 3"));
|
|
505
|
+
console.log(chalk2.dim(" \u2022 Include test files: --include-tests\n"));
|
|
506
|
+
}
|
|
507
|
+
function printFooter() {
|
|
464
508
|
console.log(
|
|
465
|
-
|
|
509
|
+
chalk2.dim(
|
|
466
510
|
"\n\u2B50 Like AIReady? Star us on GitHub: https://github.com/caopengau/aiready-pattern-detect"
|
|
467
511
|
)
|
|
468
512
|
);
|
|
469
513
|
console.log(
|
|
470
|
-
|
|
514
|
+
chalk2.dim(
|
|
471
515
|
"\u{1F41B} Found a bug? Report it: https://github.com/caopengau/aiready-pattern-detect/issues\n"
|
|
472
516
|
)
|
|
473
517
|
);
|