@aiready/pattern-detect 0.17.1 → 0.17.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.
@@ -472,7 +472,7 @@ async function getSmartDefaults(directory, userOptions) {
472
472
  6,
473
473
  Math.min(20, 6 + Math.floor(estimatedBlocks / 1e3) * 2)
474
474
  );
475
- const minSimilarity = Math.min(0.85, 0.5 + estimatedBlocks / 5e3 * 0.3);
475
+ const minSimilarity = Math.min(0.75, 0.45 + estimatedBlocks / 1e4 * 0.3);
476
476
  const batchSize = estimatedBlocks > 1e3 ? 200 : 100;
477
477
  const severity = estimatedBlocks > 3e3 ? "high" : "all";
478
478
  const maxCandidatesPerBlock = Math.max(
@@ -632,9 +632,9 @@ async function analyzePatterns(options) {
632
632
  return { results, duplicates, files, groups, clusters, config: finalOptions };
633
633
  }
634
634
  function generateSummary(results) {
635
- const allIssues = results.flatMap((r) => r.issues);
635
+ const allIssues = results.flatMap((r) => r.issues || []);
636
636
  const totalTokenCost = results.reduce(
637
- (sum, r) => sum + (r.metrics.tokenCost || 0),
637
+ (sum, r) => sum + (r.metrics?.tokenCost || 0),
638
638
  0
639
639
  );
640
640
  const patternsByType = {
@@ -2,7 +2,7 @@ import {
2
2
  analyzePatterns,
3
3
  generateSummary,
4
4
  getSmartDefaults
5
- } from "../chunk-SUUZMLPS.mjs";
5
+ } from "../chunk-K7BO57OO.mjs";
6
6
  import "../chunk-NQBYYWHJ.mjs";
7
7
  import "../chunk-J2G742QF.mjs";
8
8
  export {
@@ -0,0 +1,391 @@
1
+ import {
2
+ detectDuplicatePatterns
3
+ } from "./chunk-NQBYYWHJ.mjs";
4
+ import {
5
+ calculateSeverity
6
+ } from "./chunk-J2G742QF.mjs";
7
+
8
+ // src/grouping.ts
9
+ import { getSeverityLevel } from "@aiready/core";
10
+ import path from "path";
11
+ function groupDuplicatesByFilePair(duplicates) {
12
+ const groups = /* @__PURE__ */ new Map();
13
+ for (const dup of duplicates) {
14
+ const files = [dup.file1, dup.file2].sort();
15
+ const key = files.join("::");
16
+ if (!groups.has(key)) {
17
+ groups.set(key, {
18
+ filePair: key,
19
+ severity: dup.severity,
20
+ occurrences: 0,
21
+ totalTokenCost: 0,
22
+ averageSimilarity: 0,
23
+ patternTypes: /* @__PURE__ */ new Set(),
24
+ lineRanges: []
25
+ });
26
+ }
27
+ const group = groups.get(key);
28
+ group.occurrences++;
29
+ group.totalTokenCost += dup.tokenCost;
30
+ group.averageSimilarity += dup.similarity;
31
+ group.patternTypes.add(dup.patternType);
32
+ group.lineRanges.push({
33
+ file1: { start: dup.line1, end: dup.endLine1 },
34
+ file2: { start: dup.line2, end: dup.endLine2 }
35
+ });
36
+ const currentSev = dup.severity;
37
+ if (getSeverityLevel(currentSev) > getSeverityLevel(group.severity)) {
38
+ group.severity = currentSev;
39
+ }
40
+ }
41
+ return Array.from(groups.values()).map((g) => ({
42
+ ...g,
43
+ averageSimilarity: g.averageSimilarity / g.occurrences
44
+ }));
45
+ }
46
+ function createRefactorClusters(duplicates) {
47
+ const adjacency = /* @__PURE__ */ new Map();
48
+ const visited = /* @__PURE__ */ new Set();
49
+ const components = [];
50
+ for (const dup of duplicates) {
51
+ if (!adjacency.has(dup.file1)) adjacency.set(dup.file1, /* @__PURE__ */ new Set());
52
+ if (!adjacency.has(dup.file2)) adjacency.set(dup.file2, /* @__PURE__ */ new Set());
53
+ adjacency.get(dup.file1).add(dup.file2);
54
+ adjacency.get(dup.file2).add(dup.file1);
55
+ }
56
+ for (const file of adjacency.keys()) {
57
+ if (visited.has(file)) continue;
58
+ const component = [];
59
+ const queue = [file];
60
+ visited.add(file);
61
+ while (queue.length > 0) {
62
+ const curr = queue.shift();
63
+ component.push(curr);
64
+ for (const neighbor of adjacency.get(curr) || []) {
65
+ if (!visited.has(neighbor)) {
66
+ visited.add(neighbor);
67
+ queue.push(neighbor);
68
+ }
69
+ }
70
+ }
71
+ components.push(component);
72
+ }
73
+ const clusters = [];
74
+ for (const component of components) {
75
+ if (component.length < 2) continue;
76
+ const componentDups = duplicates.filter(
77
+ (d) => component.includes(d.file1) && component.includes(d.file2)
78
+ );
79
+ const totalTokenCost = componentDups.reduce(
80
+ (sum, d) => sum + d.tokenCost,
81
+ 0
82
+ );
83
+ const avgSimilarity = componentDups.reduce((sum, d) => sum + d.similarity, 0) / Math.max(1, componentDups.length);
84
+ const name = determineClusterName(component);
85
+ const { severity, reason, suggestion } = calculateSeverity(
86
+ component[0],
87
+ component[1],
88
+ "",
89
+ // Code not available here
90
+ avgSimilarity,
91
+ 30
92
+ // Assume substantial if clustered
93
+ );
94
+ clusters.push({
95
+ id: `cluster-${clusters.length}`,
96
+ name,
97
+ files: component,
98
+ severity,
99
+ duplicateCount: componentDups.length,
100
+ totalTokenCost,
101
+ averageSimilarity: avgSimilarity,
102
+ reason,
103
+ suggestion
104
+ });
105
+ }
106
+ return clusters;
107
+ }
108
+ function determineClusterName(files) {
109
+ if (files.length === 0) return "Unknown Cluster";
110
+ if (files.some((f) => f.includes("blog"))) return "Blog SEO Boilerplate";
111
+ if (files.some((f) => f.includes("buttons")))
112
+ return "Button Component Variants";
113
+ if (files.some((f) => f.includes("cards"))) return "Card Component Variants";
114
+ if (files.some((f) => f.includes("login.test"))) return "E2E Test Patterns";
115
+ const first = files[0];
116
+ const dirName = path.dirname(first).split(path.sep).pop();
117
+ if (dirName && dirName !== "." && dirName !== "..") {
118
+ return `${dirName.charAt(0).toUpperCase() + dirName.slice(1)} Domain Group`;
119
+ }
120
+ return "Shared Pattern Group";
121
+ }
122
+ function filterClustersByImpact(clusters, minTokenCost = 1e3, minFiles = 3) {
123
+ return clusters.filter(
124
+ (c) => c.totalTokenCost >= minTokenCost && c.files.length >= minFiles
125
+ );
126
+ }
127
+
128
+ // src/analyzer.ts
129
+ import { scanFiles, readFileContent, Severity as Severity2, IssueType } from "@aiready/core";
130
+ function getRefactoringSuggestion(patternType, similarity) {
131
+ const baseMessages = {
132
+ "api-handler": "Extract common middleware or create a base handler class",
133
+ validator: "Consolidate validation logic into shared schema validators (Zod/Yup)",
134
+ utility: "Move to a shared utilities file and reuse across modules",
135
+ "class-method": "Consider inheritance or composition to share behavior",
136
+ component: "Extract shared logic into a custom hook or HOC",
137
+ function: "Extract into a shared helper function",
138
+ unknown: "Extract common logic into a reusable module"
139
+ };
140
+ const urgency = similarity > 0.95 ? " (CRITICAL: Nearly identical code)" : similarity > 0.9 ? " (HIGH: Very similar, refactor soon)" : "";
141
+ return baseMessages[patternType] + urgency;
142
+ }
143
+ async function getSmartDefaults(directory, userOptions) {
144
+ if (userOptions.useSmartDefaults === false) {
145
+ return {
146
+ rootDir: directory,
147
+ minSimilarity: 0.6,
148
+ minLines: 8,
149
+ batchSize: 100,
150
+ approx: true,
151
+ minSharedTokens: 12,
152
+ maxCandidatesPerBlock: 5,
153
+ streamResults: false,
154
+ severity: "all",
155
+ includeTests: false
156
+ };
157
+ }
158
+ const scanOptions = {
159
+ rootDir: directory,
160
+ include: userOptions.include || ["**/*.{ts,tsx,js,jsx,py,java}"],
161
+ exclude: userOptions.exclude
162
+ };
163
+ const files = await scanFiles(scanOptions);
164
+ const fileCount = files.length;
165
+ const estimatedBlocks = fileCount * 5;
166
+ const minLines = Math.max(
167
+ 6,
168
+ Math.min(20, 6 + Math.floor(estimatedBlocks / 1e3) * 2)
169
+ );
170
+ const minSimilarity = Math.min(0.75, 0.45 + estimatedBlocks / 1e4 * 0.3);
171
+ const batchSize = estimatedBlocks > 1e3 ? 200 : 100;
172
+ const severity = estimatedBlocks > 3e3 ? "high" : "all";
173
+ const maxCandidatesPerBlock = Math.max(
174
+ 5,
175
+ Math.min(100, Math.floor(1e6 / estimatedBlocks))
176
+ );
177
+ const defaults = {
178
+ rootDir: directory,
179
+ minSimilarity,
180
+ minLines,
181
+ batchSize,
182
+ approx: true,
183
+ minSharedTokens: 10,
184
+ maxCandidatesPerBlock,
185
+ streamResults: false,
186
+ severity,
187
+ includeTests: false
188
+ };
189
+ const result = { ...defaults };
190
+ for (const key of Object.keys(defaults)) {
191
+ if (key in userOptions && userOptions[key] !== void 0) {
192
+ result[key] = userOptions[key];
193
+ }
194
+ }
195
+ return result;
196
+ }
197
+ function logConfiguration(config, estimatedBlocks) {
198
+ if (config.suppressToolConfig) return;
199
+ console.log("\u{1F4CB} Configuration:");
200
+ console.log(` Repository size: ~${estimatedBlocks} code blocks`);
201
+ console.log(` Similarity threshold: ${config.minSimilarity}`);
202
+ console.log(` Minimum lines: ${config.minLines}`);
203
+ console.log(` Approximate mode: ${config.approx ? "enabled" : "disabled"}`);
204
+ console.log(` Max candidates per block: ${config.maxCandidatesPerBlock}`);
205
+ console.log(` Min shared tokens: ${config.minSharedTokens}`);
206
+ console.log(` Severity filter: ${config.severity}`);
207
+ console.log(` Include tests: ${config.includeTests}`);
208
+ if (config.excludePatterns && config.excludePatterns.length > 0) {
209
+ console.log(` Exclude patterns: ${config.excludePatterns.length} active`);
210
+ }
211
+ if (config.confidenceThreshold && config.confidenceThreshold > 0) {
212
+ console.log(` Confidence threshold: ${config.confidenceThreshold}`);
213
+ }
214
+ if (config.ignoreWhitelist && config.ignoreWhitelist.length > 0) {
215
+ console.log(
216
+ ` Ignore whitelist: ${config.ignoreWhitelist.length} entries`
217
+ );
218
+ }
219
+ console.log("");
220
+ }
221
+ async function analyzePatterns(options) {
222
+ const smartDefaults = await getSmartDefaults(options.rootDir || ".", options);
223
+ const finalOptions = { ...smartDefaults, ...options };
224
+ const {
225
+ minSimilarity = 0.4,
226
+ minLines = 5,
227
+ batchSize = 100,
228
+ approx = true,
229
+ minSharedTokens = 8,
230
+ maxCandidatesPerBlock = 100,
231
+ streamResults = false,
232
+ severity = "all",
233
+ groupByFilePair = true,
234
+ createClusters = true,
235
+ minClusterTokenCost = 1e3,
236
+ minClusterFiles = 3,
237
+ excludePatterns = [],
238
+ confidenceThreshold = 0,
239
+ ignoreWhitelist = [],
240
+ ...scanOptions
241
+ } = finalOptions;
242
+ const files = await scanFiles(scanOptions);
243
+ const estimatedBlocks = files.length * 3;
244
+ logConfiguration(finalOptions, estimatedBlocks);
245
+ const results = [];
246
+ const READ_BATCH_SIZE = 50;
247
+ const fileContents = [];
248
+ for (let i = 0; i < files.length; i += READ_BATCH_SIZE) {
249
+ const batch = files.slice(i, i + READ_BATCH_SIZE);
250
+ const batchContents = await Promise.all(
251
+ batch.map(async (file) => ({
252
+ file,
253
+ content: await readFileContent(file)
254
+ }))
255
+ );
256
+ fileContents.push(...batchContents);
257
+ }
258
+ const duplicates = await detectDuplicatePatterns(fileContents, {
259
+ minSimilarity,
260
+ minLines,
261
+ batchSize,
262
+ approx,
263
+ minSharedTokens,
264
+ maxCandidatesPerBlock,
265
+ streamResults,
266
+ excludePatterns,
267
+ confidenceThreshold,
268
+ ignoreWhitelist,
269
+ onProgress: options.onProgress
270
+ });
271
+ for (const file of files) {
272
+ const fileDuplicates = duplicates.filter(
273
+ (dup) => dup.file1 === file || dup.file2 === file
274
+ );
275
+ const issues = fileDuplicates.map((dup) => {
276
+ const otherFile = dup.file1 === file ? dup.file2 : dup.file1;
277
+ const severity2 = dup.similarity > 0.95 ? Severity2.Critical : dup.similarity > 0.9 ? Severity2.Major : Severity2.Minor;
278
+ return {
279
+ type: IssueType.DuplicatePattern,
280
+ severity: severity2,
281
+ message: `${dup.patternType} pattern ${Math.round(dup.similarity * 100)}% similar to ${otherFile} (${dup.tokenCost} tokens wasted)`,
282
+ location: {
283
+ file,
284
+ line: dup.file1 === file ? dup.line1 : dup.line2
285
+ },
286
+ suggestion: getRefactoringSuggestion(dup.patternType, dup.similarity)
287
+ };
288
+ });
289
+ let filteredIssues = issues;
290
+ if (severity !== "all") {
291
+ const severityMap = {
292
+ critical: [Severity2.Critical],
293
+ high: [Severity2.Critical, Severity2.Major],
294
+ medium: [Severity2.Critical, Severity2.Major, Severity2.Minor]
295
+ };
296
+ const allowedSeverities = severityMap[severity] || [Severity2.Critical, Severity2.Major, Severity2.Minor];
297
+ filteredIssues = issues.filter(
298
+ (issue) => allowedSeverities.includes(issue.severity)
299
+ );
300
+ }
301
+ const totalTokenCost = fileDuplicates.reduce(
302
+ (sum, dup) => sum + dup.tokenCost,
303
+ 0
304
+ );
305
+ results.push({
306
+ fileName: file,
307
+ issues: filteredIssues,
308
+ metrics: {
309
+ tokenCost: totalTokenCost,
310
+ consistencyScore: Math.max(0, 1 - fileDuplicates.length * 0.1)
311
+ }
312
+ });
313
+ }
314
+ let groups;
315
+ let clusters;
316
+ if (groupByFilePair) {
317
+ groups = groupDuplicatesByFilePair(duplicates);
318
+ }
319
+ if (createClusters) {
320
+ const allClusters = createRefactorClusters(duplicates);
321
+ clusters = filterClustersByImpact(
322
+ allClusters,
323
+ minClusterTokenCost,
324
+ minClusterFiles
325
+ );
326
+ }
327
+ return { results, duplicates, files, groups, clusters, config: finalOptions };
328
+ }
329
+ function generateSummary(results) {
330
+ const allIssues = results.flatMap((r) => r.issues || []);
331
+ const totalTokenCost = results.reduce(
332
+ (sum, r) => sum + (r.metrics?.tokenCost || 0),
333
+ 0
334
+ );
335
+ const patternsByType = {
336
+ "api-handler": 0,
337
+ validator: 0,
338
+ utility: 0,
339
+ "class-method": 0,
340
+ component: 0,
341
+ function: 0,
342
+ unknown: 0
343
+ };
344
+ allIssues.forEach((issue) => {
345
+ const match = issue.message.match(/^(\S+(?:-\S+)*) pattern/);
346
+ if (match) {
347
+ const type = match[1];
348
+ patternsByType[type] = (patternsByType[type] || 0) + 1;
349
+ }
350
+ });
351
+ const topDuplicates = allIssues.slice(0, 10).map((issue) => {
352
+ const similarityMatch = issue.message.match(/(\d+)% similar/);
353
+ const tokenMatch = issue.message.match(/\((\d+) tokens/);
354
+ const typeMatch = issue.message.match(/^(\S+(?:-\S+)*) pattern/);
355
+ const fileMatch = issue.message.match(/similar to (.+?) \(/);
356
+ return {
357
+ files: [
358
+ {
359
+ path: issue.location.file,
360
+ startLine: issue.location.line,
361
+ endLine: 0
362
+ },
363
+ {
364
+ path: fileMatch?.[1] || "unknown",
365
+ startLine: 0,
366
+ endLine: 0
367
+ }
368
+ ],
369
+ similarity: similarityMatch ? parseInt(similarityMatch[1]) / 100 : 0,
370
+ confidence: similarityMatch ? parseInt(similarityMatch[1]) / 100 : 0,
371
+ // Fallback for summary
372
+ patternType: typeMatch?.[1] || "unknown",
373
+ tokenCost: tokenMatch ? parseInt(tokenMatch[1]) : 0
374
+ };
375
+ });
376
+ return {
377
+ totalPatterns: allIssues.length,
378
+ totalTokenCost,
379
+ patternsByType,
380
+ topDuplicates
381
+ };
382
+ }
383
+
384
+ export {
385
+ groupDuplicatesByFilePair,
386
+ createRefactorClusters,
387
+ filterClustersByImpact,
388
+ getSmartDefaults,
389
+ analyzePatterns,
390
+ generateSummary
391
+ };
package/dist/cli.js CHANGED
@@ -26,12 +26,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  // src/cli.ts
27
27
  var import_commander = require("commander");
28
28
 
29
- // src/index.ts
30
- var import_core7 = require("@aiready/core");
31
-
32
- // src/provider.ts
33
- var import_core6 = require("@aiready/core");
34
-
35
29
  // src/analyzer.ts
36
30
  var import_core4 = require("@aiready/core");
37
31
 
@@ -468,7 +462,7 @@ async function getSmartDefaults(directory, userOptions) {
468
462
  6,
469
463
  Math.min(20, 6 + Math.floor(estimatedBlocks / 1e3) * 2)
470
464
  );
471
- const minSimilarity = Math.min(0.85, 0.5 + estimatedBlocks / 5e3 * 0.3);
465
+ const minSimilarity = Math.min(0.75, 0.45 + estimatedBlocks / 1e4 * 0.3);
472
466
  const batchSize = estimatedBlocks > 1e3 ? 200 : 100;
473
467
  const severity = estimatedBlocks > 3e3 ? "high" : "all";
474
468
  const maxCandidatesPerBlock = Math.max(
@@ -628,9 +622,9 @@ async function analyzePatterns(options) {
628
622
  return { results, duplicates, files, groups, clusters, config: finalOptions };
629
623
  }
630
624
  function generateSummary(results) {
631
- const allIssues = results.flatMap((r) => r.issues);
625
+ const allIssues = results.flatMap((r) => r.issues || []);
632
626
  const totalTokenCost = results.reduce(
633
- (sum, r) => sum + (r.metrics.tokenCost || 0),
627
+ (sum, r) => sum + (r.metrics?.tokenCost || 0),
634
628
  0
635
629
  );
636
630
  const patternsByType = {
@@ -682,163 +676,14 @@ function generateSummary(results) {
682
676
  };
683
677
  }
684
678
 
685
- // src/scoring.ts
686
- var import_core5 = require("@aiready/core");
687
- function calculatePatternScore(duplicates, totalFilesAnalyzed, costConfig) {
688
- const totalDuplicates = duplicates.length;
689
- const totalTokenCost = duplicates.reduce((sum, d) => sum + d.tokenCost, 0);
690
- const highImpactDuplicates = duplicates.filter(
691
- (d) => d.tokenCost > 1e3 || d.similarity > 0.7
692
- ).length;
693
- if (totalFilesAnalyzed === 0) {
694
- return {
695
- toolName: import_core5.ToolName.PatternDetect,
696
- score: 100,
697
- rawMetrics: {
698
- totalDuplicates: 0,
699
- totalTokenCost: 0,
700
- highImpactDuplicates: 0,
701
- totalFilesAnalyzed: 0
702
- },
703
- factors: [],
704
- recommendations: []
705
- };
706
- }
707
- const duplicatesPerFile = totalDuplicates / totalFilesAnalyzed * 100;
708
- const tokenWastePerFile = totalTokenCost / totalFilesAnalyzed;
709
- const duplicatesPenalty = Math.min(60, duplicatesPerFile * 0.6);
710
- const tokenPenalty = Math.min(40, tokenWastePerFile / 125);
711
- const highImpactPenalty = highImpactDuplicates > 0 ? Math.min(15, highImpactDuplicates * 2 - 5) : -5;
712
- const score = 100 - duplicatesPenalty - tokenPenalty - highImpactPenalty;
713
- const finalScore = Math.max(0, Math.min(100, Math.round(score)));
714
- const factors = [
715
- {
716
- name: "Duplication Density",
717
- impact: -Math.round(duplicatesPenalty),
718
- description: `${duplicatesPerFile.toFixed(1)} duplicates per 100 files`
719
- },
720
- {
721
- name: "Token Waste",
722
- impact: -Math.round(tokenPenalty),
723
- description: `${Math.round(tokenWastePerFile)} tokens wasted per file`
724
- }
725
- ];
726
- if (highImpactDuplicates > 0) {
727
- factors.push({
728
- name: "High-Impact Patterns",
729
- impact: -Math.round(highImpactPenalty),
730
- description: `${highImpactDuplicates} high-impact duplicates (>1000 tokens or >70% similar)`
731
- });
732
- } else {
733
- factors.push({
734
- name: "No High-Impact Patterns",
735
- impact: 5,
736
- description: "No severe duplicates detected"
737
- });
738
- }
739
- const recommendations = [];
740
- if (highImpactDuplicates > 0) {
741
- const estimatedImpact = Math.min(15, highImpactDuplicates * 3);
742
- recommendations.push({
743
- action: `Deduplicate ${highImpactDuplicates} high-impact pattern${highImpactDuplicates > 1 ? "s" : ""}`,
744
- estimatedImpact,
745
- priority: "high"
746
- });
747
- }
748
- if (totalDuplicates > 10 && duplicatesPerFile > 20) {
749
- const estimatedImpact = Math.min(10, Math.round(duplicatesPenalty * 0.3));
750
- recommendations.push({
751
- action: "Extract common patterns into shared utilities",
752
- estimatedImpact,
753
- priority: "medium"
754
- });
755
- }
756
- if (tokenWastePerFile > 2e3) {
757
- const estimatedImpact = Math.min(8, Math.round(tokenPenalty * 0.4));
758
- recommendations.push({
759
- action: "Consolidate duplicated logic to reduce AI context waste",
760
- estimatedImpact,
761
- priority: totalTokenCost > 1e4 ? "high" : "medium"
762
- });
763
- }
764
- const cfg = { ...import_core5.DEFAULT_COST_CONFIG, ...costConfig };
765
- const estimatedMonthlyCost = (0, import_core5.calculateMonthlyCost)(totalTokenCost, cfg);
766
- const issues = duplicates.map((d) => ({
767
- severity: d.severity === "critical" ? "critical" : d.severity === "major" ? "major" : "minor"
768
- }));
769
- const productivityImpact = (0, import_core5.calculateProductivityImpact)(issues);
770
- return {
771
- toolName: "pattern-detect",
772
- score: finalScore,
773
- rawMetrics: {
774
- totalDuplicates,
775
- totalTokenCost,
776
- highImpactDuplicates,
777
- totalFilesAnalyzed,
778
- duplicatesPerFile: Math.round(duplicatesPerFile * 10) / 10,
779
- tokenWastePerFile: Math.round(tokenWastePerFile),
780
- // Business value metrics
781
- estimatedMonthlyCost,
782
- estimatedDeveloperHours: productivityImpact.totalHours
783
- },
784
- factors,
785
- recommendations
786
- };
787
- }
788
-
789
- // src/provider.ts
790
- var PatternDetectProvider = {
791
- id: import_core6.ToolName.PatternDetect,
792
- alias: ["patterns", "duplicates", "duplication"],
793
- async analyze(options) {
794
- const results = await analyzePatterns(options);
795
- return import_core6.SpokeOutputSchema.parse({
796
- results: results.results,
797
- summary: {
798
- totalFiles: results.files.length,
799
- totalIssues: results.results.reduce(
800
- (sum, r) => sum + r.issues.length,
801
- 0
802
- ),
803
- duplicates: results.duplicates,
804
- // Keep the raw duplicates for score calculation
805
- clusters: results.clusters,
806
- config: Object.fromEntries(
807
- Object.entries(results.config).filter(
808
- ([key]) => !import_core6.GLOBAL_SCAN_OPTIONS.includes(key) || key === "rootDir"
809
- )
810
- )
811
- },
812
- metadata: {
813
- toolName: import_core6.ToolName.PatternDetect,
814
- version: "0.12.5",
815
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
816
- }
817
- });
818
- },
819
- score(output, options) {
820
- const duplicates = output.summary.duplicates || [];
821
- const totalFiles = output.summary.totalFiles || output.results.length;
822
- return calculatePatternScore(
823
- duplicates,
824
- totalFiles,
825
- options.costConfig
826
- );
827
- },
828
- defaultWeight: 22
829
- };
830
-
831
- // src/index.ts
832
- import_core7.ToolRegistry.register(PatternDetectProvider);
833
-
834
679
  // src/cli-action.ts
835
680
  var import_chalk2 = __toESM(require("chalk"));
836
681
  var import_fs = require("fs");
837
682
  var import_path2 = require("path");
838
- var import_core11 = require("@aiready/core");
683
+ var import_core8 = require("@aiready/core");
839
684
 
840
685
  // src/config-resolver.ts
841
- var import_core8 = require("@aiready/core");
686
+ var import_core5 = require("@aiready/core");
842
687
 
843
688
  // src/constants.ts
844
689
  var DEFAULT_MIN_SIMILARITY = 0.4;
@@ -870,7 +715,7 @@ EXAMPLES:
870
715
 
871
716
  // src/config-resolver.ts
872
717
  async function resolvePatternConfig(directory, options) {
873
- const fileConfig = await (0, import_core8.loadConfig)(directory);
718
+ const fileConfig = await (0, import_core5.loadConfig)(directory);
874
719
  const defaults = {
875
720
  minSimilarity: DEFAULT_MIN_SIMILARITY,
876
721
  minLines: DEFAULT_MIN_LINES,
@@ -884,7 +729,7 @@ async function resolvePatternConfig(directory, options) {
884
729
  excludePatterns: void 0,
885
730
  confidenceThreshold: 0,
886
731
  ignoreWhitelist: void 0,
887
- minSeverity: import_core8.Severity.Minor,
732
+ minSeverity: import_core5.Severity.Minor,
888
733
  excludeTestFixtures: false,
889
734
  excludeTemplates: false,
890
735
  includeTests: false,
@@ -895,7 +740,7 @@ async function resolvePatternConfig(directory, options) {
895
740
  minClusterFiles: DEFAULT_MIN_CLUSTER_FILES,
896
741
  showRawDuplicates: false
897
742
  };
898
- const mergedConfig = (0, import_core8.mergeConfigWithDefaults)(fileConfig, defaults);
743
+ const mergedConfig = (0, import_core5.mergeConfigWithDefaults)(fileConfig, defaults);
899
744
  const finalOptions = {
900
745
  rootDir: directory,
901
746
  minSimilarity: options.similarity ? parseFloat(options.similarity) : mergedConfig.minSimilarity,
@@ -937,7 +782,7 @@ async function resolvePatternConfig(directory, options) {
937
782
  }
938
783
 
939
784
  // src/cli-output.ts
940
- var import_core9 = require("@aiready/core");
785
+ var import_core6 = require("@aiready/core");
941
786
  function getPatternIcon(type) {
942
787
  const icons = {
943
788
  "api-handler": "\u{1F50C}",
@@ -963,7 +808,7 @@ function generateHTMLReport(results, summary) {
963
808
  dup.files.map((f) => `<code>${f.path}:${f.startLine}-${f.endLine}</code>`).join("<br>\u2194<br>"),
964
809
  dup.tokenCost.toLocaleString()
965
810
  ]);
966
- return (0, import_core9.generateStandardHtmlReport)(
811
+ return (0, import_core6.generateStandardHtmlReport)(
967
812
  {
968
813
  title: "Pattern Detection Report",
969
814
  packageName: "pattern-detect",
@@ -980,7 +825,7 @@ function generateHTMLReport(results, summary) {
980
825
  [
981
826
  {
982
827
  title: "Duplicate Patterns",
983
- content: (0, import_core9.generateTable)({
828
+ content: (0, import_core6.generateTable)({
984
829
  headers: ["Similarity", "Type", "Locations", "Tokens Wasted"],
985
830
  rows: tableRows
986
831
  })
@@ -992,9 +837,9 @@ function generateHTMLReport(results, summary) {
992
837
 
993
838
  // src/terminal-output.ts
994
839
  var import_chalk = __toESM(require("chalk"));
995
- var import_core10 = require("@aiready/core");
840
+ var import_core7 = require("@aiready/core");
996
841
  function printAnalysisSummary(resultsLength, totalIssues, totalTokenCost, elapsedTime) {
997
- (0, import_core10.printTerminalHeader)("PATTERN ANALYSIS SUMMARY");
842
+ (0, import_core7.printTerminalHeader)("PATTERN ANALYSIS SUMMARY");
998
843
  console.log(import_chalk.default.white(`\u{1F4C1} Files analyzed: ${import_chalk.default.bold(resultsLength)}`));
999
844
  console.log(
1000
845
  import_chalk.default.yellow(
@@ -1011,9 +856,9 @@ function printAnalysisSummary(resultsLength, totalIssues, totalTokenCost, elapse
1011
856
  function printPatternBreakdown(patternsByType) {
1012
857
  const sortedTypes = Object.entries(patternsByType).filter(([, count]) => count > 0).sort(([, a], [, b]) => b - a);
1013
858
  if (sortedTypes.length > 0) {
1014
- console.log("\n" + (0, import_core10.getTerminalDivider)());
859
+ console.log("\n" + (0, import_core7.getTerminalDivider)());
1015
860
  console.log(import_chalk.default.bold.white(" PATTERNS BY TYPE"));
1016
- console.log((0, import_core10.getTerminalDivider)() + "\n");
861
+ console.log((0, import_core7.getTerminalDivider)() + "\n");
1017
862
  sortedTypes.forEach(([type, count]) => {
1018
863
  const icon = getPatternIcon(type);
1019
864
  console.log(
@@ -1024,20 +869,20 @@ function printPatternBreakdown(patternsByType) {
1024
869
  }
1025
870
  function printDuplicateGroups(groups, maxResults) {
1026
871
  if (groups.length === 0) return;
1027
- console.log("\n" + (0, import_core10.getTerminalDivider)());
872
+ console.log("\n" + (0, import_core7.getTerminalDivider)());
1028
873
  console.log(
1029
874
  import_chalk.default.bold.white(` \u{1F4E6} DUPLICATE GROUPS (${groups.length} file pairs)`)
1030
875
  );
1031
- console.log((0, import_core10.getTerminalDivider)() + "\n");
876
+ console.log((0, import_core7.getTerminalDivider)() + "\n");
1032
877
  const topGroups = groups.sort((a, b) => {
1033
- const bVal = (0, import_core10.getSeverityValue)(b.severity);
1034
- const aVal = (0, import_core10.getSeverityValue)(a.severity);
878
+ const bVal = (0, import_core7.getSeverityValue)(b.severity);
879
+ const aVal = (0, import_core7.getSeverityValue)(a.severity);
1035
880
  const severityDiff = bVal - aVal;
1036
881
  if (severityDiff !== 0) return severityDiff;
1037
882
  return b.totalTokenCost - a.totalTokenCost;
1038
883
  }).slice(0, maxResults);
1039
884
  topGroups.forEach((group, idx) => {
1040
- const severityBadge = (0, import_core10.getSeverityBadge)(group.severity);
885
+ const severityBadge = (0, import_core7.getSeverityBadge)(group.severity);
1041
886
  const [file1, file2] = group.filePair.split("::");
1042
887
  const file1Name = file1.split("/").pop() || file1;
1043
888
  const file2Name = file2.split("/").pop() || file2;
@@ -1070,13 +915,13 @@ function printDuplicateGroups(groups, maxResults) {
1070
915
  }
1071
916
  function printRefactorClusters(clusters) {
1072
917
  if (clusters.length === 0) return;
1073
- console.log("\n" + (0, import_core10.getTerminalDivider)());
918
+ console.log("\n" + (0, import_core7.getTerminalDivider)());
1074
919
  console.log(
1075
920
  import_chalk.default.bold.white(` \u{1F3AF} REFACTOR CLUSTERS (${clusters.length} patterns)`)
1076
921
  );
1077
- console.log((0, import_core10.getTerminalDivider)() + "\n");
922
+ console.log((0, import_core7.getTerminalDivider)() + "\n");
1078
923
  clusters.sort((a, b) => b.totalTokenCost - a.totalTokenCost).forEach((cluster, idx) => {
1079
- const severityBadge = (0, import_core10.getSeverityBadge)(cluster.severity);
924
+ const severityBadge = (0, import_core7.getSeverityBadge)(cluster.severity);
1080
925
  console.log(`${idx + 1}. ${severityBadge} ${import_chalk.default.bold(cluster.name)}`);
1081
926
  console.log(
1082
927
  ` Total tokens: ${import_chalk.default.bold(cluster.totalTokenCost.toLocaleString())} | Avg similarity: ${import_chalk.default.bold(Math.round(cluster.averageSimilarity * 100) + "%")} | Duplicates: ${import_chalk.default.bold(cluster.duplicateCount)}`
@@ -1103,18 +948,18 @@ function printRefactorClusters(clusters) {
1103
948
  }
1104
949
  function printRawDuplicates(duplicates, maxResults) {
1105
950
  if (duplicates.length === 0) return;
1106
- console.log("\n" + (0, import_core10.getTerminalDivider)());
951
+ console.log("\n" + (0, import_core7.getTerminalDivider)());
1107
952
  console.log(import_chalk.default.bold.white(" TOP DUPLICATE PATTERNS"));
1108
- console.log((0, import_core10.getTerminalDivider)() + "\n");
953
+ console.log((0, import_core7.getTerminalDivider)() + "\n");
1109
954
  const topDuplicates = duplicates.sort((a, b) => {
1110
- const bVal = (0, import_core10.getSeverityValue)(b.severity);
1111
- const aVal = (0, import_core10.getSeverityValue)(a.severity);
955
+ const bVal = (0, import_core7.getSeverityValue)(b.severity);
956
+ const aVal = (0, import_core7.getSeverityValue)(a.severity);
1112
957
  const severityDiff = bVal - aVal;
1113
958
  if (severityDiff !== 0) return severityDiff;
1114
959
  return b.similarity - a.similarity;
1115
960
  }).slice(0, maxResults);
1116
961
  topDuplicates.forEach((dup) => {
1117
- const severityBadge = (0, import_core10.getSeverityBadge)(dup.severity);
962
+ const severityBadge = (0, import_core7.getSeverityBadge)(dup.severity);
1118
963
  const file1Name = dup.file1.split("/").pop() || dup.file1;
1119
964
  const file2Name = dup.file2.split("/").pop() || dup.file2;
1120
965
  console.log(
@@ -1147,9 +992,9 @@ function printRawDuplicates(duplicates, maxResults) {
1147
992
  }
1148
993
  function printCriticalIssues(issues) {
1149
994
  if (issues.length === 0) return;
1150
- console.log((0, import_core10.getTerminalDivider)());
995
+ console.log((0, import_core7.getTerminalDivider)());
1151
996
  console.log(import_chalk.default.bold.white(" CRITICAL ISSUES (>95% similar)"));
1152
- console.log((0, import_core10.getTerminalDivider)() + "\n");
997
+ console.log((0, import_core7.getTerminalDivider)() + "\n");
1153
998
  issues.slice(0, 5).forEach((issue) => {
1154
999
  console.log(
1155
1000
  import_chalk.default.red("\u25CF ") + import_chalk.default.white(`${issue.file}:${issue.location.line}`)
@@ -1189,8 +1034,11 @@ async function patternActionHandler(directory, options) {
1189
1034
  );
1190
1035
  }
1191
1036
  const elapsedTime = ((Date.now() - startTime) / 1e3).toFixed(2);
1192
- const summary = generateSummary(results);
1193
- const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0);
1037
+ const summary = generateSummary(results || []);
1038
+ const totalIssues = (results || []).reduce(
1039
+ (sum, r) => sum + (r.issues?.length || 0),
1040
+ 0
1041
+ );
1194
1042
  if (options.output === "json") {
1195
1043
  handleJsonOutput(options.outputFile, directory, {
1196
1044
  summary,
@@ -1218,7 +1066,7 @@ async function patternActionHandler(directory, options) {
1218
1066
  );
1219
1067
  }
1220
1068
  function handleJsonOutput(outputFile, directory, data) {
1221
- const outputPath = (0, import_core11.resolveOutputPath)(
1069
+ const outputPath = (0, import_core8.resolveOutputPath)(
1222
1070
  outputFile,
1223
1071
  `pattern-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
1224
1072
  directory
@@ -1231,7 +1079,7 @@ function handleJsonOutput(outputFile, directory, data) {
1231
1079
  }
1232
1080
  function handleHtmlOutput(outputFile, directory, summary, results) {
1233
1081
  const html = generateHTMLReport(summary, results);
1234
- const outputPath = (0, import_core11.resolveOutputPath)(
1082
+ const outputPath = (0, import_core8.resolveOutputPath)(
1235
1083
  outputFile,
1236
1084
  `pattern-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.html`,
1237
1085
  directory
@@ -1266,7 +1114,7 @@ function renderTerminalOutput(fileCount, totalIssues, summary, elapsedTime, opti
1266
1114
  } else if (totalIssues < 5) {
1267
1115
  printGuidance();
1268
1116
  }
1269
- console.log((0, import_core11.getTerminalDivider)());
1117
+ console.log((0, import_core8.getTerminalDivider)());
1270
1118
  if (totalIssues > 0) {
1271
1119
  console.log(
1272
1120
  import_chalk2.default.white(
@@ -1278,7 +1126,7 @@ function renderTerminalOutput(fileCount, totalIssues, summary, elapsedTime, opti
1278
1126
  printFooter();
1279
1127
  }
1280
1128
  function resultsToCriticalIssues(summary, duplicates) {
1281
- return duplicates.filter((d) => (0, import_core11.getSeverityValue)(d.severity) === 4).map((d) => ({
1129
+ return duplicates.filter((d) => (0, import_core8.getSeverityValue)(d.severity) === 4).map((d) => ({
1282
1130
  file: d.file1,
1283
1131
  location: { line: d.line1 },
1284
1132
  message: `${d.patternType} pattern highly similar to ${d.file2}`,
package/dist/cli.mjs CHANGED
@@ -1,14 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-J5CW6NYY.mjs";
3
2
  import {
4
3
  analyzePatterns,
5
4
  generateSummary
6
- } from "./chunk-SUUZMLPS.mjs";
5
+ } from "./chunk-K7BO57OO.mjs";
7
6
  import "./chunk-NQBYYWHJ.mjs";
8
7
  import {
9
8
  filterBySeverity
10
9
  } from "./chunk-J2G742QF.mjs";
11
- import "./chunk-WBBO35SC.mjs";
12
10
 
13
11
  // src/cli.ts
14
12
  import { Command } from "commander";
@@ -385,8 +383,11 @@ async function patternActionHandler(directory, options) {
385
383
  );
386
384
  }
387
385
  const elapsedTime = ((Date.now() - startTime) / 1e3).toFixed(2);
388
- const summary = generateSummary(results);
389
- const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0);
386
+ const summary = generateSummary(results || []);
387
+ const totalIssues = (results || []).reduce(
388
+ (sum, r) => sum + (r.issues?.length || 0),
389
+ 0
390
+ );
390
391
  if (options.output === "json") {
391
392
  handleJsonOutput(options.outputFile, directory, {
392
393
  summary,
package/dist/index.js CHANGED
@@ -498,7 +498,7 @@ async function getSmartDefaults(directory, userOptions) {
498
498
  6,
499
499
  Math.min(20, 6 + Math.floor(estimatedBlocks / 1e3) * 2)
500
500
  );
501
- const minSimilarity = Math.min(0.85, 0.5 + estimatedBlocks / 5e3 * 0.3);
501
+ const minSimilarity = Math.min(0.75, 0.45 + estimatedBlocks / 1e4 * 0.3);
502
502
  const batchSize = estimatedBlocks > 1e3 ? 200 : 100;
503
503
  const severity = estimatedBlocks > 3e3 ? "high" : "all";
504
504
  const maxCandidatesPerBlock = Math.max(
@@ -658,9 +658,9 @@ async function analyzePatterns(options) {
658
658
  return { results, duplicates, files, groups, clusters, config: finalOptions };
659
659
  }
660
660
  function generateSummary(results) {
661
- const allIssues = results.flatMap((r) => r.issues);
661
+ const allIssues = results.flatMap((r) => r.issues || []);
662
662
  const totalTokenCost = results.reduce(
663
- (sum, r) => sum + (r.metrics.tokenCost || 0),
663
+ (sum, r) => sum + (r.metrics?.tokenCost || 0),
664
664
  0
665
665
  );
666
666
  const patternsByType = {
package/dist/index.mjs CHANGED
@@ -1,7 +1,3 @@
1
- import {
2
- PatternDetectProvider,
3
- Severity
4
- } from "./chunk-J5CW6NYY.mjs";
5
1
  import {
6
2
  analyzePatterns,
7
3
  createRefactorClusters,
@@ -9,7 +5,7 @@ import {
9
5
  generateSummary,
10
6
  getSmartDefaults,
11
7
  groupDuplicatesByFilePair
12
- } from "./chunk-SUUZMLPS.mjs";
8
+ } from "./chunk-K7BO57OO.mjs";
13
9
  import {
14
10
  detectDuplicatePatterns
15
11
  } from "./chunk-NQBYYWHJ.mjs";
@@ -24,6 +20,59 @@ import {
24
20
  import {
25
21
  calculatePatternScore
26
22
  } from "./chunk-WBBO35SC.mjs";
23
+
24
+ // src/index.ts
25
+ import { ToolRegistry, Severity } from "@aiready/core";
26
+
27
+ // src/provider.ts
28
+ import {
29
+ ToolName,
30
+ SpokeOutputSchema,
31
+ GLOBAL_SCAN_OPTIONS
32
+ } from "@aiready/core";
33
+ var PatternDetectProvider = {
34
+ id: ToolName.PatternDetect,
35
+ alias: ["patterns", "duplicates", "duplication"],
36
+ async analyze(options) {
37
+ const results = await analyzePatterns(options);
38
+ return SpokeOutputSchema.parse({
39
+ results: results.results,
40
+ summary: {
41
+ totalFiles: results.files.length,
42
+ totalIssues: results.results.reduce(
43
+ (sum, r) => sum + r.issues.length,
44
+ 0
45
+ ),
46
+ duplicates: results.duplicates,
47
+ // Keep the raw duplicates for score calculation
48
+ clusters: results.clusters,
49
+ config: Object.fromEntries(
50
+ Object.entries(results.config).filter(
51
+ ([key]) => !GLOBAL_SCAN_OPTIONS.includes(key) || key === "rootDir"
52
+ )
53
+ )
54
+ },
55
+ metadata: {
56
+ toolName: ToolName.PatternDetect,
57
+ version: "0.12.5",
58
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
59
+ }
60
+ });
61
+ },
62
+ score(output, options) {
63
+ const duplicates = output.summary.duplicates || [];
64
+ const totalFiles = output.summary.totalFiles || output.results.length;
65
+ return calculatePatternScore(
66
+ duplicates,
67
+ totalFiles,
68
+ options.costConfig
69
+ );
70
+ },
71
+ defaultWeight: 22
72
+ };
73
+
74
+ // src/index.ts
75
+ ToolRegistry.register(PatternDetectProvider);
27
76
  export {
28
77
  CONTEXT_RULES,
29
78
  IssueType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/pattern-detect",
3
- "version": "0.17.1",
3
+ "version": "0.17.3",
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",
@@ -65,7 +65,7 @@
65
65
  "dependencies": {
66
66
  "commander": "^14.0.0",
67
67
  "chalk": "^5.3.0",
68
- "@aiready/core": "0.24.1"
68
+ "@aiready/core": "0.24.3"
69
69
  },
70
70
  "devDependencies": {
71
71
  "tsup": "^8.3.5",