@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/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>", "Filter by severity: critical|high|medium|all. Default: all").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(
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
- severity: "all",
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
- severity: options.severity || mergedConfig.severity,
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
- let filteredDuplicates = rawDuplicates;
651
- if (finalOptions.severity !== "all") {
652
- const severityThresholds = {
653
- critical: 0.95,
654
- high: 0.9,
655
- medium: 0.4
656
- };
657
- const threshold = severityThresholds[finalOptions.severity] || 0.4;
658
- filteredDuplicates = rawDuplicates.filter((dup) => dup.similarity >= threshold);
659
- }
660
- const topDuplicates = filteredDuplicates.sort((a, b) => b.similarity - a.similarity).slice(0, finalOptions.maxResults);
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 severity = dup.similarity > 0.95 ? "CRITICAL" : dup.similarity > 0.9 ? "HIGH" : "MEDIUM";
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(`${severityIcon} ${severity}: ${import_chalk.default.bold(file1Name)} \u2194 ${import_chalk.default.bold(file2Name)}`);
667
- console.log(` Similarity: ${import_chalk.default.bold(Math.round(dup.similarity * 100) + "%")} | Wasted: ${import_chalk.default.bold(dup.tokenCost.toLocaleString())} tokens each`);
668
- console.log(` Location: lines ${import_chalk.default.cyan(dup.line1 + "-" + dup.endLine1)} \u2194 lines ${import_chalk.default.cyan(dup.line2 + "-" + dup.endLine2)}
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-65G3HXLQ.mjs";
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>", "Filter by severity: critical|high|medium|all. Default: all").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
+ 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
- severity: "all",
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
- severity: options.severity || mergedConfig.severity,
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
- let filteredDuplicates = rawDuplicates;
139
- if (finalOptions.severity !== "all") {
140
- const severityThresholds = {
141
- critical: 0.95,
142
- high: 0.9,
143
- medium: 0.4
144
- };
145
- const threshold = severityThresholds[finalOptions.severity] || 0.4;
146
- filteredDuplicates = rawDuplicates.filter((dup) => dup.similarity >= threshold);
147
- }
148
- const topDuplicates = filteredDuplicates.sort((a, b) => b.similarity - a.similarity).slice(0, finalOptions.maxResults);
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 severity = dup.similarity > 0.95 ? "CRITICAL" : dup.similarity > 0.9 ? "HIGH" : "MEDIUM";
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(`${severityIcon} ${severity}: ${chalk.bold(file1Name)} \u2194 ${chalk.bold(file2Name)}`);
155
- console.log(` Similarity: ${chalk.bold(Math.round(dup.similarity * 100) + "%")} | Wasted: ${chalk.bold(dup.tokenCost.toLocaleString())} tokens each`);
156
- console.log(` Location: lines ${chalk.cyan(dup.line1 + "-" + dup.endLine1)} \u2194 lines ${chalk.cyan(dup.line2 + "-" + dup.endLine2)}
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 };