@aiready/pattern-detect 0.11.31 → 0.11.34

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/cli.js CHANGED
@@ -113,8 +113,14 @@ function calculatePythonSimilarity(pattern1, pattern2) {
113
113
  }
114
114
  function calculateNameSimilarity(name1, name2) {
115
115
  if (name1 === name2) return 1;
116
- const clean1 = name1.replace(/^(get|set|is|has|create|delete|update|fetch)_?/, "");
117
- const clean2 = name2.replace(/^(get|set|is|has|create|delete|update|fetch)_?/, "");
116
+ const clean1 = name1.replace(
117
+ /^(get|set|is|has|create|delete|update|fetch)_?/,
118
+ ""
119
+ );
120
+ const clean2 = name2.replace(
121
+ /^(get|set|is|has|create|delete|update|fetch)_?/,
122
+ ""
123
+ );
118
124
  if (clean1 === clean2) return 0.9;
119
125
  if (clean1.includes(clean2) || clean2.includes(clean1)) {
120
126
  return 0.7;
@@ -146,7 +152,10 @@ function detectPythonAntiPatterns(patterns) {
146
152
  const antiPatterns = [];
147
153
  const nameGroups = /* @__PURE__ */ new Map();
148
154
  for (const pattern of patterns) {
149
- const baseName = pattern.name.replace(/^(get|set|create|delete|update)_/, "");
155
+ const baseName = pattern.name.replace(
156
+ /^(get|set|create|delete|update)_/,
157
+ ""
158
+ );
150
159
  if (!nameGroups.has(baseName)) {
151
160
  nameGroups.set(baseName, []);
152
161
  }
@@ -419,11 +428,15 @@ async function detectDuplicatePatterns(files, options) {
419
428
  linesOfCode: block.linesOfCode
420
429
  }))
421
430
  );
422
- console.log(`Extracted ${allBlocks.length} code blocks for analysis`);
431
+ if (!options.onProgress) {
432
+ console.log(`Extracted ${allBlocks.length} code blocks for analysis`);
433
+ }
423
434
  const pythonFiles = files.filter((f) => f.file.toLowerCase().endsWith(".py"));
424
435
  if (pythonFiles.length > 0) {
425
436
  const { extractPythonPatterns: extractPythonPatterns2 } = await Promise.resolve().then(() => (init_python_extractor(), python_extractor_exports));
426
- const patterns = await extractPythonPatterns2(pythonFiles.map((f) => f.file));
437
+ const patterns = await extractPythonPatterns2(
438
+ pythonFiles.map((f) => f.file)
439
+ );
427
440
  const pythonBlocks = patterns.filter((p) => p.code && p.code.trim().length > 0).map((p) => ({
428
441
  content: p.code,
429
442
  startLine: p.startLine,
@@ -435,11 +448,17 @@ async function detectDuplicatePatterns(files, options) {
435
448
  linesOfCode: p.endLine - p.startLine + 1
436
449
  }));
437
450
  allBlocks.push(...pythonBlocks);
438
- console.log(`Added ${pythonBlocks.length} Python patterns`);
451
+ if (!options.onProgress) {
452
+ console.log(`Added ${pythonBlocks.length} Python patterns`);
453
+ }
439
454
  }
440
455
  if (!approx && allBlocks.length > 500) {
441
- console.log(`\u26A0\uFE0F Using --no-approx mode with ${allBlocks.length} blocks may be slow (O(B\xB2) complexity).`);
442
- console.log(` Consider using approximate mode (default) for better performance.`);
456
+ console.log(
457
+ `\u26A0\uFE0F Using --no-approx mode with ${allBlocks.length} blocks may be slow (O(B\xB2) complexity).`
458
+ );
459
+ console.log(
460
+ ` Consider using approximate mode (default) for better performance.`
461
+ );
443
462
  }
444
463
  const stopwords = /* @__PURE__ */ new Set([
445
464
  "return",
@@ -469,7 +488,11 @@ async function detectDuplicatePatterns(files, options) {
469
488
  "undefined",
470
489
  "this"
471
490
  ]);
472
- const tokenize = (norm) => norm.split(/[\s(){}\[\];,\.]+/).filter((t) => t && t.length >= 3 && !stopwords.has(t.toLowerCase()));
491
+ const tokenize = (norm) => {
492
+ const punctuation = "(){}[];.,";
493
+ const cleaned = norm.split("").map((ch) => punctuation.includes(ch) ? " " : ch).join("");
494
+ return cleaned.split(/\s+/).filter((t) => t && t.length >= 3 && !stopwords.has(t.toLowerCase()));
495
+ };
473
496
  const blockTokens = allBlocks.map((b) => tokenize(b.normalized));
474
497
  const invertedIndex = /* @__PURE__ */ new Map();
475
498
  if (approx) {
@@ -486,9 +509,13 @@ async function detectDuplicatePatterns(files, options) {
486
509
  }
487
510
  const totalComparisons = approx ? void 0 : allBlocks.length * (allBlocks.length - 1) / 2;
488
511
  if (totalComparisons !== void 0) {
489
- console.log(`Processing ${totalComparisons.toLocaleString()} comparisons in batches...`);
512
+ console.log(
513
+ `Processing ${totalComparisons.toLocaleString()} comparisons in batches...`
514
+ );
490
515
  } else {
491
- console.log(`Using approximate candidate selection to reduce comparisons...`);
516
+ console.log(
517
+ `Using approximate candidate selection to reduce comparisons...`
518
+ );
492
519
  }
493
520
  let comparisonsProcessed = 0;
494
521
  let comparisonsBudgetExhausted = false;
@@ -499,16 +526,24 @@ async function detectDuplicatePatterns(files, options) {
499
526
  break;
500
527
  }
501
528
  if (i % batchSize === 0 && i > 0) {
502
- const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
503
- const duplicatesFound = duplicates.length;
504
- if (totalComparisons !== void 0) {
505
- const progress = (comparisonsProcessed / totalComparisons * 100).toFixed(1);
506
- const remaining = totalComparisons - comparisonsProcessed;
507
- const rate = comparisonsProcessed / parseFloat(elapsed);
508
- const eta = remaining > 0 ? (remaining / rate).toFixed(0) : 0;
509
- console.log(` ${progress}% (${comparisonsProcessed.toLocaleString()}/${totalComparisons.toLocaleString()} comparisons, ${elapsed}s elapsed, ~${eta}s remaining, ${duplicatesFound} duplicates)`);
529
+ if (options.onProgress) {
530
+ options.onProgress(i, allBlocks.length, `pattern-detect: analyzing blocks`);
510
531
  } else {
511
- console.log(` Processed ${i.toLocaleString()}/${allBlocks.length} blocks (${elapsed}s elapsed, ${duplicatesFound} duplicates)`);
532
+ const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
533
+ const duplicatesFound = duplicates.length;
534
+ if (totalComparisons !== void 0) {
535
+ const progress = (comparisonsProcessed / totalComparisons * 100).toFixed(1);
536
+ const remaining = totalComparisons - comparisonsProcessed;
537
+ const rate = comparisonsProcessed / parseFloat(elapsed);
538
+ const eta = remaining > 0 ? (remaining / rate).toFixed(0) : 0;
539
+ console.log(
540
+ ` ${progress}% (${comparisonsProcessed.toLocaleString()}/${totalComparisons.toLocaleString()} comparisons, ${elapsed}s elapsed, ~${eta}s remaining, ${duplicatesFound} duplicates)`
541
+ );
542
+ } else {
543
+ console.log(
544
+ ` Processed ${i.toLocaleString()}/${allBlocks.length} blocks (${elapsed}s elapsed, ${duplicatesFound} duplicates)`
545
+ );
546
+ }
512
547
  }
513
548
  await new Promise((resolve) => setImmediate(resolve));
514
549
  }
@@ -542,8 +577,12 @@ async function detectDuplicatePatterns(files, options) {
542
577
  if (approx && candidates) {
543
578
  for (const { j } of candidates) {
544
579
  if (!approx && maxComparisons !== Infinity && comparisonsProcessed >= maxComparisons) {
545
- console.log(`\u26A0\uFE0F Comparison safety limit reached (${maxComparisons.toLocaleString()} comparisons in --no-approx mode).`);
546
- console.log(` This prevents excessive runtime on large repos. Consider using approximate mode (default) or --min-lines to reduce blocks.`);
580
+ console.log(
581
+ `\u26A0\uFE0F Comparison safety limit reached (${maxComparisons.toLocaleString()} comparisons in --no-approx mode).`
582
+ );
583
+ console.log(
584
+ ` This prevents excessive runtime on large repos. Consider using approximate mode (default) or --min-lines to reduce blocks.`
585
+ );
547
586
  break;
548
587
  }
549
588
  comparisonsProcessed++;
@@ -576,10 +615,16 @@ async function detectDuplicatePatterns(files, options) {
576
615
  };
577
616
  duplicates.push(duplicate);
578
617
  if (streamResults) {
579
- console.log(`
580
- \u2705 Found: ${duplicate.patternType} ${Math.round(similarity * 100)}% similar`);
581
- console.log(` ${duplicate.file1}:${duplicate.line1}-${duplicate.endLine1} \u21D4 ${duplicate.file2}:${duplicate.line2}-${duplicate.endLine2}`);
582
- console.log(` Token cost: ${duplicate.tokenCost.toLocaleString()}`);
618
+ console.log(
619
+ `
620
+ \u2705 Found: ${duplicate.patternType} ${Math.round(similarity * 100)}% similar`
621
+ );
622
+ console.log(
623
+ ` ${duplicate.file1}:${duplicate.line1}-${duplicate.endLine1} \u21D4 ${duplicate.file2}:${duplicate.line2}-${duplicate.endLine2}`
624
+ );
625
+ console.log(
626
+ ` Token cost: ${duplicate.tokenCost.toLocaleString()}`
627
+ );
583
628
  }
584
629
  }
585
630
  }
@@ -617,17 +662,25 @@ async function detectDuplicatePatterns(files, options) {
617
662
  };
618
663
  duplicates.push(duplicate);
619
664
  if (streamResults) {
620
- console.log(`
621
- \u2705 Found: ${duplicate.patternType} ${Math.round(similarity * 100)}% similar`);
622
- console.log(` ${duplicate.file1}:${duplicate.line1}-${duplicate.endLine1} \u21D4 ${duplicate.file2}:${duplicate.line2}-${duplicate.endLine2}`);
623
- console.log(` Token cost: ${duplicate.tokenCost.toLocaleString()}`);
665
+ console.log(
666
+ `
667
+ \u2705 Found: ${duplicate.patternType} ${Math.round(similarity * 100)}% similar`
668
+ );
669
+ console.log(
670
+ ` ${duplicate.file1}:${duplicate.line1}-${duplicate.endLine1} \u21D4 ${duplicate.file2}:${duplicate.line2}-${duplicate.endLine2}`
671
+ );
672
+ console.log(
673
+ ` Token cost: ${duplicate.tokenCost.toLocaleString()}`
674
+ );
624
675
  }
625
676
  }
626
677
  }
627
678
  }
628
679
  }
629
680
  if (comparisonsBudgetExhausted) {
630
- console.log(`\u26A0\uFE0F Comparison budget exhausted (${maxComparisons.toLocaleString()} comparisons). Use --max-comparisons to increase.`);
681
+ console.log(
682
+ `\u26A0\uFE0F Comparison budget exhausted (${maxComparisons.toLocaleString()} comparisons). Use --max-comparisons to increase.`
683
+ );
631
684
  }
632
685
  return duplicates.sort(
633
686
  (a, b) => b.similarity - a.similarity || b.tokenCost - a.tokenCost
@@ -653,7 +706,10 @@ function groupDuplicatesByFilePair(duplicates) {
653
706
  const result = [];
654
707
  for (const [filePair, groupDups] of groups.entries()) {
655
708
  const deduplicated = deduplicateOverlappingRanges(groupDups);
656
- const totalTokenCost = deduplicated.reduce((sum, d) => sum + d.tokenCost, 0);
709
+ const totalTokenCost = deduplicated.reduce(
710
+ (sum, d) => sum + d.tokenCost,
711
+ 0
712
+ );
657
713
  const averageSimilarity = deduplicated.reduce((sum, d) => sum + d.similarity, 0) / deduplicated.length;
658
714
  const maxSimilarity = Math.max(...deduplicated.map((d) => d.similarity));
659
715
  const severity = getHighestSeverity(deduplicated.map((d) => d.severity));
@@ -759,7 +815,9 @@ function identifyCluster(dup) {
759
815
  if ((file1.includes("/components/") || file1.startsWith("components/")) && (file2.includes("/components/") || file2.startsWith("components/")) && dup.patternType === "component") {
760
816
  const component1 = extractComponentName(dup.file1);
761
817
  const component2 = extractComponentName(dup.file2);
762
- console.log(`Component check: ${dup.file1} -> ${component1}, ${dup.file2} -> ${component2}`);
818
+ console.log(
819
+ `Component check: ${dup.file1} -> ${component1}, ${dup.file2} -> ${component2}`
820
+ );
763
821
  if (component1 && component2 && areSimilarComponents(component1, component2)) {
764
822
  const category = getComponentCategory(component1);
765
823
  console.log(`Creating cluster: component-${category}`);
@@ -858,7 +916,7 @@ function getClusterInfo(clusterId, patternType, fileCount) {
858
916
  suggestion: "Extract common middleware, error handling, and response formatting",
859
917
  reason: "API handler duplication leads to inconsistent error handling and response formats"
860
918
  },
861
- "validators": {
919
+ validators: {
862
920
  name: `Validator Patterns (${fileCount} files)`,
863
921
  suggestion: "Consolidate into shared schema validators (Zod/Yup) with reusable rules",
864
922
  reason: "Validator duplication causes inconsistent validation and harder maintenance"
@@ -939,13 +997,22 @@ async function getSmartDefaults(directory, userOptions) {
939
997
  const { scanFiles: scanFiles2 } = await import("@aiready/core");
940
998
  const files = await scanFiles2(scanOptions);
941
999
  const estimatedBlocks = files.length * 3;
942
- const maxCandidatesPerBlock = Math.max(3, Math.min(10, Math.floor(3e4 / estimatedBlocks)));
1000
+ const maxCandidatesPerBlock = Math.max(
1001
+ 3,
1002
+ Math.min(10, Math.floor(3e4 / estimatedBlocks))
1003
+ );
943
1004
  const minSimilarity = Math.min(0.75, 0.5 + estimatedBlocks / 1e4 * 0.25);
944
- const minLines = Math.max(6, Math.min(12, 6 + Math.floor(estimatedBlocks / 2e3)));
945
- const minSharedTokens = Math.max(10, Math.min(20, 10 + Math.floor(estimatedBlocks / 2e3)));
1005
+ const minLines = Math.max(
1006
+ 6,
1007
+ Math.min(12, 6 + Math.floor(estimatedBlocks / 2e3))
1008
+ );
1009
+ const minSharedTokens = Math.max(
1010
+ 10,
1011
+ Math.min(20, 10 + Math.floor(estimatedBlocks / 2e3))
1012
+ );
946
1013
  const batchSize = estimatedBlocks > 1e3 ? 200 : 100;
947
1014
  const severity = estimatedBlocks > 5e3 ? "high" : "all";
948
- let defaults = {
1015
+ const defaults = {
949
1016
  rootDir: directory,
950
1017
  minSimilarity,
951
1018
  minLines,
@@ -1015,7 +1082,8 @@ async function analyzePatterns(options) {
1015
1082
  approx,
1016
1083
  minSharedTokens,
1017
1084
  maxCandidatesPerBlock,
1018
- streamResults
1085
+ streamResults,
1086
+ onProgress: options.onProgress
1019
1087
  });
1020
1088
  for (const file of files) {
1021
1089
  const fileDuplicates = duplicates.filter(
@@ -1043,7 +1111,9 @@ async function analyzePatterns(options) {
1043
1111
  medium: ["critical", "major", "minor"]
1044
1112
  };
1045
1113
  const allowedSeverities = severityMap[severity] || ["critical", "major", "minor"];
1046
- filteredIssues = issues.filter((issue) => allowedSeverities.includes(issue.severity));
1114
+ filteredIssues = issues.filter(
1115
+ (issue) => allowedSeverities.includes(issue.severity)
1116
+ );
1047
1117
  }
1048
1118
  const totalTokenCost = fileDuplicates.reduce(
1049
1119
  (sum, dup) => sum + dup.tokenCost,
@@ -1065,7 +1135,11 @@ async function analyzePatterns(options) {
1065
1135
  }
1066
1136
  if (createClusters) {
1067
1137
  const allClusters = createRefactorClusters(duplicates);
1068
- clusters = filterClustersByImpact(allClusters, minClusterTokenCost, minClusterFiles);
1138
+ clusters = filterClustersByImpact(
1139
+ allClusters,
1140
+ minClusterTokenCost,
1141
+ minClusterFiles
1142
+ );
1069
1143
  }
1070
1144
  return { results, duplicates, files, groups, clusters };
1071
1145
  }
@@ -1131,7 +1205,52 @@ var import_fs = require("fs");
1131
1205
  var import_path = require("path");
1132
1206
  var import_core5 = require("@aiready/core");
1133
1207
  var program = new import_commander.Command();
1134
- program.name("aiready-patterns").description("Detect duplicate patterns in your codebase").version("0.1.0").addHelpText("after", "\nCONFIGURATION:\n Supports config files: aiready.json, aiready.config.json, .aiready.json, .aireadyrc.json, aiready.config.js, .aireadyrc.js\n CLI options override config file settings\n\nPARAMETER TUNING:\n If you get too few results: decrease --similarity, --min-lines, or --min-shared-tokens\n If analysis is too slow: increase --min-lines, --min-shared-tokens, or decrease --max-candidates\n If you get too many false positives: increase --similarity or --min-lines\n\nEXAMPLES:\n aiready-patterns . # Basic analysis with smart defaults\n aiready-patterns . --similarity 0.3 --min-lines 3 # More sensitive detection\n aiready-patterns . --max-candidates 50 --no-approx # Slower but more thorough\n aiready-patterns . --output json > report.json # JSON export").argument("<directory>", "Directory to analyze").option("-s, --similarity <number>", "Minimum similarity score (0-1). Lower = more results, higher = fewer but more accurate. Default: 0.4").option("-l, --min-lines <number>", "Minimum lines to consider. Lower = more results, higher = faster analysis. Default: 5").option("--batch-size <number>", "Batch size for comparisons. Higher = faster but more memory. Default: 100").option("--no-approx", "Disable approximate candidate selection. Slower but more thorough on small repos").option("--min-shared-tokens <number>", "Minimum shared tokens to consider a candidate. Higher = faster, fewer results. Default: 8").option("--max-candidates <number>", "Maximum candidates per block. Higher = more thorough but slower. Default: 100").option("--no-stream-results", "Disable incremental output (default: enabled)").option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("--min-severity <level>", "Minimum severity to show: critical|major|minor|info. Default: minor").option("--exclude-test-fixtures", "Exclude test fixture duplication (beforeAll/afterAll)").option("--exclude-templates", "Exclude template file duplication").option("--include-tests", "Include test files in analysis (excluded by default)").option("--max-results <number>", "Maximum number of results to show in console output. Default: 10").option("--no-group-by-file-pair", "Disable grouping duplicates by file pair").option("--no-create-clusters", "Disable creating refactor clusters").option("--min-cluster-tokens <number>", "Minimum token cost for cluster reporting. Default: 1000").option("--min-cluster-files <number>", "Minimum files for cluster reporting. Default: 3").option("--show-raw-duplicates", "Show raw duplicates instead of grouped view").option(
1208
+ program.name("aiready-patterns").description("Detect duplicate patterns in your codebase").version("0.1.0").addHelpText(
1209
+ "after",
1210
+ "\nCONFIGURATION:\n Supports config files: aiready.json, aiready.config.json, .aiready.json, .aireadyrc.json, aiready.config.js, .aireadyrc.js\n CLI options override config file settings\n\nPARAMETER TUNING:\n If you get too few results: decrease --similarity, --min-lines, or --min-shared-tokens\n If analysis is too slow: increase --min-lines, --min-shared-tokens, or decrease --max-candidates\n If you get too many false positives: increase --similarity or --min-lines\n\nEXAMPLES:\n aiready-patterns . # Basic analysis with smart defaults\n aiready-patterns . --similarity 0.3 --min-lines 3 # More sensitive detection\n aiready-patterns . --max-candidates 50 --no-approx # Slower but more thorough\n aiready-patterns . --output json > report.json # JSON export"
1211
+ ).argument("<directory>", "Directory to analyze").option(
1212
+ "-s, --similarity <number>",
1213
+ "Minimum similarity score (0-1). Lower = more results, higher = fewer but more accurate. Default: 0.4"
1214
+ ).option(
1215
+ "-l, --min-lines <number>",
1216
+ "Minimum lines to consider. Lower = more results, higher = faster analysis. Default: 5"
1217
+ ).option(
1218
+ "--batch-size <number>",
1219
+ "Batch size for comparisons. Higher = faster but more memory. Default: 100"
1220
+ ).option(
1221
+ "--no-approx",
1222
+ "Disable approximate candidate selection. Slower but more thorough on small repos"
1223
+ ).option(
1224
+ "--min-shared-tokens <number>",
1225
+ "Minimum shared tokens to consider a candidate. Higher = faster, fewer results. Default: 8"
1226
+ ).option(
1227
+ "--max-candidates <number>",
1228
+ "Maximum candidates per block. Higher = more thorough but slower. Default: 100"
1229
+ ).option(
1230
+ "--no-stream-results",
1231
+ "Disable incremental output (default: enabled)"
1232
+ ).option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option(
1233
+ "--min-severity <level>",
1234
+ "Minimum severity to show: critical|major|minor|info. Default: minor"
1235
+ ).option(
1236
+ "--exclude-test-fixtures",
1237
+ "Exclude test fixture duplication (beforeAll/afterAll)"
1238
+ ).option("--exclude-templates", "Exclude template file duplication").option(
1239
+ "--include-tests",
1240
+ "Include test files in analysis (excluded by default)"
1241
+ ).option(
1242
+ "--max-results <number>",
1243
+ "Maximum number of results to show in console output. Default: 10"
1244
+ ).option("--no-group-by-file-pair", "Disable grouping duplicates by file pair").option("--no-create-clusters", "Disable creating refactor clusters").option(
1245
+ "--min-cluster-tokens <number>",
1246
+ "Minimum token cost for cluster reporting. Default: 1000"
1247
+ ).option(
1248
+ "--min-cluster-files <number>",
1249
+ "Minimum files for cluster reporting. Default: 3"
1250
+ ).option(
1251
+ "--show-raw-duplicates",
1252
+ "Show raw duplicates instead of grouped view"
1253
+ ).option(
1135
1254
  "-o, --output <format>",
1136
1255
  "Output format: console, json, html",
1137
1256
  "console"
@@ -1196,16 +1315,29 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
1196
1315
  (pattern) => !testPatterns.includes(pattern)
1197
1316
  );
1198
1317
  }
1199
- const { results, duplicates: rawDuplicates, files, groups, clusters } = await analyzePatterns(finalOptions);
1318
+ const {
1319
+ results,
1320
+ duplicates: rawDuplicates,
1321
+ files,
1322
+ groups,
1323
+ clusters
1324
+ } = await analyzePatterns(finalOptions);
1200
1325
  let filteredDuplicates = rawDuplicates;
1201
1326
  if (finalOptions.minSeverity) {
1202
- filteredDuplicates = filterBySeverity(filteredDuplicates, finalOptions.minSeverity);
1327
+ filteredDuplicates = filterBySeverity(
1328
+ filteredDuplicates,
1329
+ finalOptions.minSeverity
1330
+ );
1203
1331
  }
1204
1332
  if (finalOptions.excludeTestFixtures) {
1205
- filteredDuplicates = filteredDuplicates.filter((d) => d.matchedRule !== "test-fixtures");
1333
+ filteredDuplicates = filteredDuplicates.filter(
1334
+ (d) => d.matchedRule !== "test-fixtures"
1335
+ );
1206
1336
  }
1207
1337
  if (finalOptions.excludeTemplates) {
1208
- filteredDuplicates = filteredDuplicates.filter((d) => d.matchedRule !== "templates");
1338
+ filteredDuplicates = filteredDuplicates.filter(
1339
+ (d) => d.matchedRule !== "templates"
1340
+ );
1209
1341
  }
1210
1342
  const elapsedTime = ((Date.now() - startTime) / 1e3).toFixed(2);
1211
1343
  const summary = generateSummary(results);
@@ -1259,7 +1391,9 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
1259
1391
  import_chalk.default.white(`\u{1F4C1} Files analyzed: ${import_chalk.default.bold(results.length)}`)
1260
1392
  );
1261
1393
  console.log(
1262
- import_chalk.default.yellow(`\u26A0 AI confusion patterns detected: ${import_chalk.default.bold(totalIssues)}`)
1394
+ import_chalk.default.yellow(
1395
+ `\u26A0 AI confusion patterns detected: ${import_chalk.default.bold(totalIssues)}`
1396
+ )
1263
1397
  );
1264
1398
  console.log(
1265
1399
  import_chalk.default.red(
@@ -1276,12 +1410,16 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
1276
1410
  console.log(import_chalk.default.cyan(divider) + "\n");
1277
1411
  sortedTypes.forEach(([type, count]) => {
1278
1412
  const icon = getPatternIcon(type);
1279
- console.log(`${icon} ${import_chalk.default.white(type.padEnd(15))} ${import_chalk.default.bold(count)}`);
1413
+ console.log(
1414
+ `${icon} ${import_chalk.default.white(type.padEnd(15))} ${import_chalk.default.bold(count)}`
1415
+ );
1280
1416
  });
1281
1417
  }
1282
1418
  if (!finalOptions.showRawDuplicates && groups && groups.length > 0) {
1283
1419
  console.log(import_chalk.default.cyan("\n" + divider));
1284
- console.log(import_chalk.default.bold.white(` \u{1F4E6} DUPLICATE GROUPS (${groups.length} file pairs)`));
1420
+ console.log(
1421
+ import_chalk.default.bold.white(` \u{1F4E6} DUPLICATE GROUPS (${groups.length} file pairs)`)
1422
+ );
1285
1423
  console.log(import_chalk.default.cyan(divider) + "\n");
1286
1424
  const severityOrder = {
1287
1425
  critical: 4,
@@ -1299,39 +1437,63 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
1299
1437
  const [file1, file2] = group.filePair.split("::");
1300
1438
  const file1Name = file1.split("/").pop() || file1;
1301
1439
  const file2Name = file2.split("/").pop() || file2;
1302
- console.log(`${idx + 1}. ${severityBadge} ${import_chalk.default.bold(file1Name)} \u2194 ${import_chalk.default.bold(file2Name)}`);
1303
- console.log(` Occurrences: ${import_chalk.default.bold(group.occurrences)} | Total tokens: ${import_chalk.default.bold(group.totalTokenCost.toLocaleString())} | Avg similarity: ${import_chalk.default.bold(Math.round(group.averageSimilarity * 100) + "%")}`);
1440
+ console.log(
1441
+ `${idx + 1}. ${severityBadge} ${import_chalk.default.bold(file1Name)} \u2194 ${import_chalk.default.bold(file2Name)}`
1442
+ );
1443
+ console.log(
1444
+ ` Occurrences: ${import_chalk.default.bold(group.occurrences)} | Total tokens: ${import_chalk.default.bold(group.totalTokenCost.toLocaleString())} | Avg similarity: ${import_chalk.default.bold(Math.round(group.averageSimilarity * 100) + "%")}`
1445
+ );
1304
1446
  const displayRanges = group.lineRanges.slice(0, 3);
1305
1447
  displayRanges.forEach((range) => {
1306
- console.log(` ${import_chalk.default.gray(file1)}:${import_chalk.default.cyan(`${range.file1.start}-${range.file1.end}`)} \u2194 ${import_chalk.default.gray(file2)}:${import_chalk.default.cyan(`${range.file2.start}-${range.file2.end}`)}`);
1448
+ console.log(
1449
+ ` ${import_chalk.default.gray(file1)}:${import_chalk.default.cyan(`${range.file1.start}-${range.file1.end}`)} \u2194 ${import_chalk.default.gray(file2)}:${import_chalk.default.cyan(`${range.file2.start}-${range.file2.end}`)}`
1450
+ );
1307
1451
  });
1308
1452
  if (group.lineRanges.length > 3) {
1309
- console.log(` ${import_chalk.default.gray(`... and ${group.lineRanges.length - 3} more ranges`)}`);
1453
+ console.log(
1454
+ ` ${import_chalk.default.gray(`... and ${group.lineRanges.length - 3} more ranges`)}`
1455
+ );
1310
1456
  }
1311
1457
  console.log();
1312
1458
  });
1313
1459
  if (groups.length > topGroups.length) {
1314
- console.log(import_chalk.default.gray(` ... and ${groups.length - topGroups.length} more file pairs`));
1460
+ console.log(
1461
+ import_chalk.default.gray(
1462
+ ` ... and ${groups.length - topGroups.length} more file pairs`
1463
+ )
1464
+ );
1315
1465
  }
1316
1466
  }
1317
1467
  if (!finalOptions.showRawDuplicates && clusters && clusters.length > 0) {
1318
1468
  console.log(import_chalk.default.cyan("\n" + divider));
1319
- console.log(import_chalk.default.bold.white(` \u{1F3AF} REFACTOR CLUSTERS (${clusters.length} patterns)`));
1469
+ console.log(
1470
+ import_chalk.default.bold.white(` \u{1F3AF} REFACTOR CLUSTERS (${clusters.length} patterns)`)
1471
+ );
1320
1472
  console.log(import_chalk.default.cyan(divider) + "\n");
1321
1473
  clusters.sort((a, b) => b.totalTokenCost - a.totalTokenCost).forEach((cluster, idx) => {
1322
1474
  const severityBadge = getSeverityBadge(cluster.severity);
1323
- console.log(`${idx + 1}. ${severityBadge} ${import_chalk.default.bold(cluster.name)}`);
1324
- console.log(` 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)}`);
1475
+ console.log(
1476
+ `${idx + 1}. ${severityBadge} ${import_chalk.default.bold(cluster.name)}`
1477
+ );
1478
+ console.log(
1479
+ ` 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)}`
1480
+ );
1325
1481
  const displayFiles = cluster.files.slice(0, 5);
1326
- console.log(` Files (${cluster.files.length}): ${displayFiles.map((f) => import_chalk.default.gray(f.split("/").pop() || f)).join(", ")}`);
1482
+ console.log(
1483
+ ` Files (${cluster.files.length}): ${displayFiles.map((f) => import_chalk.default.gray(f.split("/").pop() || f)).join(", ")}`
1484
+ );
1327
1485
  if (cluster.files.length > 5) {
1328
- console.log(` ${import_chalk.default.gray(`... and ${cluster.files.length - 5} more files`)}`);
1486
+ console.log(
1487
+ ` ${import_chalk.default.gray(`... and ${cluster.files.length - 5} more files`)}`
1488
+ );
1329
1489
  }
1330
1490
  if (cluster.reason) {
1331
1491
  console.log(` ${import_chalk.default.italic.gray(cluster.reason)}`);
1332
1492
  }
1333
1493
  if (cluster.suggestion) {
1334
- console.log(` ${import_chalk.default.cyan("\u2192")} ${import_chalk.default.italic(cluster.suggestion)}`);
1494
+ console.log(
1495
+ ` ${import_chalk.default.cyan("\u2192")} ${import_chalk.default.italic(cluster.suggestion)}`
1496
+ );
1335
1497
  }
1336
1498
  console.log();
1337
1499
  });
@@ -1355,10 +1517,18 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
1355
1517
  const severityBadge = getSeverityBadge(dup.severity);
1356
1518
  const file1Name = dup.file1.split("/").pop() || dup.file1;
1357
1519
  const file2Name = dup.file2.split("/").pop() || dup.file2;
1358
- console.log(`${severityBadge} ${import_chalk.default.bold(file1Name)} \u2194 ${import_chalk.default.bold(file2Name)}`);
1359
- console.log(` Similarity: ${import_chalk.default.bold(Math.round(dup.similarity * 100) + "%")} | Pattern: ${dup.patternType} | Tokens: ${import_chalk.default.bold(dup.tokenCost.toLocaleString())}`);
1360
- console.log(` ${import_chalk.default.gray(dup.file1)}:${import_chalk.default.cyan(dup.line1 + "-" + dup.endLine1)}`);
1361
- console.log(` ${import_chalk.default.gray(dup.file2)}:${import_chalk.default.cyan(dup.line2 + "-" + dup.endLine2)}`);
1520
+ console.log(
1521
+ `${severityBadge} ${import_chalk.default.bold(file1Name)} \u2194 ${import_chalk.default.bold(file2Name)}`
1522
+ );
1523
+ console.log(
1524
+ ` Similarity: ${import_chalk.default.bold(Math.round(dup.similarity * 100) + "%")} | Pattern: ${dup.patternType} | Tokens: ${import_chalk.default.bold(dup.tokenCost.toLocaleString())}`
1525
+ );
1526
+ console.log(
1527
+ ` ${import_chalk.default.gray(dup.file1)}:${import_chalk.default.cyan(dup.line1 + "-" + dup.endLine1)}`
1528
+ );
1529
+ console.log(
1530
+ ` ${import_chalk.default.gray(dup.file2)}:${import_chalk.default.cyan(dup.line2 + "-" + dup.endLine2)}`
1531
+ );
1362
1532
  if (dup.reason) {
1363
1533
  console.log(` ${import_chalk.default.italic.gray(dup.reason)}`);
1364
1534
  }
@@ -1368,7 +1538,11 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
1368
1538
  console.log();
1369
1539
  });
1370
1540
  if (filteredDuplicates.length > topDuplicates.length) {
1371
- console.log(import_chalk.default.gray(` ... and ${filteredDuplicates.length - topDuplicates.length} more duplicates`));
1541
+ console.log(
1542
+ import_chalk.default.gray(
1543
+ ` ... and ${filteredDuplicates.length - topDuplicates.length} more duplicates`
1544
+ )
1545
+ );
1372
1546
  }
1373
1547
  }
1374
1548
  const allIssues = results.flatMap(
@@ -1382,27 +1556,45 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
1382
1556
  console.log(import_chalk.default.bold.white(" CRITICAL ISSUES (>95% similar)"));
1383
1557
  console.log(import_chalk.default.cyan(divider) + "\n");
1384
1558
  criticalIssues.slice(0, 5).forEach((issue) => {
1385
- console.log(import_chalk.default.red("\u25CF ") + import_chalk.default.white(`${issue.file}:${issue.location.line}`));
1559
+ console.log(
1560
+ import_chalk.default.red("\u25CF ") + import_chalk.default.white(`${issue.file}:${issue.location.line}`)
1561
+ );
1386
1562
  console.log(` ${import_chalk.default.dim(issue.message)}`);
1387
- console.log(` ${import_chalk.default.green("\u2192")} ${import_chalk.default.italic(issue.suggestion)}
1388
- `);
1563
+ console.log(
1564
+ ` ${import_chalk.default.green("\u2192")} ${import_chalk.default.italic(issue.suggestion)}
1565
+ `
1566
+ );
1389
1567
  });
1390
1568
  }
1391
1569
  if (totalIssues === 0) {
1392
1570
  console.log(import_chalk.default.green("\n\u2728 Great! No duplicate patterns detected.\n"));
1393
- console.log(import_chalk.default.yellow("\u{1F4A1} If you expected to find duplicates, try adjusting parameters:"));
1394
- console.log(import_chalk.default.dim(" \u2022 Lower similarity threshold: --similarity 0.3"));
1571
+ console.log(
1572
+ import_chalk.default.yellow(
1573
+ "\u{1F4A1} If you expected to find duplicates, try adjusting parameters:"
1574
+ )
1575
+ );
1576
+ console.log(
1577
+ import_chalk.default.dim(" \u2022 Lower similarity threshold: --similarity 0.3")
1578
+ );
1395
1579
  console.log(import_chalk.default.dim(" \u2022 Reduce minimum lines: --min-lines 3"));
1396
1580
  console.log(import_chalk.default.dim(" \u2022 Include test files: --include-tests"));
1397
- console.log(import_chalk.default.dim(" \u2022 Lower shared tokens threshold: --min-shared-tokens 5"));
1581
+ console.log(
1582
+ import_chalk.default.dim(" \u2022 Lower shared tokens threshold: --min-shared-tokens 5")
1583
+ );
1398
1584
  console.log("");
1399
1585
  }
1400
1586
  if (totalIssues > 0 && totalIssues < 5) {
1401
- console.log(import_chalk.default.yellow("\n\u{1F4A1} Few results found. To find more duplicates, try:"));
1402
- console.log(import_chalk.default.dim(" \u2022 Lower similarity threshold: --similarity 0.3"));
1587
+ console.log(
1588
+ import_chalk.default.yellow("\n\u{1F4A1} Few results found. To find more duplicates, try:")
1589
+ );
1590
+ console.log(
1591
+ import_chalk.default.dim(" \u2022 Lower similarity threshold: --similarity 0.3")
1592
+ );
1403
1593
  console.log(import_chalk.default.dim(" \u2022 Reduce minimum lines: --min-lines 3"));
1404
1594
  console.log(import_chalk.default.dim(" \u2022 Include test files: --include-tests"));
1405
- console.log(import_chalk.default.dim(" \u2022 Lower shared tokens threshold: --min-shared-tokens 5"));
1595
+ console.log(
1596
+ import_chalk.default.dim(" \u2022 Lower shared tokens threshold: --min-shared-tokens 5")
1597
+ );
1406
1598
  console.log("");
1407
1599
  }
1408
1600
  console.log(import_chalk.default.cyan(divider));
@@ -1420,7 +1612,9 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
1420
1612
  )
1421
1613
  );
1422
1614
  console.log(
1423
- import_chalk.default.dim("\u{1F41B} Found a bug? Report it: https://github.com/caopengau/aiready-pattern-detect/issues\n")
1615
+ import_chalk.default.dim(
1616
+ "\u{1F41B} Found a bug? Report it: https://github.com/caopengau/aiready-pattern-detect/issues\n"
1617
+ )
1424
1618
  );
1425
1619
  });
1426
1620
  function getPatternIcon(type) {