@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.
Files changed (35) hide show
  1. package/dist/analyzer-entry/index.mjs +3 -3
  2. package/dist/chunk-J2G742QF.mjs +162 -0
  3. package/dist/chunk-J5CW6NYY.mjs +64 -0
  4. package/dist/chunk-NQBYYWHJ.mjs +143 -0
  5. package/dist/chunk-SUUZMLPS.mjs +391 -0
  6. package/dist/cli.js +336 -303
  7. package/dist/cli.mjs +347 -303
  8. package/dist/context-rules-entry/index.d.mts +2 -2
  9. package/dist/context-rules-entry/index.d.ts +2 -2
  10. package/dist/context-rules-entry/index.js +2 -25
  11. package/dist/context-rules-entry/index.mjs +1 -1
  12. package/dist/detector-entry/index.mjs +2 -2
  13. package/dist/index-szjQDBsm.d.mts +49 -0
  14. package/dist/index-szjQDBsm.d.ts +49 -0
  15. package/dist/index.d.mts +2 -2
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.js +4 -25
  18. package/dist/index.mjs +6 -4
  19. package/package.json +2 -2
  20. package/dist/__tests__/context-rules.test.d.ts +0 -2
  21. package/dist/__tests__/context-rules.test.d.ts.map +0 -1
  22. package/dist/__tests__/context-rules.test.js +0 -189
  23. package/dist/__tests__/context-rules.test.js.map +0 -1
  24. package/dist/__tests__/detector.test.d.ts +0 -2
  25. package/dist/__tests__/detector.test.d.ts.map +0 -1
  26. package/dist/__tests__/detector.test.js +0 -259
  27. package/dist/__tests__/detector.test.js.map +0 -1
  28. package/dist/__tests__/grouping.test.d.ts +0 -2
  29. package/dist/__tests__/grouping.test.d.ts.map +0 -1
  30. package/dist/__tests__/grouping.test.js +0 -443
  31. package/dist/__tests__/grouping.test.js.map +0 -1
  32. package/dist/__tests__/scoring.test.d.ts +0 -2
  33. package/dist/__tests__/scoring.test.d.ts.map +0 -1
  34. package/dist/__tests__/scoring.test.js +0 -102
  35. 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-6JTVOBJX.mjs";
