@aiready/pattern-detect 0.9.2 → 0.9.3
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/chunk-H73HEG7M.mjs +670 -0
- package/dist/chunk-PBCXSG7E.mjs +658 -0
- package/dist/cli.js +216 -22
- package/dist/cli.mjs +56 -21
- package/dist/index.d.mts +30 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +178 -2
- package/dist/index.mjs +7 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -31,6 +31,144 @@ var import_core2 = require("@aiready/core");
|
|
|
31
31
|
|
|
32
32
|
// src/detector.ts
|
|
33
33
|
var import_core = require("@aiready/core");
|
|
34
|
+
|
|
35
|
+
// src/context-rules.ts
|
|
36
|
+
var CONTEXT_RULES = [
|
|
37
|
+
// Test Fixtures - Intentional duplication for test isolation
|
|
38
|
+
{
|
|
39
|
+
name: "test-fixtures",
|
|
40
|
+
detect: (file, code) => {
|
|
41
|
+
const isTestFile = file.includes(".test.") || file.includes(".spec.") || file.includes("__tests__") || file.includes("/test/") || file.includes("/tests/");
|
|
42
|
+
const hasTestFixtures = code.includes("beforeAll") || code.includes("afterAll") || code.includes("beforeEach") || code.includes("afterEach") || code.includes("setUp") || code.includes("tearDown");
|
|
43
|
+
return isTestFile && hasTestFixtures;
|
|
44
|
+
},
|
|
45
|
+
severity: "info",
|
|
46
|
+
reason: "Test fixture duplication is intentional for test isolation",
|
|
47
|
+
suggestion: "Consider if shared test setup would improve maintainability without coupling tests"
|
|
48
|
+
},
|
|
49
|
+
// Email/Document Templates - Often intentionally similar for consistency
|
|
50
|
+
{
|
|
51
|
+
name: "templates",
|
|
52
|
+
detect: (file, code) => {
|
|
53
|
+
const isTemplate = file.includes("/templates/") || file.includes("-template") || file.includes("/email-templates/") || file.includes("/emails/");
|
|
54
|
+
const hasTemplateContent = (code.includes("return") || code.includes("export")) && (code.includes("html") || code.includes("subject") || code.includes("body"));
|
|
55
|
+
return isTemplate && hasTemplateContent;
|
|
56
|
+
},
|
|
57
|
+
severity: "low",
|
|
58
|
+
reason: "Template duplication may be intentional for maintainability and branding consistency",
|
|
59
|
+
suggestion: "Extract shared structure only if templates become hard to maintain"
|
|
60
|
+
},
|
|
61
|
+
// E2E/Integration Test Page Objects - Test independence
|
|
62
|
+
{
|
|
63
|
+
name: "e2e-page-objects",
|
|
64
|
+
detect: (file, code) => {
|
|
65
|
+
const isE2ETest = file.includes("e2e/") || file.includes("/e2e/") || file.includes(".e2e.") || file.includes("/playwright/") || file.includes("playwright/") || file.includes("/cypress/") || file.includes("cypress/") || file.includes("/integration/") || file.includes("integration/");
|
|
66
|
+
const hasPageObjectPatterns = code.includes("page.") || code.includes("await page") || code.includes("locator") || code.includes("getBy") || code.includes("selector") || code.includes("click(") || code.includes("fill(");
|
|
67
|
+
return isE2ETest && hasPageObjectPatterns;
|
|
68
|
+
},
|
|
69
|
+
severity: "low",
|
|
70
|
+
reason: "E2E test duplication ensures test independence and reduces coupling",
|
|
71
|
+
suggestion: "Consider page object pattern only if duplication causes maintenance issues"
|
|
72
|
+
},
|
|
73
|
+
// Configuration Files - Often necessarily similar by design
|
|
74
|
+
{
|
|
75
|
+
name: "config-files",
|
|
76
|
+
detect: (file) => {
|
|
77
|
+
return file.endsWith(".config.ts") || file.endsWith(".config.js") || file.includes("jest.config") || file.includes("vite.config") || file.includes("webpack.config") || file.includes("rollup.config") || file.includes("tsconfig");
|
|
78
|
+
},
|
|
79
|
+
severity: "low",
|
|
80
|
+
reason: "Configuration files often have similar structure by design",
|
|
81
|
+
suggestion: "Consider shared config base only if configurations become hard to maintain"
|
|
82
|
+
},
|
|
83
|
+
// Type Definitions - Duplication for type safety and module independence
|
|
84
|
+
{
|
|
85
|
+
name: "type-definitions",
|
|
86
|
+
detect: (file, code) => {
|
|
87
|
+
const isTypeFile = file.endsWith(".d.ts") || file.includes("/types/");
|
|
88
|
+
const hasTypeDefinitions = code.includes("interface ") || code.includes("type ") || code.includes("enum ");
|
|
89
|
+
return isTypeFile && hasTypeDefinitions;
|
|
90
|
+
},
|
|
91
|
+
severity: "info",
|
|
92
|
+
reason: "Type duplication may be intentional for module independence and type safety",
|
|
93
|
+
suggestion: "Extract to shared types package only if causing maintenance burden"
|
|
94
|
+
},
|
|
95
|
+
// Migration Scripts - One-off scripts that are similar by nature
|
|
96
|
+
{
|
|
97
|
+
name: "migration-scripts",
|
|
98
|
+
detect: (file) => {
|
|
99
|
+
return file.includes("/migrations/") || file.includes("/migrate/") || file.includes(".migration.");
|
|
100
|
+
},
|
|
101
|
+
severity: "info",
|
|
102
|
+
reason: "Migration scripts are typically one-off and intentionally similar",
|
|
103
|
+
suggestion: "Duplication is acceptable for migration scripts"
|
|
104
|
+
},
|
|
105
|
+
// Mock Data - Test data intentionally duplicated
|
|
106
|
+
{
|
|
107
|
+
name: "mock-data",
|
|
108
|
+
detect: (file, code) => {
|
|
109
|
+
const isMockFile = file.includes("/mocks/") || file.includes("/__mocks__/") || file.includes("/fixtures/") || file.includes(".mock.") || file.includes(".fixture.");
|
|
110
|
+
const hasMockData = code.includes("mock") || code.includes("Mock") || code.includes("fixture") || code.includes("stub") || code.includes("export const");
|
|
111
|
+
return isMockFile && hasMockData;
|
|
112
|
+
},
|
|
113
|
+
severity: "info",
|
|
114
|
+
reason: "Mock data duplication is expected for comprehensive test coverage",
|
|
115
|
+
suggestion: "Consider shared factories only for complex mock generation"
|
|
116
|
+
}
|
|
117
|
+
];
|
|
118
|
+
function calculateSeverity(file1, file2, code, similarity, linesOfCode) {
|
|
119
|
+
for (const rule of CONTEXT_RULES) {
|
|
120
|
+
if (rule.detect(file1, code) || rule.detect(file2, code)) {
|
|
121
|
+
return {
|
|
122
|
+
severity: rule.severity,
|
|
123
|
+
reason: rule.reason,
|
|
124
|
+
suggestion: rule.suggestion,
|
|
125
|
+
matchedRule: rule.name
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (similarity >= 0.95 && linesOfCode >= 30) {
|
|
130
|
+
return {
|
|
131
|
+
severity: "critical",
|
|
132
|
+
reason: "Large nearly-identical code blocks waste tokens and create maintenance burden",
|
|
133
|
+
suggestion: "Extract to shared utility module immediately"
|
|
134
|
+
};
|
|
135
|
+
} else if (similarity >= 0.95 && linesOfCode >= 15) {
|
|
136
|
+
return {
|
|
137
|
+
severity: "high",
|
|
138
|
+
reason: "Nearly identical code should be consolidated",
|
|
139
|
+
suggestion: "Move to shared utility file"
|
|
140
|
+
};
|
|
141
|
+
} else if (similarity >= 0.85) {
|
|
142
|
+
return {
|
|
143
|
+
severity: "high",
|
|
144
|
+
reason: "High similarity indicates significant duplication",
|
|
145
|
+
suggestion: "Extract common logic to shared function"
|
|
146
|
+
};
|
|
147
|
+
} else if (similarity >= 0.7) {
|
|
148
|
+
return {
|
|
149
|
+
severity: "medium",
|
|
150
|
+
reason: "Moderate similarity detected",
|
|
151
|
+
suggestion: "Consider extracting shared patterns if code evolves together"
|
|
152
|
+
};
|
|
153
|
+
} else {
|
|
154
|
+
return {
|
|
155
|
+
severity: "low",
|
|
156
|
+
reason: "Minor similarity detected",
|
|
157
|
+
suggestion: "Monitor but refactoring may not be worthwhile"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function filterBySeverity(duplicates, minSeverity) {
|
|
162
|
+
const severityOrder = ["info", "low", "medium", "high", "critical"];
|
|
163
|
+
const minIndex = severityOrder.indexOf(minSeverity);
|
|
164
|
+
if (minIndex === -1) return duplicates;
|
|
165
|
+
return duplicates.filter((dup) => {
|
|
166
|
+
const dupIndex = severityOrder.indexOf(dup.severity);
|
|
167
|
+
return dupIndex >= minIndex;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/detector.ts
|
|
34
172
|
function categorizePattern(code) {
|
|
35
173
|
const lower = code.toLowerCase();
|
|
36
174
|
if (lower.includes("request") && lower.includes("response") || lower.includes("router.") || lower.includes("app.get") || lower.includes("app.post") || lower.includes("express") || lower.includes("ctx.body")) {
|
|
@@ -246,6 +384,13 @@ async function detectDuplicatePatterns(files, options) {
|
|
|
246
384
|
const block2 = allBlocks[j];
|
|
247
385
|
const similarity = jaccardSimilarity(blockTokens[i], blockTokens[j]);
|
|
248
386
|
if (similarity >= minSimilarity) {
|
|
387
|
+
const { severity, reason, suggestion, matchedRule } = calculateSeverity(
|
|
388
|
+
block1.file,
|
|
389
|
+
block2.file,
|
|
390
|
+
block1.content,
|
|
391
|
+
similarity,
|
|
392
|
+
block1.linesOfCode
|
|
393
|
+
);
|
|
249
394
|
const duplicate = {
|
|
250
395
|
file1: block1.file,
|
|
251
396
|
file2: block2.file,
|
|
@@ -257,7 +402,11 @@ async function detectDuplicatePatterns(files, options) {
|
|
|
257
402
|
snippet: block1.content.split("\n").slice(0, 5).join("\n") + "\n...",
|
|
258
403
|
patternType: block1.patternType,
|
|
259
404
|
tokenCost: block1.tokenCost + block2.tokenCost,
|
|
260
|
-
linesOfCode: block1.linesOfCode
|
|
405
|
+
linesOfCode: block1.linesOfCode,
|
|
406
|
+
severity,
|
|
407
|
+
reason,
|
|
408
|
+
suggestion,
|
|
409
|
+
matchedRule
|
|
261
410
|
};
|
|
262
411
|
duplicates.push(duplicate);
|
|
263
412
|
if (streamResults) {
|
|
@@ -276,6 +425,13 @@ async function detectDuplicatePatterns(files, options) {
|
|
|
276
425
|
if (block1.file === block2.file) continue;
|
|
277
426
|
const similarity = jaccardSimilarity(blockTokens[i], blockTokens[j]);
|
|
278
427
|
if (similarity >= minSimilarity) {
|
|
428
|
+
const { severity, reason, suggestion, matchedRule } = calculateSeverity(
|
|
429
|
+
block1.file,
|
|
430
|
+
block2.file,
|
|
431
|
+
block1.content,
|
|
432
|
+
similarity,
|
|
433
|
+
block1.linesOfCode
|
|
434
|
+
);
|
|
279
435
|
const duplicate = {
|
|
280
436
|
file1: block1.file,
|
|
281
437
|
file2: block2.file,
|
|
@@ -287,7 +443,11 @@ async function detectDuplicatePatterns(files, options) {
|
|
|
287
443
|
snippet: block1.content.split("\n").slice(0, 5).join("\n") + "\n...",
|
|
288
444
|
patternType: block1.patternType,
|
|
289
445
|
tokenCost: block1.tokenCost + block2.tokenCost,
|
|
290
|
-
linesOfCode: block1.linesOfCode
|
|
446
|
+
linesOfCode: block1.linesOfCode,
|
|
447
|
+
severity,
|
|
448
|
+
reason,
|
|
449
|
+
suggestion,
|
|
450
|
+
matchedRule
|
|
291
451
|
};
|
|
292
452
|
duplicates.push(duplicate);
|
|
293
453
|
if (streamResults) {
|
|
@@ -523,7 +683,7 @@ var import_fs = require("fs");
|
|
|
523
683
|
var import_path = require("path");
|
|
524
684
|
var import_core3 = require("@aiready/core");
|
|
525
685
|
var program = new import_commander.Command();
|
|
526
|
-
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("--severity <level>", "
|
|
686
|
+
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|high|medium|low|info. Default: medium").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(
|
|
527
687
|
"-o, --output <format>",
|
|
528
688
|
"Output format: console, json, html",
|
|
529
689
|
"console"
|
|
@@ -541,7 +701,9 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
|
|
|
541
701
|
streamResults: true,
|
|
542
702
|
include: void 0,
|
|
543
703
|
exclude: void 0,
|
|
544
|
-
|
|
704
|
+
minSeverity: "medium",
|
|
705
|
+
excludeTestFixtures: false,
|
|
706
|
+
excludeTemplates: false,
|
|
545
707
|
includeTests: false,
|
|
546
708
|
maxResults: 10
|
|
547
709
|
};
|
|
@@ -558,7 +720,9 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
|
|
|
558
720
|
streamResults: options.streamResults !== false && mergedConfig.streamResults,
|
|
559
721
|
include: options.include?.split(",") || mergedConfig.include,
|
|
560
722
|
exclude: options.exclude?.split(",") || mergedConfig.exclude,
|
|
561
|
-
|
|
723
|
+
minSeverity: options.minSeverity || mergedConfig.minSeverity,
|
|
724
|
+
excludeTestFixtures: options.excludeTestFixtures || mergedConfig.excludeTestFixtures,
|
|
725
|
+
excludeTemplates: options.excludeTemplates || mergedConfig.excludeTemplates,
|
|
562
726
|
includeTests: options.includeTests || mergedConfig.includeTests,
|
|
563
727
|
maxResults: options.maxResults ? parseInt(options.maxResults) : mergedConfig.maxResults
|
|
564
728
|
};
|
|
@@ -574,6 +738,16 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
|
|
|
574
738
|
finalOptions.exclude = finalOptions.exclude ? [...finalOptions.exclude, ...testPatterns] : testPatterns;
|
|
575
739
|
}
|
|
576
740
|
const { results, duplicates: rawDuplicates, files } = await analyzePatterns(finalOptions);
|
|
741
|
+
let filteredDuplicates = rawDuplicates;
|
|
742
|
+
if (finalOptions.minSeverity) {
|
|
743
|
+
filteredDuplicates = filterBySeverity(filteredDuplicates, finalOptions.minSeverity);
|
|
744
|
+
}
|
|
745
|
+
if (finalOptions.excludeTestFixtures) {
|
|
746
|
+
filteredDuplicates = filteredDuplicates.filter((d) => d.matchedRule !== "test-fixtures");
|
|
747
|
+
}
|
|
748
|
+
if (finalOptions.excludeTemplates) {
|
|
749
|
+
filteredDuplicates = filteredDuplicates.filter((d) => d.matchedRule !== "templates");
|
|
750
|
+
}
|
|
577
751
|
const elapsedTime = ((Date.now() - startTime) / 1e3).toFixed(2);
|
|
578
752
|
const summary = generateSummary(results);
|
|
579
753
|
const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0);
|
|
@@ -647,27 +821,37 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
|
|
|
647
821
|
console.log(import_chalk.default.cyan("\n" + divider));
|
|
648
822
|
console.log(import_chalk.default.bold.white(" TOP DUPLICATE PATTERNS"));
|
|
649
823
|
console.log(import_chalk.default.cyan(divider) + "\n");
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
824
|
+
const severityOrder = {
|
|
825
|
+
critical: 5,
|
|
826
|
+
high: 4,
|
|
827
|
+
medium: 3,
|
|
828
|
+
low: 2,
|
|
829
|
+
info: 1
|
|
830
|
+
};
|
|
831
|
+
const topDuplicates = filteredDuplicates.sort((a, b) => {
|
|
832
|
+
const severityDiff = severityOrder[b.severity] - severityOrder[a.severity];
|
|
833
|
+
if (severityDiff !== 0) return severityDiff;
|
|
834
|
+
return b.similarity - a.similarity;
|
|
835
|
+
}).slice(0, finalOptions.maxResults);
|
|
661
836
|
topDuplicates.forEach((dup, idx) => {
|
|
662
|
-
const
|
|
663
|
-
const severityIcon = dup.similarity > 0.95 ? "\u{1F534}" : dup.similarity > 0.9 ? "\u{1F7E1}" : "\u{1F535}";
|
|
837
|
+
const severityBadge = getSeverityBadge(dup.severity);
|
|
664
838
|
const file1Name = dup.file1.split("/").pop() || dup.file1;
|
|
665
839
|
const file2Name = dup.file2.split("/").pop() || dup.file2;
|
|
666
|
-
console.log(`${
|
|
667
|
-
console.log(` Similarity: ${import_chalk.default.bold(Math.round(dup.similarity * 100) + "%")} |
|
|
668
|
-
console.log(`
|
|
669
|
-
`);
|
|
840
|
+
console.log(`${severityBadge} ${import_chalk.default.bold(file1Name)} \u2194 ${import_chalk.default.bold(file2Name)}`);
|
|
841
|
+
console.log(` Similarity: ${import_chalk.default.bold(Math.round(dup.similarity * 100) + "%")} | Pattern: ${dup.patternType} | Tokens: ${import_chalk.default.bold(dup.tokenCost.toLocaleString())}`);
|
|
842
|
+
console.log(` ${import_chalk.default.gray(dup.file1)}:${import_chalk.default.cyan(dup.line1 + "-" + dup.endLine1)}`);
|
|
843
|
+
console.log(` ${import_chalk.default.gray(dup.file2)}:${import_chalk.default.cyan(dup.line2 + "-" + dup.endLine2)}`);
|
|
844
|
+
if (dup.reason) {
|
|
845
|
+
console.log(` ${import_chalk.default.italic.gray(dup.reason)}`);
|
|
846
|
+
}
|
|
847
|
+
if (dup.suggestion) {
|
|
848
|
+
console.log(` ${import_chalk.default.cyan("\u2192")} ${import_chalk.default.italic(dup.suggestion)}`);
|
|
849
|
+
}
|
|
850
|
+
console.log();
|
|
670
851
|
});
|
|
852
|
+
if (filteredDuplicates.length > topDuplicates.length) {
|
|
853
|
+
console.log(import_chalk.default.gray(` ... and ${filteredDuplicates.length - topDuplicates.length} more duplicates`));
|
|
854
|
+
}
|
|
671
855
|
}
|
|
672
856
|
const allIssues = results.flatMap(
|
|
673
857
|
(r) => r.issues.map((issue) => ({ ...issue, file: r.fileName }))
|
|
@@ -808,3 +992,13 @@ function generateHTMLReport(summary, results) {
|
|
|
808
992
|
</html>`;
|
|
809
993
|
}
|
|
810
994
|
program.parse();
|
|
995
|
+
function getSeverityBadge(severity) {
|
|
996
|
+
const badges = {
|
|
997
|
+
critical: import_chalk.default.bgRed.white.bold(" CRITICAL "),
|
|
998
|
+
high: import_chalk.default.bgYellow.black.bold(" HIGH "),
|
|
999
|
+
medium: import_chalk.default.bgBlue.white.bold(" MEDIUM "),
|
|
1000
|
+
low: import_chalk.default.bgGray.white(" LOW "),
|
|
1001
|
+
info: import_chalk.default.bgCyan.black(" INFO ")
|
|
1002
|
+
};
|
|
1003
|
+
return badges[severity] || badges.info;
|
|
1004
|
+
}
|
package/dist/cli.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
analyzePatterns,
|
|
4
|
+
filterBySeverity,
|
|
4
5
|
generateSummary
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-H73HEG7M.mjs";
|
|
6
7
|
|
|
7
8
|
// src/cli.ts
|
|
8
9
|
import { Command } from "commander";
|
|
@@ -11,7 +12,7 @@ import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
|
11
12
|
import { dirname } from "path";
|
|
12
13
|
import { loadConfig, mergeConfigWithDefaults, resolveOutputPath } from "@aiready/core";
|
|
13
14
|
var program = new Command();
|
|
14
|
-
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("--severity <level>", "
|
|
15
|
+
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|high|medium|low|info. Default: medium").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(
|
|
15
16
|
"-o, --output <format>",
|
|
16
17
|
"Output format: console, json, html",
|
|
17
18
|
"console"
|
|
@@ -29,7 +30,9 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
|
|
|
29
30
|
streamResults: true,
|
|
30
31
|
include: void 0,
|
|
31
32
|
exclude: void 0,
|
|
32
|
-
|
|
33
|
+
minSeverity: "medium",
|
|
34
|
+
excludeTestFixtures: false,
|
|
35
|
+
excludeTemplates: false,
|
|
33
36
|
includeTests: false,
|
|
34
37
|
maxResults: 10
|
|
35
38
|
};
|
|
@@ -46,7 +49,9 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
|
|
|
46
49
|
streamResults: options.streamResults !== false && mergedConfig.streamResults,
|
|
47
50
|
include: options.include?.split(",") || mergedConfig.include,
|
|
48
51
|
exclude: options.exclude?.split(",") || mergedConfig.exclude,
|
|
49
|
-
|
|
52
|
+
minSeverity: options.minSeverity || mergedConfig.minSeverity,
|
|
53
|
+
excludeTestFixtures: options.excludeTestFixtures || mergedConfig.excludeTestFixtures,
|
|
54
|
+
excludeTemplates: options.excludeTemplates || mergedConfig.excludeTemplates,
|
|
50
55
|
includeTests: options.includeTests || mergedConfig.includeTests,
|
|
51
56
|
maxResults: options.maxResults ? parseInt(options.maxResults) : mergedConfig.maxResults
|
|
52
57
|
};
|
|
@@ -62,6 +67,16 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
|
|
|
62
67
|
finalOptions.exclude = finalOptions.exclude ? [...finalOptions.exclude, ...testPatterns] : testPatterns;
|
|
63
68
|
}
|
|
64
69
|
const { results, duplicates: rawDuplicates, files } = await analyzePatterns(finalOptions);
|
|
70
|
+
let filteredDuplicates = rawDuplicates;
|
|
71
|
+
if (finalOptions.minSeverity) {
|
|
72
|
+
filteredDuplicates = filterBySeverity(filteredDuplicates, finalOptions.minSeverity);
|
|
73
|
+
}
|
|
74
|
+
if (finalOptions.excludeTestFixtures) {
|
|
75
|
+
filteredDuplicates = filteredDuplicates.filter((d) => d.matchedRule !== "test-fixtures");
|
|
76
|
+
}
|
|
77
|
+
if (finalOptions.excludeTemplates) {
|
|
78
|
+
filteredDuplicates = filteredDuplicates.filter((d) => d.matchedRule !== "templates");
|
|
79
|
+
}
|
|
65
80
|
const elapsedTime = ((Date.now() - startTime) / 1e3).toFixed(2);
|
|
66
81
|
const summary = generateSummary(results);
|
|
67
82
|
const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0);
|
|
@@ -135,27 +150,37 @@ program.name("aiready-patterns").description("Detect duplicate patterns in your
|
|
|
135
150
|
console.log(chalk.cyan("\n" + divider));
|
|
136
151
|
console.log(chalk.bold.white(" TOP DUPLICATE PATTERNS"));
|
|
137
152
|
console.log(chalk.cyan(divider) + "\n");
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
const severityOrder = {
|
|
154
|
+
critical: 5,
|
|
155
|
+
high: 4,
|
|
156
|
+
medium: 3,
|
|
157
|
+
low: 2,
|
|
158
|
+
info: 1
|
|
159
|
+
};
|
|
160
|
+
const topDuplicates = filteredDuplicates.sort((a, b) => {
|
|
161
|
+
const severityDiff = severityOrder[b.severity] - severityOrder[a.severity];
|
|
162
|
+
if (severityDiff !== 0) return severityDiff;
|
|
163
|
+
return b.similarity - a.similarity;
|
|
164
|
+
}).slice(0, finalOptions.maxResults);
|
|
149
165
|
topDuplicates.forEach((dup, idx) => {
|
|
150
|
-
const
|
|
151
|
-
const severityIcon = dup.similarity > 0.95 ? "\u{1F534}" : dup.similarity > 0.9 ? "\u{1F7E1}" : "\u{1F535}";
|
|
166
|
+
const severityBadge = getSeverityBadge(dup.severity);
|
|
152
167
|
const file1Name = dup.file1.split("/").pop() || dup.file1;
|
|
153
168
|
const file2Name = dup.file2.split("/").pop() || dup.file2;
|
|
154
|
-
console.log(`${
|
|
155
|
-
console.log(` Similarity: ${chalk.bold(Math.round(dup.similarity * 100) + "%")} |
|
|
156
|
-
console.log(`
|
|
157
|
-
`);
|
|
169
|
+
console.log(`${severityBadge} ${chalk.bold(file1Name)} \u2194 ${chalk.bold(file2Name)}`);
|
|
170
|
+
console.log(` Similarity: ${chalk.bold(Math.round(dup.similarity * 100) + "%")} | Pattern: ${dup.patternType} | Tokens: ${chalk.bold(dup.tokenCost.toLocaleString())}`);
|
|
171
|
+
console.log(` ${chalk.gray(dup.file1)}:${chalk.cyan(dup.line1 + "-" + dup.endLine1)}`);
|
|
172
|
+
console.log(` ${chalk.gray(dup.file2)}:${chalk.cyan(dup.line2 + "-" + dup.endLine2)}`);
|
|
173
|
+
if (dup.reason) {
|
|
174
|
+
console.log(` ${chalk.italic.gray(dup.reason)}`);
|
|
175
|
+
}
|
|
176
|
+
if (dup.suggestion) {
|
|
177
|
+
console.log(` ${chalk.cyan("\u2192")} ${chalk.italic(dup.suggestion)}`);
|
|
178
|
+
}
|
|
179
|
+
console.log();
|
|
158
180
|
});
|
|
181
|
+
if (filteredDuplicates.length > topDuplicates.length) {
|
|
182
|
+
console.log(chalk.gray(` ... and ${filteredDuplicates.length - topDuplicates.length} more duplicates`));
|
|
183
|
+
}
|
|
159
184
|
}
|
|
160
185
|
const allIssues = results.flatMap(
|
|
161
186
|
(r) => r.issues.map((issue) => ({ ...issue, file: r.fileName }))
|
|
@@ -296,3 +321,13 @@ function generateHTMLReport(summary, results) {
|
|
|
296
321
|
</html>`;
|
|
297
322
|
}
|
|
298
323
|
program.parse();
|
|
324
|
+
function getSeverityBadge(severity) {
|
|
325
|
+
const badges = {
|
|
326
|
+
critical: chalk.bgRed.white.bold(" CRITICAL "),
|
|
327
|
+
high: chalk.bgYellow.black.bold(" HIGH "),
|
|
328
|
+
medium: chalk.bgBlue.white.bold(" MEDIUM "),
|
|
329
|
+
low: chalk.bgGray.white(" LOW "),
|
|
330
|
+
info: chalk.bgCyan.black(" INFO ")
|
|
331
|
+
};
|
|
332
|
+
return badges[severity] || badges.info;
|
|
333
|
+
}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import { ScanOptions, AnalysisResult } from '@aiready/core';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Context-aware severity detection for duplicate patterns
|
|
5
|
+
* Identifies intentional duplication patterns and adjusts severity accordingly
|
|
6
|
+
*/
|
|
7
|
+
type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info';
|
|
8
|
+
/**
|
|
9
|
+
* Calculate severity based on context rules and code characteristics
|
|
10
|
+
*/
|
|
11
|
+
declare function calculateSeverity(file1: string, file2: string, code: string, similarity: number, linesOfCode: number): {
|
|
12
|
+
severity: Severity;
|
|
13
|
+
reason?: string;
|
|
14
|
+
suggestion?: string;
|
|
15
|
+
matchedRule?: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Get a human-readable severity label with emoji
|
|
19
|
+
*/
|
|
20
|
+
declare function getSeverityLabel(severity: Severity): string;
|
|
21
|
+
/**
|
|
22
|
+
* Filter duplicates by minimum severity threshold
|
|
23
|
+
*/
|
|
24
|
+
declare function filterBySeverity<T extends {
|
|
25
|
+
severity: Severity;
|
|
26
|
+
}>(duplicates: T[], minSeverity: Severity): T[];
|
|
27
|
+
|
|
3
28
|
interface DuplicatePattern {
|
|
4
29
|
file1: string;
|
|
5
30
|
file2: string;
|
|
@@ -12,6 +37,10 @@ interface DuplicatePattern {
|
|
|
12
37
|
patternType: PatternType;
|
|
13
38
|
tokenCost: number;
|
|
14
39
|
linesOfCode: number;
|
|
40
|
+
severity: Severity;
|
|
41
|
+
reason?: string;
|
|
42
|
+
suggestion?: string;
|
|
43
|
+
matchedRule?: string;
|
|
15
44
|
}
|
|
16
45
|
type PatternType = 'function' | 'class-method' | 'api-handler' | 'validator' | 'utility' | 'component' | 'unknown';
|
|
17
46
|
interface FileContent {
|
|
@@ -75,4 +104,4 @@ declare function analyzePatterns(options: PatternDetectOptions): Promise<{
|
|
|
75
104
|
*/
|
|
76
105
|
declare function generateSummary(results: AnalysisResult[]): PatternSummary;
|
|
77
106
|
|
|
78
|
-
export { type DuplicatePattern, type PatternDetectOptions, type PatternSummary, type PatternType, analyzePatterns, detectDuplicatePatterns, generateSummary, getSmartDefaults };
|
|
107
|
+
export { type DuplicatePattern, type PatternDetectOptions, type PatternSummary, type PatternType, type Severity, analyzePatterns, calculateSeverity, detectDuplicatePatterns, filterBySeverity, generateSummary, getSeverityLabel, getSmartDefaults };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import { ScanOptions, AnalysisResult } from '@aiready/core';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Context-aware severity detection for duplicate patterns
|
|
5
|
+
* Identifies intentional duplication patterns and adjusts severity accordingly
|
|
6
|
+
*/
|
|
7
|
+
type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info';
|
|
8
|
+
/**
|
|
9
|
+
* Calculate severity based on context rules and code characteristics
|
|
10
|
+
*/
|
|
11
|
+
declare function calculateSeverity(file1: string, file2: string, code: string, similarity: number, linesOfCode: number): {
|
|
12
|
+
severity: Severity;
|
|
13
|
+
reason?: string;
|
|
14
|
+
suggestion?: string;
|
|
15
|
+
matchedRule?: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Get a human-readable severity label with emoji
|
|
19
|
+
*/
|
|
20
|
+
declare function getSeverityLabel(severity: Severity): string;
|
|
21
|
+
/**
|
|
22
|
+
* Filter duplicates by minimum severity threshold
|
|
23
|
+
*/
|
|
24
|
+
declare function filterBySeverity<T extends {
|
|
25
|
+
severity: Severity;
|
|
26
|
+
}>(duplicates: T[], minSeverity: Severity): T[];
|
|
27
|
+
|
|
3
28
|
interface DuplicatePattern {
|
|
4
29
|
file1: string;
|
|
5
30
|
file2: string;
|
|
@@ -12,6 +37,10 @@ interface DuplicatePattern {
|
|
|
12
37
|
patternType: PatternType;
|
|
13
38
|
tokenCost: number;
|
|
14
39
|
linesOfCode: number;
|
|
40
|
+
severity: Severity;
|
|
41
|
+
reason?: string;
|
|
42
|
+
suggestion?: string;
|
|
43
|
+
matchedRule?: string;
|
|
15
44
|
}
|
|
16
45
|
type PatternType = 'function' | 'class-method' | 'api-handler' | 'validator' | 'utility' | 'component' | 'unknown';
|
|
17
46
|
interface FileContent {
|
|
@@ -75,4 +104,4 @@ declare function analyzePatterns(options: PatternDetectOptions): Promise<{
|
|
|
75
104
|
*/
|
|
76
105
|
declare function generateSummary(results: AnalysisResult[]): PatternSummary;
|
|
77
106
|
|
|
78
|
-
export { type DuplicatePattern, type PatternDetectOptions, type PatternSummary, type PatternType, analyzePatterns, detectDuplicatePatterns, generateSummary, getSmartDefaults };
|
|
107
|
+
export { type DuplicatePattern, type PatternDetectOptions, type PatternSummary, type PatternType, type Severity, analyzePatterns, calculateSeverity, detectDuplicatePatterns, filterBySeverity, generateSummary, getSeverityLabel, getSmartDefaults };
|