@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/CONTRIBUTING.md +8 -1
- package/README.md +1 -1
- package/dist/chunk-FWUKMJEQ.mjs +1133 -0
- package/dist/chunk-SLDK5PQK.mjs +1129 -0
- package/dist/chunk-YSDOUNJJ.mjs +1142 -0
- package/dist/cli.js +269 -75
- package/dist/cli.mjs +160 -36
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +121 -42
- package/dist/index.mjs +1 -1
- package/dist/python-extractor-ELAKYK2W.mjs +140 -0
- package/package.json +2 -2
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(
|
|
117
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
442
|
-
|
|
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) =>
|
|
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(
|
|
512
|
+
console.log(
|
|
513
|
+
`Processing ${totalComparisons.toLocaleString()} comparisons in batches...`
|
|
514
|
+
);
|
|
490
515
|
} else {
|
|
491
|
-
console.log(
|
|
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
|
-
|
|
503
|
-
|
|
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
|
-
|
|
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(
|
|
546
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
945
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
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(
|
|
1327
|
+
filteredDuplicates = filterBySeverity(
|
|
1328
|
+
filteredDuplicates,
|
|
1329
|
+
finalOptions.minSeverity
|
|
1330
|
+
);
|
|
1203
1331
|
}
|
|
1204
1332
|
if (finalOptions.excludeTestFixtures) {
|
|
1205
|
-
filteredDuplicates = filteredDuplicates.filter(
|
|
1333
|
+
filteredDuplicates = filteredDuplicates.filter(
|
|
1334
|
+
(d) => d.matchedRule !== "test-fixtures"
|
|
1335
|
+
);
|
|
1206
1336
|
}
|
|
1207
1337
|
if (finalOptions.excludeTemplates) {
|
|
1208
|
-
filteredDuplicates = filteredDuplicates.filter(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1303
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1324
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1394
|
-
|
|
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(
|
|
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(
|
|
1402
|
-
|
|
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(
|
|
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(
|
|
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) {
|