2
+ import "./chunk-J5CW6NYY.mjs";
3
3
  import {
4
4
  analyzePatterns,
5
5
  generateSummary
6
- } from "./chunk-DNZS4ESD.mjs";
7
- import "./chunk-VGMM3L3O.mjs";
6
+ } from "./chunk-SUUZMLPS.mjs";
7
+ import "./chunk-NQBYYWHJ.mjs";
8
8
  import {
9
9
  filterBySeverity
10
- } from "./chunk-I6ETJC7L.mjs";
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 chalk from "chalk";
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
- Severity,
25
- getSeverityBadge as getSeverityBadge2,
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/cli-output.ts
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/cli-action.ts
132
- async function patternActionHandler(directory, options) {
133
- console.log(chalk.blue("\u{1F50D} Analyzing patterns...\n"));
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(config, defaults);
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
- const {
200
- results,
201
- duplicates: rawDuplicates,
202
- groups,
203
- clusters
204
- } = await analyzePatterns(finalOptions);
205
- let filteredDuplicates = rawDuplicates;
206
- if (finalOptions.minSeverity) {
207
- filteredDuplicates = filterBySeverity(
208
- filteredDuplicates,
209
- finalOptions.minSeverity
210
- );
211
- }
212
- if (finalOptions.excludeTestFixtures) {
213
- filteredDuplicates = filteredDuplicates.filter(
214
- (d) => d.matchedRule !== "test-fixtures"
215
- );
216
- }
217
- if (finalOptions.excludeTemplates) {
218
- filteredDuplicates = filteredDuplicates.filter(
219
- (d) => d.matchedRule !== "templates"
220
- );
221
- }
222
- const elapsedTime = ((Date.now() - startTime) / 1e3).toFixed(2);
223
- const summary = generateSummary(results);
224
- const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0);
225
- if (options.output === "json") {
226
- const jsonOutput = {
227
- summary,
228
- results,
229
- duplicates: rawDuplicates,
230
- groups: groups || [],
231
- clusters: clusters || [],
232
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
233
- };
234
- const outputPath = resolveOutputPath(
235
- options.outputFile,
236
- `pattern-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
237
- directory
238
- );
239
- const dir = dirname(outputPath);
240
- if (!existsSync(dir)) {
241
- mkdirSync(dir, { recursive: true });
242
- }
243
- writeFileSync(outputPath, JSON.stringify(jsonOutput, null, 2));
244
- console.log(chalk.green(`
245
- \u2713 JSON report saved to ${outputPath}`));
246
- return;
247
- }
248
- if (options.output === "html") {
249
- const html = generateHTMLReport(summary, results);
250
- const outputPath = resolveOutputPath(
251
- options.outputFile,
252
- `pattern-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.html`,
253
- directory
254
- );
255
- const dir = dirname(outputPath);
256
- if (!existsSync(dir)) {
257
- mkdirSync(dir, { recursive: true });
258
- }
259
- writeFileSync(outputPath, html);
260
- console.log(chalk.green(`
261
- \u2713 HTML report saved to ${outputPath}`));
262
- return;
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(results.length)}`));
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(summary.totalTokenCost.toLocaleString())}`
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
- const sortedTypes = Object.entries(summary.patternsByType).filter(([, count]) => count > 0).sort(([, a], [, b]) => b - a);
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
- if (!finalOptions.showRawDuplicates && groups && groups.length > 0) {
290
- console.log("\n" + getTerminalDivider());
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.white(` \u{1F4E6} DUPLICATE GROUPS (${groups.length} file pairs)`)
241
+ `${idx + 1}. ${severityBadge} ${chalk.bold(file1Name)} \u2194 ${chalk.bold(file2Name)}`
293
242
  );
294
- console.log(getTerminalDivider() + "\n");
295
- const topGroups = groups.sort((a, b) => {
296
- const bVal = getSeverityValue2(b.severity);
297
- const aVal = getSeverityValue2(a.severity);
298
- const severityDiff = bVal - aVal;
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
- ` Occurrences: ${chalk.bold(group.occurrences)} | Total tokens: ${chalk.bold(group.totalTokenCost.toLocaleString())} | Avg similarity: ${chalk.bold(Math.round(group.averageSimilarity * 100) + "%")}`
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 (groups.length > topGroups.length) {
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
- if (!finalOptions.showRawDuplicates && clusters && clusters.length > 0) {
335
- console.log("\n" + getTerminalDivider());
257
+ console.log();
258
+ });
259
+ if (groups.length > topGroups.length) {
336
260
  console.log(
337
- chalk.bold.white(` \u{1F3AF} REFACTOR CLUSTERS (${clusters.length} patterns)`)
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
- if (totalIssues > 0 && (finalOptions.showRawDuplicates || !groups || groups.length === 0)) {
367
- console.log("\n" + getTerminalDivider());
368
- console.log(chalk.bold.white(" TOP DUPLICATE PATTERNS"));
369
- console.log(getTerminalDivider() + "\n");
370
- const topDuplicates = filteredDuplicates.sort((a, b) => {
371
- const bVal = getSeverityValue2(b.severity);
372
- const aVal = getSeverityValue2(a.severity);
373
- const severityDiff = bVal - aVal;
374
- if (severityDiff !== 0) return severityDiff;
375
- return b.similarity - a.similarity;
376
- }).slice(0, finalOptions.maxResults);
377
- topDuplicates.forEach((dup) => {
378
- const severityBadge = getSeverityBadge2(dup.severity);
379
- const file1Name = dup.file1.split("/").pop() || dup.file1;
380
- const file2Name = dup.file2.split("/").pop() || dup.file2;
381
- console.log(
382
- `${severityBadge} ${chalk.bold(file1Name)} \u2194 ${chalk.bold(file2Name)}`
383
- );
384
- console.log(
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
- const allIssues = results.flatMap(
410
- (r) => r.issues.map((issue) => ({ ...issue, file: r.fileName }))
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.red("\u25CF ") + chalk.white(`${issue.file}:${issue.location.line}`)
294
+ ` ${chalk.cyan("\u2192")} ${chalk.italic(cluster.suggestion)}`
422
295
  );
423
- console.log(` ${chalk.dim(issue.message)}`);
424
- console.log(` ${chalk.green("\u2192")} ${chalk.italic(issue.suggestion)}
425
- `);
426
- });
427
- }
428
- if (totalIssues === 0) {
429
- console.log(chalk.green("\n\u2728 Great! No duplicate patterns detected.\n"));
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.yellow(
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.dim(" \u2022 Lower shared tokens threshold: --min-shared-tokens 5")
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.yellow("\n\u{1F4A1} Few results found. To find more duplicates, try:")
326
+ ` ${chalk.gray(dup.file2)}:${chalk.cyan(dup.line2 + "-" + dup.endLine2)}`
446
327
  );
447
- console.log(chalk.dim(" \u2022 Lower similarity threshold: --similarity 0.3"));
448
- console.log(chalk.dim(" \u2022 Reduce minimum lines: --min-lines 3"));
449
- console.log(chalk.dim(" \u2022 Include test files: --include-tests"));
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.dim(" \u2022 Lower shared tokens threshold: --min-shared-tokens 5")
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
- chalk.white(
468
+ chalk2.white(
459
469
  `
460
- \u{1F4A1} Run with ${chalk.bold("--output json")} or ${chalk.bold("--output html")} for detailed reports`
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
- chalk.dim(
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
- chalk.dim(
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
  );