@aiready/pattern-detect 0.14.18 → 0.14.21
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-H4ADJYOG.mjs +925 -0
- package/dist/cli.js +176 -174
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +67 -37
- package/dist/index.d.ts +67 -37
- package/dist/index.js +188 -167
- package/dist/index.mjs +13 -3
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ToolProvider, Severity, ScanOptions, AnalysisResult, CostConfig, ToolScoringOutput } from '@aiready/core';
|
|
2
2
|
export { Severity } from '@aiready/core';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Pattern Detection Tool Provider
|
|
6
|
+
*/
|
|
7
|
+
declare const PatternDetectProvider: ToolProvider;
|
|
8
|
+
|
|
4
9
|
type PatternType = 'api-handler' | 'validator' | 'utility' | 'class-method' | 'component' | 'function' | 'unknown';
|
|
5
10
|
interface DuplicatePattern {
|
|
6
11
|
file1: string;
|
|
@@ -68,45 +73,19 @@ interface RefactorCluster {
|
|
|
68
73
|
reason?: string;
|
|
69
74
|
suggestion?: string;
|
|
70
75
|
}
|
|
71
|
-
|
|
72
76
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* Based on:
|
|
76
|
-
* - Number of duplicates per file
|
|
77
|
-
* - Token waste per file
|
|
78
|
-
* - High-impact duplicates (>1000 tokens or >70% similarity)
|
|
79
|
-
*
|
|
80
|
-
* Includes business value metrics:
|
|
81
|
-
* - Estimated monthly cost of token waste
|
|
82
|
-
* - Estimated developer hours to fix
|
|
77
|
+
* Group raw duplicates by file pairs to reduce noise
|
|
83
78
|
*/
|
|
84
|
-
declare function
|
|
85
|
-
|
|
79
|
+
declare function groupDuplicatesByFilePair(duplicates: DuplicatePattern[]): DuplicateGroup[];
|
|
86
80
|
/**
|
|
87
|
-
*
|
|
81
|
+
* Create clusters of highly related files (refactor targets)
|
|
82
|
+
* Uses a simple connected components algorithm
|
|
88
83
|
*/
|
|
89
|
-
declare
|
|
90
|
-
|
|
84
|
+
declare function createRefactorClusters(duplicates: DuplicatePattern[]): RefactorCluster[];
|
|
91
85
|
/**
|
|
92
|
-
*
|
|
93
|
-
*/
|
|
94
|
-
declare function calculateSeverity(file1: string, file2: string, code: string, similarity: number, linesOfCode: number): {
|
|
95
|
-
severity: Severity;
|
|
96
|
-
reason?: string;
|
|
97
|
-
suggestion?: string;
|
|
98
|
-
matchedRule?: string;
|
|
99
|
-
};
|
|
100
|
-
/**
|
|
101
|
-
* Get a human-readable severity label with emoji
|
|
86
|
+
* Filter clusters by impact threshold
|
|
102
87
|
*/
|
|
103
|
-
declare function
|
|
104
|
-
/**
|
|
105
|
-
* Filter duplicates by minimum severity threshold
|
|
106
|
-
*/
|
|
107
|
-
declare function filterBySeverity<T extends {
|
|
108
|
-
severity: Severity;
|
|
109
|
-
}>(duplicates: T[], minSeverity: Severity): T[];
|
|
88
|
+
declare function filterClustersByImpact(clusters: RefactorCluster[], minTokenCost?: number, minFiles?: number): RefactorCluster[];
|
|
110
89
|
|
|
111
90
|
interface PatternDetectOptions extends ScanOptions {
|
|
112
91
|
minSimilarity?: number;
|
|
@@ -152,9 +131,60 @@ declare function analyzePatterns(options: PatternDetectOptions): Promise<{
|
|
|
152
131
|
clusters?: RefactorCluster[];
|
|
153
132
|
config: PatternDetectOptions;
|
|
154
133
|
}>;
|
|
134
|
+
declare function generateSummary(results: AnalysisResult[]): PatternSummary;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Calculate AI Readiness Score for pattern duplication (0-100)
|
|
138
|
+
*
|
|
139
|
+
* Based on:
|
|
140
|
+
* - Number of duplicates per file
|
|
141
|
+
* - Token waste per file
|
|
142
|
+
* - High-impact duplicates (>1000 tokens or >70% similarity)
|
|
143
|
+
*
|
|
144
|
+
* Includes business value metrics:
|
|
145
|
+
* - Estimated monthly cost of token waste
|
|
146
|
+
* - Estimated developer hours to fix
|
|
147
|
+
*/
|
|
148
|
+
declare function calculatePatternScore(duplicates: DuplicatePattern[], totalFilesAnalyzed: number, costConfig?: Partial<CostConfig>): ToolScoringOutput;
|
|
149
|
+
|
|
155
150
|
/**
|
|
156
|
-
*
|
|
151
|
+
* Context-aware severity detection for duplicate patterns
|
|
152
|
+
* Identifies intentional duplication patterns and adjusts severity accordingly
|
|
157
153
|
*/
|
|
158
|
-
|
|
154
|
+
interface ContextRule {
|
|
155
|
+
name: string;
|
|
156
|
+
detect: (file: string, code: string) => boolean;
|
|
157
|
+
severity: Severity;
|
|
158
|
+
reason: string;
|
|
159
|
+
suggestion?: string;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Context rules for detecting intentional or acceptable duplication patterns
|
|
163
|
+
* Rules are checked in order - first match wins
|
|
164
|
+
*/
|
|
165
|
+
declare const CONTEXT_RULES: ContextRule[];
|
|
166
|
+
/**
|
|
167
|
+
* Calculate severity based on context rules and code characteristics
|
|
168
|
+
*/
|
|
169
|
+
declare function calculateSeverity(file1: string, file2: string, code: string, similarity: number, linesOfCode: number): {
|
|
170
|
+
severity: Severity;
|
|
171
|
+
reason?: string;
|
|
172
|
+
suggestion?: string;
|
|
173
|
+
matchedRule?: string;
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Get a human-readable severity label with emoji
|
|
177
|
+
*/
|
|
178
|
+
declare function getSeverityLabel(severity: Severity): string;
|
|
179
|
+
/**
|
|
180
|
+
* Filter duplicates by minimum severity threshold
|
|
181
|
+
*/
|
|
182
|
+
declare function filterBySeverity<T extends {
|
|
183
|
+
severity: Severity;
|
|
184
|
+
}>(duplicates: T[], minSeverity: Severity): T[];
|
|
185
|
+
/**
|
|
186
|
+
* Get severity threshold for filtering
|
|
187
|
+
*/
|
|
188
|
+
declare function getSeverityThreshold(severity: Severity): number;
|
|
159
189
|
|
|
160
|
-
export { type DuplicateGroup, type DuplicatePattern, type PatternDetectOptions, PatternDetectProvider, type PatternSummary, type PatternType, type RefactorCluster, analyzePatterns, calculatePatternScore, calculateSeverity, detectDuplicatePatterns, filterBySeverity, generateSummary, getSeverityLabel, getSmartDefaults };
|
|
190
|
+
export { CONTEXT_RULES, type ContextRule, type DuplicateGroup, type DuplicatePattern, type PatternDetectOptions, PatternDetectProvider, type PatternSummary, type PatternType, type RefactorCluster, analyzePatterns, calculatePatternScore, calculateSeverity, createRefactorClusters, detectDuplicatePatterns, filterBySeverity, filterClustersByImpact, generateSummary, getSeverityLabel, getSeverityThreshold, getSmartDefaults, groupDuplicatesByFilePair };
|
package/dist/index.js
CHANGED
|
@@ -30,20 +30,31 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
CONTEXT_RULES: () => CONTEXT_RULES,
|
|
33
34
|
PatternDetectProvider: () => PatternDetectProvider,
|
|
34
|
-
Severity: () =>
|
|
35
|
+
Severity: () => import_core7.Severity,
|
|
35
36
|
analyzePatterns: () => analyzePatterns,
|
|
36
37
|
calculatePatternScore: () => calculatePatternScore,
|
|
37
38
|
calculateSeverity: () => calculateSeverity,
|
|
39
|
+
createRefactorClusters: () => createRefactorClusters,
|
|
38
40
|
detectDuplicatePatterns: () => detectDuplicatePatterns,
|
|
39
41
|
filterBySeverity: () => filterBySeverity,
|
|
42
|
+
filterClustersByImpact: () => filterClustersByImpact,
|
|
40
43
|
generateSummary: () => generateSummary,
|
|
41
44
|
getSeverityLabel: () => getSeverityLabel,
|
|
42
|
-
|
|
45
|
+
getSeverityThreshold: () => getSeverityThreshold,
|
|
46
|
+
getSmartDefaults: () => getSmartDefaults,
|
|
47
|
+
groupDuplicatesByFilePair: () => groupDuplicatesByFilePair
|
|
43
48
|
});
|
|
44
49
|
module.exports = __toCommonJS(index_exports);
|
|
50
|
+
var import_core7 = require("@aiready/core");
|
|
51
|
+
|
|
52
|
+
// src/provider.ts
|
|
45
53
|
var import_core6 = require("@aiready/core");
|
|
46
54
|
|
|
55
|
+
// src/analyzer.ts
|
|
56
|
+
var import_core4 = require("@aiready/core");
|
|
57
|
+
|
|
47
58
|
// src/detector.ts
|
|
48
59
|
var import_core2 = require("@aiready/core");
|
|
49
60
|
|
|
@@ -197,6 +208,15 @@ function filterBySeverity(duplicates, minSeverity) {
|
|
|
197
208
|
return dupIndex >= minIndex;
|
|
198
209
|
});
|
|
199
210
|
}
|
|
211
|
+
function getSeverityThreshold(severity) {
|
|
212
|
+
const thresholds = {
|
|
213
|
+
[import_core.Severity.Critical]: 0.95,
|
|
214
|
+
[import_core.Severity.Major]: 0.85,
|
|
215
|
+
[import_core.Severity.Minor]: 0.5,
|
|
216
|
+
[import_core.Severity.Info]: 0
|
|
217
|
+
};
|
|
218
|
+
return thresholds[severity] || 0;
|
|
219
|
+
}
|
|
200
220
|
|
|
201
221
|
// src/detector.ts
|
|
202
222
|
function normalizeCode(code, isPython = false) {
|
|
@@ -541,153 +561,7 @@ function filterClustersByImpact(clusters, minTokenCost = 1e3, minFiles = 3) {
|
|
|
541
561
|
);
|
|
542
562
|
}
|
|
543
563
|
|
|
544
|
-
// src/
|
|
545
|
-
var import_core4 = require("@aiready/core");
|
|
546
|
-
function calculatePatternScore(duplicates, totalFilesAnalyzed, costConfig) {
|
|
547
|
-
const totalDuplicates = duplicates.length;
|
|
548
|
-
const totalTokenCost = duplicates.reduce((sum, d) => sum + d.tokenCost, 0);
|
|
549
|
-
const highImpactDuplicates = duplicates.filter(
|
|
550
|
-
(d) => d.tokenCost > 1e3 || d.similarity > 0.7
|
|
551
|
-
).length;
|
|
552
|
-
if (totalFilesAnalyzed === 0) {
|
|
553
|
-
return {
|
|
554
|
-
toolName: import_core4.ToolName.PatternDetect,
|
|
555
|
-
score: 100,
|
|
556
|
-
rawMetrics: {
|
|
557
|
-
totalDuplicates: 0,
|
|
558
|
-
totalTokenCost: 0,
|
|
559
|
-
highImpactDuplicates: 0,
|
|
560
|
-
totalFilesAnalyzed: 0
|
|
561
|
-
},
|
|
562
|
-
factors: [],
|
|
563
|
-
recommendations: []
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
const duplicatesPerFile = totalDuplicates / totalFilesAnalyzed * 100;
|
|
567
|
-
const tokenWastePerFile = totalTokenCost / totalFilesAnalyzed;
|
|
568
|
-
const duplicatesPenalty = Math.min(60, duplicatesPerFile * 0.6);
|
|
569
|
-
const tokenPenalty = Math.min(40, tokenWastePerFile / 125);
|
|
570
|
-
const highImpactPenalty = highImpactDuplicates > 0 ? Math.min(15, highImpactDuplicates * 2 - 5) : -5;
|
|
571
|
-
const score = 100 - duplicatesPenalty - tokenPenalty - highImpactPenalty;
|
|
572
|
-
const finalScore = Math.max(0, Math.min(100, Math.round(score)));
|
|
573
|
-
const factors = [
|
|
574
|
-
{
|
|
575
|
-
name: "Duplication Density",
|
|
576
|
-
impact: -Math.round(duplicatesPenalty),
|
|
577
|
-
description: `${duplicatesPerFile.toFixed(1)} duplicates per 100 files`
|
|
578
|
-
},
|
|
579
|
-
{
|
|
580
|
-
name: "Token Waste",
|
|
581
|
-
impact: -Math.round(tokenPenalty),
|
|
582
|
-
description: `${Math.round(tokenWastePerFile)} tokens wasted per file`
|
|
583
|
-
}
|
|
584
|
-
];
|
|
585
|
-
if (highImpactDuplicates > 0) {
|
|
586
|
-
factors.push({
|
|
587
|
-
name: "High-Impact Patterns",
|
|
588
|
-
impact: -Math.round(highImpactPenalty),
|
|
589
|
-
description: `${highImpactDuplicates} high-impact duplicates (>1000 tokens or >70% similar)`
|
|
590
|
-
});
|
|
591
|
-
} else {
|
|
592
|
-
factors.push({
|
|
593
|
-
name: "No High-Impact Patterns",
|
|
594
|
-
impact: 5,
|
|
595
|
-
description: "No severe duplicates detected"
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
const recommendations = [];
|
|
599
|
-
if (highImpactDuplicates > 0) {
|
|
600
|
-
const estimatedImpact = Math.min(15, highImpactDuplicates * 3);
|
|
601
|
-
recommendations.push({
|
|
602
|
-
action: `Deduplicate ${highImpactDuplicates} high-impact pattern${highImpactDuplicates > 1 ? "s" : ""}`,
|
|
603
|
-
estimatedImpact,
|
|
604
|
-
priority: "high"
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
if (totalDuplicates > 10 && duplicatesPerFile > 20) {
|
|
608
|
-
const estimatedImpact = Math.min(10, Math.round(duplicatesPenalty * 0.3));
|
|
609
|
-
recommendations.push({
|
|
610
|
-
action: "Extract common patterns into shared utilities",
|
|
611
|
-
estimatedImpact,
|
|
612
|
-
priority: "medium"
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
if (tokenWastePerFile > 2e3) {
|
|
616
|
-
const estimatedImpact = Math.min(8, Math.round(tokenPenalty * 0.4));
|
|
617
|
-
recommendations.push({
|
|
618
|
-
action: "Consolidate duplicated logic to reduce AI context waste",
|
|
619
|
-
estimatedImpact,
|
|
620
|
-
priority: totalTokenCost > 1e4 ? "high" : "medium"
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
const cfg = { ...import_core4.DEFAULT_COST_CONFIG, ...costConfig };
|
|
624
|
-
const estimatedMonthlyCost = (0, import_core4.calculateMonthlyCost)(totalTokenCost, cfg);
|
|
625
|
-
const issues = duplicates.map((d) => ({
|
|
626
|
-
severity: d.severity === "critical" ? "critical" : d.severity === "major" ? "major" : "minor"
|
|
627
|
-
}));
|
|
628
|
-
const productivityImpact = (0, import_core4.calculateProductivityImpact)(issues);
|
|
629
|
-
return {
|
|
630
|
-
toolName: "pattern-detect",
|
|
631
|
-
score: finalScore,
|
|
632
|
-
rawMetrics: {
|
|
633
|
-
totalDuplicates,
|
|
634
|
-
totalTokenCost,
|
|
635
|
-
highImpactDuplicates,
|
|
636
|
-
totalFilesAnalyzed,
|
|
637
|
-
duplicatesPerFile: Math.round(duplicatesPerFile * 10) / 10,
|
|
638
|
-
tokenWastePerFile: Math.round(tokenWastePerFile),
|
|
639
|
-
// Business value metrics
|
|
640
|
-
estimatedMonthlyCost,
|
|
641
|
-
estimatedDeveloperHours: productivityImpact.totalHours
|
|
642
|
-
},
|
|
643
|
-
factors,
|
|
644
|
-
recommendations
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// src/provider.ts
|
|
649
|
-
var import_core5 = require("@aiready/core");
|
|
650
|
-
var PatternDetectProvider = {
|
|
651
|
-
id: import_core5.ToolName.PatternDetect,
|
|
652
|
-
alias: ["patterns", "duplicates", "duplication"],
|
|
653
|
-
async analyze(options) {
|
|
654
|
-
const results = await analyzePatterns(options);
|
|
655
|
-
return import_core5.SpokeOutputSchema.parse({
|
|
656
|
-
results: results.results,
|
|
657
|
-
summary: {
|
|
658
|
-
totalFiles: results.files.length,
|
|
659
|
-
totalIssues: results.results.reduce(
|
|
660
|
-
(sum, r) => sum + r.issues.length,
|
|
661
|
-
0
|
|
662
|
-
),
|
|
663
|
-
clusters: results.clusters,
|
|
664
|
-
config: Object.fromEntries(
|
|
665
|
-
Object.entries(results.config).filter(
|
|
666
|
-
([key]) => !import_core5.GLOBAL_SCAN_OPTIONS.includes(key) || key === "rootDir"
|
|
667
|
-
)
|
|
668
|
-
)
|
|
669
|
-
},
|
|
670
|
-
metadata: {
|
|
671
|
-
toolName: import_core5.ToolName.PatternDetect,
|
|
672
|
-
version: "0.12.5",
|
|
673
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
674
|
-
}
|
|
675
|
-
});
|
|
676
|
-
},
|
|
677
|
-
score(output, options) {
|
|
678
|
-
const duplicates = output.summary.duplicates || [];
|
|
679
|
-
const totalFiles = output.summary.totalFiles || output.results.length;
|
|
680
|
-
return calculatePatternScore(
|
|
681
|
-
duplicates,
|
|
682
|
-
totalFiles,
|
|
683
|
-
options.costConfig
|
|
684
|
-
);
|
|
685
|
-
},
|
|
686
|
-
defaultWeight: 22
|
|
687
|
-
};
|
|
688
|
-
|
|
689
|
-
// src/index.ts
|
|
690
|
-
import_core6.ToolRegistry.register(PatternDetectProvider);
|
|
564
|
+
// src/analyzer.ts
|
|
691
565
|
function getRefactoringSuggestion(patternType, similarity) {
|
|
692
566
|
const baseMessages = {
|
|
693
567
|
"api-handler": "Extract common middleware or create a base handler class",
|
|
@@ -721,8 +595,7 @@ async function getSmartDefaults(directory, userOptions) {
|
|
|
721
595
|
include: userOptions.include || ["**/*.{ts,tsx,js,jsx,py,java}"],
|
|
722
596
|
exclude: userOptions.exclude
|
|
723
597
|
};
|
|
724
|
-
const
|
|
725
|
-
const files = await scanFiles2(scanOptions);
|
|
598
|
+
const files = await (0, import_core4.scanFiles)(scanOptions);
|
|
726
599
|
const fileCount = files.length;
|
|
727
600
|
const estimatedBlocks = fileCount * 5;
|
|
728
601
|
const minLines = Math.max(
|
|
@@ -788,19 +661,18 @@ async function analyzePatterns(options) {
|
|
|
788
661
|
minClusterFiles = 3,
|
|
789
662
|
...scanOptions
|
|
790
663
|
} = finalOptions;
|
|
791
|
-
const
|
|
792
|
-
const files = await scanFiles2(scanOptions);
|
|
664
|
+
const files = await (0, import_core4.scanFiles)(scanOptions);
|
|
793
665
|
const estimatedBlocks = files.length * 3;
|
|
794
666
|
logConfiguration(finalOptions, estimatedBlocks);
|
|
795
667
|
const results = [];
|
|
796
|
-
const
|
|
668
|
+
const READ_BATCH_SIZE = 50;
|
|
797
669
|
const fileContents = [];
|
|
798
|
-
for (let i = 0; i < files.length; i +=
|
|
799
|
-
const batch = files.slice(i, i +
|
|
670
|
+
for (let i = 0; i < files.length; i += READ_BATCH_SIZE) {
|
|
671
|
+
const batch = files.slice(i, i + READ_BATCH_SIZE);
|
|
800
672
|
const batchContents = await Promise.all(
|
|
801
673
|
batch.map(async (file) => ({
|
|
802
674
|
file,
|
|
803
|
-
content: await (0,
|
|
675
|
+
content: await (0, import_core4.readFileContent)(file)
|
|
804
676
|
}))
|
|
805
677
|
);
|
|
806
678
|
fileContents.push(...batchContents);
|
|
@@ -821,9 +693,9 @@ async function analyzePatterns(options) {
|
|
|
821
693
|
);
|
|
822
694
|
const issues = fileDuplicates.map((dup) => {
|
|
823
695
|
const otherFile = dup.file1 === file ? dup.file2 : dup.file1;
|
|
824
|
-
const severity2 = dup.similarity > 0.95 ?
|
|
696
|
+
const severity2 = dup.similarity > 0.95 ? import_core4.Severity.Critical : dup.similarity > 0.9 ? import_core4.Severity.Major : import_core4.Severity.Minor;
|
|
825
697
|
return {
|
|
826
|
-
type:
|
|
698
|
+
type: import_core4.IssueType.DuplicatePattern,
|
|
827
699
|
severity: severity2,
|
|
828
700
|
message: `${dup.patternType} pattern ${Math.round(dup.similarity * 100)}% similar to ${otherFile} (${dup.tokenCost} tokens wasted)`,
|
|
829
701
|
location: {
|
|
@@ -836,11 +708,11 @@ async function analyzePatterns(options) {
|
|
|
836
708
|
let filteredIssues = issues;
|
|
837
709
|
if (severity !== "all") {
|
|
838
710
|
const severityMap = {
|
|
839
|
-
critical: [
|
|
840
|
-
high: [
|
|
841
|
-
medium: [
|
|
711
|
+
critical: [import_core4.Severity.Critical],
|
|
712
|
+
high: [import_core4.Severity.Critical, import_core4.Severity.Major],
|
|
713
|
+
medium: [import_core4.Severity.Critical, import_core4.Severity.Major, import_core4.Severity.Minor]
|
|
842
714
|
};
|
|
843
|
-
const allowedSeverities = severityMap[severity] || [
|
|
715
|
+
const allowedSeverities = severityMap[severity] || [import_core4.Severity.Critical, import_core4.Severity.Major, import_core4.Severity.Minor];
|
|
844
716
|
filteredIssues = issues.filter(
|
|
845
717
|
(issue) => allowedSeverities.includes(issue.severity)
|
|
846
718
|
);
|
|
@@ -906,14 +778,11 @@ function generateSummary(results) {
|
|
|
906
778
|
path: issue.location.file,
|
|
907
779
|
startLine: issue.location.line,
|
|
908
780
|
endLine: 0
|
|
909
|
-
// Not available from Issue
|
|
910
781
|
},
|
|
911
782
|
{
|
|
912
783
|
path: fileMatch?.[1] || "unknown",
|
|
913
784
|
startLine: 0,
|
|
914
|
-
// Not available from Issue
|
|
915
785
|
endLine: 0
|
|
916
|
-
// Not available from Issue
|
|
917
786
|
}
|
|
918
787
|
],
|
|
919
788
|
similarity: similarityMatch ? parseInt(similarityMatch[1]) / 100 : 0,
|
|
@@ -928,16 +797,168 @@ function generateSummary(results) {
|
|
|
928
797
|
topDuplicates
|
|
929
798
|
};
|
|
930
799
|
}
|
|
800
|
+
|
|
801
|
+
// src/scoring.ts
|
|
802
|
+
var import_core5 = require("@aiready/core");
|
|
803
|
+
function calculatePatternScore(duplicates, totalFilesAnalyzed, costConfig) {
|
|
804
|
+
const totalDuplicates = duplicates.length;
|
|
805
|
+
const totalTokenCost = duplicates.reduce((sum, d) => sum + d.tokenCost, 0);
|
|
806
|
+
const highImpactDuplicates = duplicates.filter(
|
|
807
|
+
(d) => d.tokenCost > 1e3 || d.similarity > 0.7
|
|
808
|
+
).length;
|
|
809
|
+
if (totalFilesAnalyzed === 0) {
|
|
810
|
+
return {
|
|
811
|
+
toolName: import_core5.ToolName.PatternDetect,
|
|
812
|
+
score: 100,
|
|
813
|
+
rawMetrics: {
|
|
814
|
+
totalDuplicates: 0,
|
|
815
|
+
totalTokenCost: 0,
|
|
816
|
+
highImpactDuplicates: 0,
|
|
817
|
+
totalFilesAnalyzed: 0
|
|
818
|
+
},
|
|
819
|
+
factors: [],
|
|
820
|
+
recommendations: []
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
const duplicatesPerFile = totalDuplicates / totalFilesAnalyzed * 100;
|
|
824
|
+
const tokenWastePerFile = totalTokenCost / totalFilesAnalyzed;
|
|
825
|
+
const duplicatesPenalty = Math.min(60, duplicatesPerFile * 0.6);
|
|
826
|
+
const tokenPenalty = Math.min(40, tokenWastePerFile / 125);
|
|
827
|
+
const highImpactPenalty = highImpactDuplicates > 0 ? Math.min(15, highImpactDuplicates * 2 - 5) : -5;
|
|
828
|
+
const score = 100 - duplicatesPenalty - tokenPenalty - highImpactPenalty;
|
|
829
|
+
const finalScore = Math.max(0, Math.min(100, Math.round(score)));
|
|
830
|
+
const factors = [
|
|
831
|
+
{
|
|
832
|
+
name: "Duplication Density",
|
|
833
|
+
impact: -Math.round(duplicatesPenalty),
|
|
834
|
+
description: `${duplicatesPerFile.toFixed(1)} duplicates per 100 files`
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
name: "Token Waste",
|
|
838
|
+
impact: -Math.round(tokenPenalty),
|
|
839
|
+
description: `${Math.round(tokenWastePerFile)} tokens wasted per file`
|
|
840
|
+
}
|
|
841
|
+
];
|
|
842
|
+
if (highImpactDuplicates > 0) {
|
|
843
|
+
factors.push({
|
|
844
|
+
name: "High-Impact Patterns",
|
|
845
|
+
impact: -Math.round(highImpactPenalty),
|
|
846
|
+
description: `${highImpactDuplicates} high-impact duplicates (>1000 tokens or >70% similar)`
|
|
847
|
+
});
|
|
848
|
+
} else {
|
|
849
|
+
factors.push({
|
|
850
|
+
name: "No High-Impact Patterns",
|
|
851
|
+
impact: 5,
|
|
852
|
+
description: "No severe duplicates detected"
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
const recommendations = [];
|
|
856
|
+
if (highImpactDuplicates > 0) {
|
|
857
|
+
const estimatedImpact = Math.min(15, highImpactDuplicates * 3);
|
|
858
|
+
recommendations.push({
|
|
859
|
+
action: `Deduplicate ${highImpactDuplicates} high-impact pattern${highImpactDuplicates > 1 ? "s" : ""}`,
|
|
860
|
+
estimatedImpact,
|
|
861
|
+
priority: "high"
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
if (totalDuplicates > 10 && duplicatesPerFile > 20) {
|
|
865
|
+
const estimatedImpact = Math.min(10, Math.round(duplicatesPenalty * 0.3));
|
|
866
|
+
recommendations.push({
|
|
867
|
+
action: "Extract common patterns into shared utilities",
|
|
868
|
+
estimatedImpact,
|
|
869
|
+
priority: "medium"
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
if (tokenWastePerFile > 2e3) {
|
|
873
|
+
const estimatedImpact = Math.min(8, Math.round(tokenPenalty * 0.4));
|
|
874
|
+
recommendations.push({
|
|
875
|
+
action: "Consolidate duplicated logic to reduce AI context waste",
|
|
876
|
+
estimatedImpact,
|
|
877
|
+
priority: totalTokenCost > 1e4 ? "high" : "medium"
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
const cfg = { ...import_core5.DEFAULT_COST_CONFIG, ...costConfig };
|
|
881
|
+
const estimatedMonthlyCost = (0, import_core5.calculateMonthlyCost)(totalTokenCost, cfg);
|
|
882
|
+
const issues = duplicates.map((d) => ({
|
|
883
|
+
severity: d.severity === "critical" ? "critical" : d.severity === "major" ? "major" : "minor"
|
|
884
|
+
}));
|
|
885
|
+
const productivityImpact = (0, import_core5.calculateProductivityImpact)(issues);
|
|
886
|
+
return {
|
|
887
|
+
toolName: "pattern-detect",
|
|
888
|
+
score: finalScore,
|
|
889
|
+
rawMetrics: {
|
|
890
|
+
totalDuplicates,
|
|
891
|
+
totalTokenCost,
|
|
892
|
+
highImpactDuplicates,
|
|
893
|
+
totalFilesAnalyzed,
|
|
894
|
+
duplicatesPerFile: Math.round(duplicatesPerFile * 10) / 10,
|
|
895
|
+
tokenWastePerFile: Math.round(tokenWastePerFile),
|
|
896
|
+
// Business value metrics
|
|
897
|
+
estimatedMonthlyCost,
|
|
898
|
+
estimatedDeveloperHours: productivityImpact.totalHours
|
|
899
|
+
},
|
|
900
|
+
factors,
|
|
901
|
+
recommendations
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/provider.ts
|
|
906
|
+
var PatternDetectProvider = {
|
|
907
|
+
id: import_core6.ToolName.PatternDetect,
|
|
908
|
+
alias: ["patterns", "duplicates", "duplication"],
|
|
909
|
+
async analyze(options) {
|
|
910
|
+
const results = await analyzePatterns(options);
|
|
911
|
+
return import_core6.SpokeOutputSchema.parse({
|
|
912
|
+
results: results.results,
|
|
913
|
+
summary: {
|
|
914
|
+
totalFiles: results.files.length,
|
|
915
|
+
totalIssues: results.results.reduce(
|
|
916
|
+
(sum, r) => sum + r.issues.length,
|
|
917
|
+
0
|
|
918
|
+
),
|
|
919
|
+
clusters: results.clusters,
|
|
920
|
+
config: Object.fromEntries(
|
|
921
|
+
Object.entries(results.config).filter(
|
|
922
|
+
([key]) => !import_core6.GLOBAL_SCAN_OPTIONS.includes(key) || key === "rootDir"
|
|
923
|
+
)
|
|
924
|
+
)
|
|
925
|
+
},
|
|
926
|
+
metadata: {
|
|
927
|
+
toolName: import_core6.ToolName.PatternDetect,
|
|
928
|
+
version: "0.12.5",
|
|
929
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
},
|
|
933
|
+
score(output, options) {
|
|
934
|
+
const duplicates = output.summary.duplicates || [];
|
|
935
|
+
const totalFiles = output.summary.totalFiles || output.results.length;
|
|
936
|
+
return calculatePatternScore(
|
|
937
|
+
duplicates,
|
|
938
|
+
totalFiles,
|
|
939
|
+
options.costConfig
|
|
940
|
+
);
|
|
941
|
+
},
|
|
942
|
+
defaultWeight: 22
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
// src/index.ts
|
|
946
|
+
import_core7.ToolRegistry.register(PatternDetectProvider);
|
|
931
947
|
// Annotate the CommonJS export names for ESM import in node:
|
|
932
948
|
0 && (module.exports = {
|
|
949
|
+
CONTEXT_RULES,
|
|
933
950
|
PatternDetectProvider,
|
|
934
951
|
Severity,
|
|
935
952
|
analyzePatterns,
|
|
936
953
|
calculatePatternScore,
|
|
937
954
|
calculateSeverity,
|
|
955
|
+
createRefactorClusters,
|
|
938
956
|
detectDuplicatePatterns,
|
|
939
957
|
filterBySeverity,
|
|
958
|
+
filterClustersByImpact,
|
|
940
959
|
generateSummary,
|
|
941
960
|
getSeverityLabel,
|
|
942
|
-
|
|
961
|
+
getSeverityThreshold,
|
|
962
|
+
getSmartDefaults,
|
|
963
|
+
groupDuplicatesByFilePair
|
|
943
964
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CONTEXT_RULES,
|
|
2
3
|
PatternDetectProvider,
|
|
3
4
|
Severity,
|
|
4
5
|
analyzePatterns,
|
|
5
6
|
calculatePatternScore,
|
|
6
7
|
calculateSeverity,
|
|
8
|
+
createRefactorClusters,
|
|
7
9
|
detectDuplicatePatterns,
|
|
8
10
|
filterBySeverity,
|
|
11
|
+
filterClustersByImpact,
|
|
9
12
|
generateSummary,
|
|
10
13
|
getSeverityLabel,
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
getSeverityThreshold,
|
|
15
|
+
getSmartDefaults,
|
|
16
|
+
groupDuplicatesByFilePair
|
|
17
|
+
} from "./chunk-H4ADJYOG.mjs";
|
|
13
18
|
export {
|
|
19
|
+
CONTEXT_RULES,
|
|
14
20
|
PatternDetectProvider,
|
|
15
21
|
Severity,
|
|
16
22
|
analyzePatterns,
|
|
17
23
|
calculatePatternScore,
|
|
18
24
|
calculateSeverity,
|
|
25
|
+
createRefactorClusters,
|
|
19
26
|
detectDuplicatePatterns,
|
|
20
27
|
filterBySeverity,
|
|
28
|
+
filterClustersByImpact,
|
|
21
29
|
generateSummary,
|
|
22
30
|
getSeverityLabel,
|
|
23
|
-
|
|
31
|
+
getSeverityThreshold,
|
|
32
|
+
getSmartDefaults,
|
|
33
|
+
groupDuplicatesByFilePair
|
|
24
34
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/pattern-detect",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.21",
|
|
4
4
|
"description": "Semantic duplicate pattern detection for AI-generated code - finds similar implementations that waste AI context tokens",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"commander": "^14.0.0",
|
|
47
47
|
"chalk": "^5.3.0",
|
|
48
|
-
"@aiready/core": "0.21.
|
|
48
|
+
"@aiready/core": "0.21.21"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"tsup": "^8.3.5",
|