@aiready/pattern-detect 0.17.8 → 0.17.12
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/analyzer-entry/index.d.mts +1 -1
- package/dist/analyzer-entry/index.d.ts +1 -1
- package/dist/analyzer-entry/index.js +370 -135
- package/dist/analyzer-entry/index.mjs +4 -3
- package/dist/chunk-2P7BQHGR.mjs +306 -0
- package/dist/{chunk-VGMM3L3O.mjs → chunk-3EORD7DC.mjs} +1 -1
- package/dist/{chunk-GREN7X5H.mjs → chunk-4PVPQMRT.mjs} +2 -2
- package/dist/{chunk-RS73WLNI.mjs → chunk-6VDL7TAS.mjs} +5 -113
- package/dist/chunk-AQIP4JGM.mjs +283 -0
- package/dist/{chunk-JBUZ6YHE.mjs → chunk-B4NLWKPZ.mjs} +85 -9
- package/dist/chunk-IPBGVPUX.mjs +143 -0
- package/dist/chunk-LUUJOUK5.mjs +283 -0
- package/dist/chunk-P3BOCGVV.mjs +498 -0
- package/dist/{scoring-entry.js → chunk-PHJE6A3J.mjs} +20 -37
- package/dist/chunk-PQXOORR4.mjs +234 -0
- package/dist/{chunk-GLKAGFKX.mjs → chunk-RDR75DVI.mjs} +85 -9
- package/dist/chunk-SXVLRPMF.mjs +143 -0
- package/dist/{chunk-DNZS4ESD.mjs → chunk-SY7RX5YQ.mjs} +85 -9
- package/dist/{context-rules-entry.js → chunk-TIBF7KST.mjs} +81 -78
- package/dist/chunk-WYYSQX5M.mjs +467 -0
- package/dist/{chunk-I6ETJC7L.mjs → chunk-X553BOMI.mjs} +56 -26
- package/dist/{chunk-K7BO57OO.mjs → chunk-Y6OB7K34.mjs} +80 -4
- package/dist/chunk-YLVV6YZ5.mjs +143 -0
- package/dist/chunk-ZUWPFVJV.mjs +115 -0
- package/dist/chunk-ZZMONVPE.mjs +467 -0
- package/dist/cli.js +402 -167
- package/dist/cli.mjs +4 -3
- package/dist/context-rules-entry/index.d.mts +35 -1
- package/dist/context-rules-entry/index.d.ts +35 -1
- package/dist/context-rules-entry/index.js +194 -48
- package/dist/context-rules-entry/index.mjs +1 -1
- package/dist/detector-entry/index.js +192 -46
- package/dist/detector-entry/index.mjs +2 -2
- package/dist/{analyzer-entry-BVz-HnZd.d.mts → index-B-pnXpgn.d.mts} +10 -1
- package/dist/{index-BwuoiCNm.d.ts → index-CWgYOKaK.d.ts} +35 -16
- package/dist/{index-BVz-HnZd.d.mts → index-Dl4BrGIT.d.mts} +35 -16
- package/dist/{analyzer-entry-BwuoiCNm.d.ts → index-DqS2e0kK.d.ts} +10 -1
- package/dist/index.d.mts +5 -6
- package/dist/index.d.ts +5 -6
- package/dist/index.js +467 -214
- package/dist/index.mjs +37 -22
- package/dist/scoring-entry/index.js +7 -3
- package/dist/scoring-entry/index.mjs +1 -1
- package/package.json +2 -2
- package/dist/analyzer-entry.d.mts +0 -100
- package/dist/analyzer-entry.d.ts +0 -100
- package/dist/analyzer-entry.js +0 -693
- package/dist/analyzer-entry.mjs +0 -12
- package/dist/chunk-262N2JB7.mjs +0 -497
- package/dist/chunk-2R7HOR5H.mjs +0 -777
- package/dist/chunk-3D7RVGHM.mjs +0 -64
- package/dist/chunk-3LS3E6MO.mjs +0 -508
- package/dist/chunk-3VRQYFW3.mjs +0 -782
- package/dist/chunk-3WK24ZOX.mjs +0 -860
- package/dist/chunk-3YYN6ZXN.mjs +0 -1038
- package/dist/chunk-4BPRGZRG.mjs +0 -1041
- package/dist/chunk-4UHDGB7U.mjs +0 -920
- package/dist/chunk-5LYDB7DY.mjs +0 -771
- package/dist/chunk-65G3HXLQ.mjs +0 -497
- package/dist/chunk-65UQ5J2J.mjs +0 -64
- package/dist/chunk-6JTVOBJX.mjs +0 -64
- package/dist/chunk-6OEHUI5J.mjs +0 -1045
- package/dist/chunk-6YUGU4P4.mjs +0 -914
- package/dist/chunk-7EJGNGXM.mjs +0 -771
- package/dist/chunk-7O2DUBSN.mjs +0 -1058
- package/dist/chunk-7S4AUL5S.mjs +0 -911
- package/dist/chunk-A76JUWER.mjs +0 -786
- package/dist/chunk-AJZUNNFH.mjs +0 -817
- package/dist/chunk-AXHGYYYZ.mjs +0 -404
- package/dist/chunk-BKRPSTT2.mjs +0 -64
- package/dist/chunk-BUBQ3W6W.mjs +0 -980
- package/dist/chunk-CCHM2VLK.mjs +0 -1051
- package/dist/chunk-CHFK6EBT.mjs +0 -419
- package/dist/chunk-CMT3MWWO.mjs +0 -948
- package/dist/chunk-CMWW24HW.mjs +0 -259
- package/dist/chunk-CTDBJP25.mjs +0 -1043
- package/dist/chunk-DGAKXYIP.mjs +0 -1041
- package/dist/chunk-DQSLTL7J.mjs +0 -788
- package/dist/chunk-DR5W7S3Z.mjs +0 -968
- package/dist/chunk-EFUKPMBE.mjs +0 -950
- package/dist/chunk-EVBFDILL.mjs +0 -927
- package/dist/chunk-EXORBAXR.mjs +0 -887
- package/dist/chunk-EZT3NZGB.mjs +0 -1057
- package/dist/chunk-FWUKMJEQ.mjs +0 -1133
- package/dist/chunk-GSJFORRO.mjs +0 -504
- package/dist/chunk-H4ADJYOG.mjs +0 -925
- package/dist/chunk-H5FB2USZ.mjs +0 -762
- package/dist/chunk-H73HEG7M.mjs +0 -670
- package/dist/chunk-HOS5Z2NC.mjs +0 -669
- package/dist/chunk-HXHQOQB5.mjs +0 -508
- package/dist/chunk-INEOYHUM.mjs +0 -911
- package/dist/chunk-INJ4SBTV.mjs +0 -754
- package/dist/chunk-J5CW6NYY.mjs +0 -64
- package/dist/chunk-JAFZCZAP.mjs +0 -776
- package/dist/chunk-JKVKOXYR.mjs +0 -407
- package/dist/chunk-JTHW7EYW.mjs +0 -1041
- package/dist/chunk-JWR3AHKO.mjs +0 -788
- package/dist/chunk-KC2CQMG2.mjs +0 -858
- package/dist/chunk-KDWGWBP5.mjs +0 -832
- package/dist/chunk-KPEK5REL.mjs +0 -919
- package/dist/chunk-KT6O2IAE.mjs +0 -861
- package/dist/chunk-KWMNN3TG.mjs +0 -391
- package/dist/chunk-LUA5FXSZ.mjs +0 -771
- package/dist/chunk-LYKRYBSM.mjs +0 -64
- package/dist/chunk-M4PQMW34.mjs +0 -480
- package/dist/chunk-MH6LBXZF.mjs +0 -816
- package/dist/chunk-MHU3CL4R.mjs +0 -64
- package/dist/chunk-MJWBS6SM.mjs +0 -1058
- package/dist/chunk-OFGMDX66.mjs +0 -402
- package/dist/chunk-P7B6Z4I2.mjs +0 -1043
- package/dist/chunk-PBCXSG7E.mjs +0 -658
- package/dist/chunk-PEEHSFDR.mjs +0 -1058
- package/dist/chunk-PSVG2NLH.mjs +0 -966
- package/dist/chunk-PWNQ6JZW.mjs +0 -508
- package/dist/chunk-QE4E3F7C.mjs +0 -410
- package/dist/chunk-QEP76HGK.mjs +0 -1039
- package/dist/chunk-QX2BQJEO.mjs +0 -1058
- package/dist/chunk-R2S73CVG.mjs +0 -503
- package/dist/chunk-RMGDSNLE.mjs +0 -770
- package/dist/chunk-S2KQFII2.mjs +0 -491
- package/dist/chunk-SLDK5PQK.mjs +0 -1129
- package/dist/chunk-SNSDVGWW.mjs +0 -783
- package/dist/chunk-SUUZMLPS.mjs +0 -391
- package/dist/chunk-SVCSIZ2A.mjs +0 -259
- package/dist/chunk-T2C6WS73.mjs +0 -670
- package/dist/chunk-TCG2G32F.mjs +0 -911
- package/dist/chunk-TGBZP7SB.mjs +0 -773
- package/dist/chunk-THF4RW63.mjs +0 -254
- package/dist/chunk-TJKDLVLN.mjs +0 -503
- package/dist/chunk-TXWPOVYU.mjs +0 -402
- package/dist/chunk-UB3CGOQ7.mjs +0 -64
- package/dist/chunk-UKIKN27B.mjs +0 -950
- package/dist/chunk-V5DP4FP6.mjs +0 -876
- package/dist/chunk-VRMXVYDZ.mjs +0 -419
- package/dist/chunk-WACZ5LFH.mjs +0 -1055
- package/dist/chunk-WC7CBAA7.mjs +0 -1058
- package/dist/chunk-WKBCNITM.mjs +0 -1072
- package/dist/chunk-WMOGJFME.mjs +0 -391
- package/dist/chunk-X4GR2N2M.mjs +0 -947
- package/dist/chunk-XCWY2DQY.mjs +0 -788
- package/dist/chunk-XJD35DS6.mjs +0 -1058
- package/dist/chunk-XNPID6FU.mjs +0 -391
- package/dist/chunk-XUUVS54V.mjs +0 -776
- package/dist/chunk-YCGV65F5.mjs +0 -508
- package/dist/chunk-YJYDBFT3.mjs +0 -780
- package/dist/chunk-YP3HEDQW.mjs +0 -859
- package/dist/chunk-YSDOUNJJ.mjs +0 -1142
- package/dist/chunk-Z6GBFFOV.mjs +0 -1040
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/context-rules-entry-y2uJSngh.d.mts +0 -60
- package/dist/context-rules-entry-y2uJSngh.d.ts +0 -60
- package/dist/context-rules-entry.d.mts +0 -55
- package/dist/context-rules-entry.d.ts +0 -55
- package/dist/context-rules-entry.mjs +0 -12
- package/dist/context-rules.d.ts +0 -41
- package/dist/context-rules.d.ts.map +0 -1
- package/dist/context-rules.js +0 -225
- package/dist/context-rules.js.map +0 -1
- package/dist/detector-entry.d.mts +0 -14
- package/dist/detector-entry.d.ts +0 -14
- package/dist/detector-entry.js +0 -301
- package/dist/detector-entry.mjs +0 -7
- package/dist/detector.d.ts +0 -40
- package/dist/detector.d.ts.map +0 -1
- package/dist/detector.js +0 -385
- package/dist/detector.js.map +0 -1
- package/dist/extractors/python-extractor.d.ts +0 -19
- package/dist/extractors/python-extractor.d.ts.map +0 -1
- package/dist/extractors/python-extractor.js +0 -164
- package/dist/extractors/python-extractor.js.map +0 -1
- package/dist/grouping.d.ts +0 -54
- package/dist/grouping.d.ts.map +0 -1
- package/dist/grouping.js +0 -347
- package/dist/grouping.js.map +0 -1
- package/dist/index-y2uJSngh.d.mts +0 -60
- package/dist/index-y2uJSngh.d.ts +0 -60
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/python-extractor-BGKGX6BK.mjs +0 -131
- package/dist/python-extractor-ELAKYK2W.mjs +0 -140
- package/dist/scoring-entry.d.mts +0 -23
- package/dist/scoring-entry.d.ts +0 -23
- package/dist/scoring-entry.mjs +0 -6
- package/dist/scoring.d.ts +0 -12
- package/dist/scoring.d.ts.map +0 -1
- package/dist/scoring.js +0 -116
- package/dist/scoring.js.map +0 -1
- package/dist/types-C4lmb2Yh.d.mts +0 -36
- package/dist/types-C4lmb2Yh.d.ts +0 -36
package/dist/index.js
CHANGED
|
@@ -30,38 +30,41 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
PatternDetectProvider: () => PatternDetectProvider,
|
|
36
|
-
Severity: () => import_core7.Severity,
|
|
33
|
+
PATTERN_DETECT_PROVIDER: () => PATTERN_DETECT_PROVIDER,
|
|
34
|
+
Severity: () => import_core11.Severity,
|
|
37
35
|
analyzePatterns: () => analyzePatterns,
|
|
36
|
+
areBrandSpecificVariants: () => areBrandSpecificVariants,
|
|
38
37
|
calculatePatternScore: () => calculatePatternScore,
|
|
39
|
-
calculateSeverity: () =>
|
|
38
|
+
calculateSeverity: () => calculateSeverity2,
|
|
40
39
|
createRefactorClusters: () => createRefactorClusters,
|
|
41
40
|
detectDuplicatePatterns: () => detectDuplicatePatterns,
|
|
42
|
-
|
|
41
|
+
filterBrandSpecificVariants: () => filterBrandSpecificVariants,
|
|
42
|
+
filterBySeverity: () => filterBySeverity2,
|
|
43
43
|
filterClustersByImpact: () => filterClustersByImpact,
|
|
44
44
|
generateSummary: () => generateSummary,
|
|
45
|
-
getSeverityLabel: () =>
|
|
46
|
-
getSeverityThreshold: () => getSeverityThreshold,
|
|
45
|
+
getSeverityLabel: () => getSeverityLabel2,
|
|
47
46
|
getSmartDefaults: () => getSmartDefaults,
|
|
48
|
-
groupDuplicatesByFilePair: () => groupDuplicatesByFilePair
|
|
47
|
+
groupDuplicatesByFilePair: () => groupDuplicatesByFilePair,
|
|
48
|
+
logConfiguration: () => logConfiguration
|
|
49
49
|
});
|
|
50
50
|
module.exports = __toCommonJS(index_exports);
|
|
51
|
-
var
|
|
51
|
+
var import_core13 = require("@aiready/core");
|
|
52
52
|
|
|
53
53
|
// src/provider.ts
|
|
54
|
-
var
|
|
54
|
+
var import_core12 = require("@aiready/core");
|
|
55
55
|
|
|
56
56
|
// src/analyzer.ts
|
|
57
|
-
var
|
|
57
|
+
var import_core11 = require("@aiready/core");
|
|
58
58
|
|
|
59
59
|
// src/detector.ts
|
|
60
|
-
var
|
|
60
|
+
var import_core6 = require("@aiready/core");
|
|
61
61
|
|
|
62
62
|
// src/context-rules.ts
|
|
63
|
+
var import_core5 = require("@aiready/core");
|
|
64
|
+
|
|
65
|
+
// src/rules/categories/test-rules.ts
|
|
63
66
|
var import_core = require("@aiready/core");
|
|
64
|
-
var
|
|
67
|
+
var TEST_RULES = [
|
|
65
68
|
// Test Fixtures - Intentional duplication for test isolation
|
|
66
69
|
{
|
|
67
70
|
name: "test-fixtures",
|
|
@@ -74,6 +77,35 @@ var CONTEXT_RULES = [
|
|
|
74
77
|
reason: "Test fixture duplication is intentional for test isolation",
|
|
75
78
|
suggestion: "Consider if shared test setup would improve maintainability without coupling tests"
|
|
76
79
|
},
|
|
80
|
+
// E2E/Integration Test Page Objects - Test independence
|
|
81
|
+
{
|
|
82
|
+
name: "e2e-page-objects",
|
|
83
|
+
detect: (file, code) => {
|
|
84
|
+
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/");
|
|
85
|
+
const hasPageObjectPatterns = code.includes("page.") || code.includes("await page") || code.includes("locator") || code.includes("getBy") || code.includes("selector") || code.includes("click(") || code.includes("fill(");
|
|
86
|
+
return isE2ETest && hasPageObjectPatterns;
|
|
87
|
+
},
|
|
88
|
+
severity: import_core.Severity.Info,
|
|
89
|
+
reason: "E2E test duplication ensures test independence and reduces coupling",
|
|
90
|
+
suggestion: "Consider page object pattern only if duplication causes maintenance issues"
|
|
91
|
+
},
|
|
92
|
+
// Mock Data - Test data intentionally duplicated
|
|
93
|
+
{
|
|
94
|
+
name: "mock-data",
|
|
95
|
+
detect: (file, code) => {
|
|
96
|
+
const isMockFile = file.includes("/mocks/") || file.includes("/__mocks__/") || file.includes("/fixtures/") || file.includes(".mock.") || file.includes(".fixture.");
|
|
97
|
+
const hasMockData = code.includes("mock") || code.includes("Mock") || code.includes("fixture") || code.includes("stub") || code.includes("export const");
|
|
98
|
+
return isMockFile && hasMockData;
|
|
99
|
+
},
|
|
100
|
+
severity: import_core.Severity.Info,
|
|
101
|
+
reason: "Mock data duplication is expected for comprehensive test coverage",
|
|
102
|
+
suggestion: "Consider shared factories only for complex mock generation"
|
|
103
|
+
}
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
// src/rules/categories/web-rules.ts
|
|
107
|
+
var import_core2 = require("@aiready/core");
|
|
108
|
+
var WEB_RULES = [
|
|
77
109
|
// Email/Document Templates - Often intentionally similar for consistency
|
|
78
110
|
{
|
|
79
111
|
name: "templates",
|
|
@@ -82,66 +114,59 @@ var CONTEXT_RULES = [
|
|
|
82
114
|
const hasTemplateContent = (code.includes("return") || code.includes("export")) && (code.includes("html") || code.includes("subject") || code.includes("body"));
|
|
83
115
|
return isTemplate && hasTemplateContent;
|
|
84
116
|
},
|
|
85
|
-
severity:
|
|
117
|
+
severity: import_core2.Severity.Info,
|
|
86
118
|
reason: "Template duplication may be intentional for maintainability and branding consistency",
|
|
87
119
|
suggestion: "Extract shared structure only if templates become hard to maintain"
|
|
88
120
|
},
|
|
89
|
-
//
|
|
121
|
+
// Common UI Event Handlers - Very specific patterns only
|
|
90
122
|
{
|
|
91
|
-
name: "
|
|
123
|
+
name: "common-ui-handlers",
|
|
92
124
|
detect: (file, code) => {
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
return
|
|
125
|
+
const isUIFile = file.includes("/components/") || file.includes(".tsx") || file.includes(".jsx") || file.includes("/hooks/");
|
|
126
|
+
const hasCommonHandler = code.includes("handleClickOutside") && code.includes("dropdownRef.current") && code.includes("!dropdownRef.current.contains") || code.includes("handleEscape") && code.includes("event.key") && code.includes("=== 'Escape'") || code.includes("handleClickInside") && code.includes("event.stopPropagation");
|
|
127
|
+
return isUIFile && hasCommonHandler;
|
|
96
128
|
},
|
|
97
|
-
severity:
|
|
98
|
-
reason: "
|
|
99
|
-
suggestion: "Consider
|
|
129
|
+
severity: import_core2.Severity.Info,
|
|
130
|
+
reason: "Common UI event handlers are boilerplate patterns that repeat across components",
|
|
131
|
+
suggestion: "Consider extracting to shared hooks (useClickOutside, useEscapeKey) only if causing maintenance issues"
|
|
100
132
|
},
|
|
133
|
+
// Next.js Route Handler Patterns - Boilerplate API route patterns
|
|
134
|
+
{
|
|
135
|
+
name: "nextjs-route-handlers",
|
|
136
|
+
detect: (file, code) => {
|
|
137
|
+
const isRouteFile = file.includes("/api/") && (file.endsWith("/route.ts") || file.endsWith("/route.js"));
|
|
138
|
+
const hasRoutePattern = code.includes("export async function POST") || code.includes("export async function GET") || code.includes("export async function PUT") || code.includes("export async function DELETE") || code.includes("NextResponse.json") || code.includes("NextRequest");
|
|
139
|
+
return isRouteFile && hasRoutePattern;
|
|
140
|
+
},
|
|
141
|
+
severity: import_core2.Severity.Info,
|
|
142
|
+
reason: "Next.js route handlers follow standard patterns and are intentionally similar across endpoints",
|
|
143
|
+
suggestion: "Route handler duplication is acceptable for API endpoint boilerplate"
|
|
144
|
+
}
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
// src/rules/categories/infra-rules.ts
|
|
148
|
+
var import_core3 = require("@aiready/core");
|
|
149
|
+
var INFRA_RULES = [
|
|
101
150
|
// Configuration Files - Often necessarily similar by design
|
|
102
151
|
{
|
|
103
152
|
name: "config-files",
|
|
104
153
|
detect: (file) => {
|
|
105
154
|
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");
|
|
106
155
|
},
|
|
107
|
-
severity:
|
|
156
|
+
severity: import_core3.Severity.Info,
|
|
108
157
|
reason: "Configuration files often have similar structure by design",
|
|
109
158
|
suggestion: "Consider shared config base only if configurations become hard to maintain"
|
|
110
159
|
},
|
|
111
|
-
// Type Definitions - Duplication for type safety and module independence
|
|
112
|
-
{
|
|
113
|
-
name: "type-definitions",
|
|
114
|
-
detect: (file, code) => {
|
|
115
|
-
const isTypeFile = file.endsWith(".d.ts") || file.includes("/types/");
|
|
116
|
-
const hasTypeDefinitions = code.includes("interface ") || code.includes("type ") || code.includes("enum ");
|
|
117
|
-
return isTypeFile && hasTypeDefinitions;
|
|
118
|
-
},
|
|
119
|
-
severity: import_core.Severity.Info,
|
|
120
|
-
reason: "Type duplication may be intentional for module independence and type safety",
|
|
121
|
-
suggestion: "Extract to shared types package only if causing maintenance burden"
|
|
122
|
-
},
|
|
123
160
|
// Migration Scripts - One-off scripts that are similar by nature
|
|
124
161
|
{
|
|
125
162
|
name: "migration-scripts",
|
|
126
163
|
detect: (file) => {
|
|
127
164
|
return file.includes("/migrations/") || file.includes("/migrate/") || file.includes(".migration.");
|
|
128
165
|
},
|
|
129
|
-
severity:
|
|
166
|
+
severity: import_core3.Severity.Info,
|
|
130
167
|
reason: "Migration scripts are typically one-off and intentionally similar",
|
|
131
168
|
suggestion: "Duplication is acceptable for migration scripts"
|
|
132
169
|
},
|
|
133
|
-
// Mock Data - Test data intentionally duplicated
|
|
134
|
-
{
|
|
135
|
-
name: "mock-data",
|
|
136
|
-
detect: (file, code) => {
|
|
137
|
-
const isMockFile = file.includes("/mocks/") || file.includes("/__mocks__/") || file.includes("/fixtures/") || file.includes(".mock.") || file.includes(".fixture.");
|
|
138
|
-
const hasMockData = code.includes("mock") || code.includes("Mock") || code.includes("fixture") || code.includes("stub") || code.includes("export const");
|
|
139
|
-
return isMockFile && hasMockData;
|
|
140
|
-
},
|
|
141
|
-
severity: import_core.Severity.Info,
|
|
142
|
-
reason: "Mock data duplication is expected for comprehensive test coverage",
|
|
143
|
-
suggestion: "Consider shared factories only for complex mock generation"
|
|
144
|
-
},
|
|
145
170
|
// Tool Implementations - Structural Boilerplate
|
|
146
171
|
{
|
|
147
172
|
name: "tool-implementations",
|
|
@@ -150,11 +175,132 @@ var CONTEXT_RULES = [
|
|
|
150
175
|
const hasToolStructure = code.includes("execute") && (code.includes("try") || code.includes("catch"));
|
|
151
176
|
return isToolFile && hasToolStructure;
|
|
152
177
|
},
|
|
153
|
-
severity:
|
|
178
|
+
severity: import_core3.Severity.Info,
|
|
154
179
|
reason: "Tool implementations share structural boilerplate but have distinct business logic",
|
|
155
180
|
suggestion: "Tool duplication is acceptable for boilerplate interface wrappers"
|
|
181
|
+
},
|
|
182
|
+
// CLI Command Definitions - Commander.js boilerplate patterns
|
|
183
|
+
{
|
|
184
|
+
name: "cli-command-definitions",
|
|
185
|
+
detect: (file, code) => {
|
|
186
|
+
const isCliFile = file.includes("/commands/") || file.includes("/cli/") || file.endsWith(".command.ts");
|
|
187
|
+
const hasCommandPattern = (code.includes(".command(") || code.includes("defineCommand")) && (code.includes(".description(") || code.includes(".option(")) && (code.includes(".action(") || code.includes("async"));
|
|
188
|
+
return isCliFile && hasCommandPattern;
|
|
189
|
+
},
|
|
190
|
+
severity: import_core3.Severity.Info,
|
|
191
|
+
reason: "CLI command definitions follow standard Commander.js patterns and are intentionally similar",
|
|
192
|
+
suggestion: "Command boilerplate duplication is acceptable for CLI interfaces"
|
|
193
|
+
}
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
// src/rules/categories/logic-rules.ts
|
|
197
|
+
var import_core4 = require("@aiready/core");
|
|
198
|
+
var LOGIC_RULES = [
|
|
199
|
+
// Type Definitions - Duplication for type safety and module independence
|
|
200
|
+
{
|
|
201
|
+
name: "type-definitions",
|
|
202
|
+
detect: (file, code) => {
|
|
203
|
+
const isTypeFile = file.endsWith(".d.ts") || file.includes("/types/");
|
|
204
|
+
const hasOnlyTypeDefinitions = (code.includes("interface ") || code.includes("type ") || code.includes("enum ")) && !code.includes("function ") && !code.includes("class ") && !code.includes("const ") && !code.includes("let ") && !code.includes("export default");
|
|
205
|
+
const isInterfaceOnlySnippet = code.trim().startsWith("interface ") && code.includes("{") && code.includes("}") && !code.includes("function ") && !code.includes("const ") && !code.includes("return ");
|
|
206
|
+
return isTypeFile && hasOnlyTypeDefinitions || isInterfaceOnlySnippet;
|
|
207
|
+
},
|
|
208
|
+
severity: import_core4.Severity.Info,
|
|
209
|
+
reason: "Type/interface definitions are intentionally duplicated for module independence",
|
|
210
|
+
suggestion: "Extract to shared types package only if causing maintenance burden"
|
|
211
|
+
},
|
|
212
|
+
// Utility Functions - Small helpers in dedicated utility files
|
|
213
|
+
{
|
|
214
|
+
name: "utility-functions",
|
|
215
|
+
detect: (file, code) => {
|
|
216
|
+
const isUtilFile = file.endsWith(".util.ts") || file.endsWith(".helper.ts") || file.endsWith(".utils.ts");
|
|
217
|
+
const hasUtilPattern = code.includes("function format") || code.includes("function parse") || code.includes("function sanitize") || code.includes("function normalize") || code.includes("function convert");
|
|
218
|
+
return isUtilFile && hasUtilPattern;
|
|
219
|
+
},
|
|
220
|
+
severity: import_core4.Severity.Info,
|
|
221
|
+
reason: "Utility functions in dedicated utility files may be intentionally similar",
|
|
222
|
+
suggestion: "Consider extracting to shared utilities only if causing significant duplication"
|
|
223
|
+
},
|
|
224
|
+
// React/Vue Hooks - Standard patterns
|
|
225
|
+
{
|
|
226
|
+
name: "shared-hooks",
|
|
227
|
+
detect: (file, code) => {
|
|
228
|
+
const isHookFile = file.includes("/hooks/") || file.endsWith(".hook.ts") || file.endsWith(".hook.tsx");
|
|
229
|
+
const hasHookPattern = code.includes("function use") || code.includes("export function use") || code.includes("const use") || code.includes("export const use");
|
|
230
|
+
return isHookFile && hasHookPattern;
|
|
231
|
+
},
|
|
232
|
+
severity: import_core4.Severity.Info,
|
|
233
|
+
reason: "Hooks follow standard patterns and are often intentionally similar across components",
|
|
234
|
+
suggestion: "Consider extracting common hook logic only if hooks become complex"
|
|
235
|
+
},
|
|
236
|
+
// Score/Rating Helper Functions - Common threshold patterns
|
|
237
|
+
{
|
|
238
|
+
name: "score-helpers",
|
|
239
|
+
detect: (file, code) => {
|
|
240
|
+
const isHelperFile = file.includes("/utils/") || file.includes("/helpers/") || file.endsWith(".util.ts");
|
|
241
|
+
const hasScorePattern = (code.includes("if (score >=") || code.includes("if (score >")) && code.includes("return") && code.includes("'") && code.split("if (score").length >= 3;
|
|
242
|
+
return isHelperFile && hasScorePattern;
|
|
243
|
+
},
|
|
244
|
+
severity: import_core4.Severity.Info,
|
|
245
|
+
reason: "Score/rating helper functions use common threshold patterns that are intentionally similar",
|
|
246
|
+
suggestion: "Score formatting duplication is acceptable for consistent UI display"
|
|
247
|
+
},
|
|
248
|
+
// D3/Canvas Event Handlers - Standard visualization patterns
|
|
249
|
+
{
|
|
250
|
+
name: "visualization-handlers",
|
|
251
|
+
detect: (file, code) => {
|
|
252
|
+
const isVizFile = file.includes("/visualizer/") || file.includes("/charts/") || file.includes("GraphCanvas") || file.includes("ForceDirected");
|
|
253
|
+
const hasVizPattern = (code.includes("dragstarted") || code.includes("dragged") || code.includes("dragended")) && (code.includes("simulation") || code.includes("d3.") || code.includes("alphaTarget"));
|
|
254
|
+
return isVizFile && hasVizPattern;
|
|
255
|
+
},
|
|
256
|
+
severity: import_core4.Severity.Info,
|
|
257
|
+
reason: "D3/visualization event handlers follow standard patterns and are intentionally similar",
|
|
258
|
+
suggestion: "Visualization boilerplate duplication is acceptable for interactive charts"
|
|
259
|
+
},
|
|
260
|
+
// Icon/Switch Statement Helpers - Common enum-to-value patterns
|
|
261
|
+
{
|
|
262
|
+
name: "switch-helpers",
|
|
263
|
+
detect: (file, code) => {
|
|
264
|
+
const hasSwitchPattern = code.includes("switch (") && code.includes("case '") && code.includes("return") && code.split("case ").length >= 4;
|
|
265
|
+
const hasIconPattern = code.includes("getIcon") || code.includes("getColor") || code.includes("getLabel") || code.includes("getRating");
|
|
266
|
+
return hasSwitchPattern && hasIconPattern;
|
|
267
|
+
},
|
|
268
|
+
severity: import_core4.Severity.Info,
|
|
269
|
+
reason: "Switch statement helpers for enum-to-value mapping are inherently similar",
|
|
270
|
+
suggestion: "Switch duplication is acceptable for mapping enums to display values"
|
|
271
|
+
},
|
|
272
|
+
// Common API/Utility Functions - Legitimate duplication across modules
|
|
273
|
+
{
|
|
274
|
+
name: "common-api-functions",
|
|
275
|
+
detect: (file, code) => {
|
|
276
|
+
const isApiFile = file.includes("/api/") || file.includes("/lib/") || file.includes("/utils/") || file.endsWith(".ts");
|
|
277
|
+
const hasCommonApiPattern = code.includes("getStripe") && code.includes("process.env.STRIPE_SECRET_KEY") || code.includes("getUserByEmail") && code.includes("queryItems") || code.includes("updateUser") && code.includes("buildUpdateExpression") || code.includes("listUserRepositories") && code.includes("queryItems") || code.includes("listTeamRepositories") && code.includes("queryItems") || code.includes("getRemediation") && code.includes("queryItems") || code.includes("formatBreakdownKey") && code.includes(".replace(/([A-Z])/g") || code.includes("queryItems") && code.includes("KeyConditionExpression") || code.includes("putItem") && code.includes("createdAt") || code.includes("updateItem") && code.includes("buildUpdateExpression");
|
|
278
|
+
return isApiFile && hasCommonApiPattern;
|
|
279
|
+
},
|
|
280
|
+
severity: import_core4.Severity.Info,
|
|
281
|
+
reason: "Common API/utility functions are legitimately duplicated across modules for clarity and independence",
|
|
282
|
+
suggestion: "Consider extracting to shared utilities only if causing significant duplication"
|
|
283
|
+
},
|
|
284
|
+
// Validation Functions - Inherently similar patterns
|
|
285
|
+
{
|
|
286
|
+
name: "validation-functions",
|
|
287
|
+
detect: (file, code) => {
|
|
288
|
+
const hasValidationPattern = code.includes("isValid") || code.includes("validate") || code.includes("checkValid") || code.includes("isEmail") || code.includes("isPhone") || code.includes("isUrl") || code.includes("isNumeric") || code.includes("isAlpha") || code.includes("isAlphanumeric") || code.includes("isEmpty") || code.includes("isNotEmpty") || code.includes("isRequired") || code.includes("isOptional");
|
|
289
|
+
return hasValidationPattern;
|
|
290
|
+
},
|
|
291
|
+
severity: import_core4.Severity.Info,
|
|
292
|
+
reason: "Validation functions are inherently similar and often intentionally duplicated for domain clarity",
|
|
293
|
+
suggestion: "Consider extracting to shared validators only if validation logic becomes complex"
|
|
156
294
|
}
|
|
157
295
|
];
|
|
296
|
+
|
|
297
|
+
// src/context-rules.ts
|
|
298
|
+
var CONTEXT_RULES = [
|
|
299
|
+
...TEST_RULES,
|
|
300
|
+
...WEB_RULES,
|
|
301
|
+
...INFRA_RULES,
|
|
302
|
+
...LOGIC_RULES
|
|
303
|
+
];
|
|
158
304
|
function calculateSeverity(file1, file2, code, similarity, linesOfCode) {
|
|
159
305
|
for (const rule of CONTEXT_RULES) {
|
|
160
306
|
if (rule.detect(file1, code) || rule.detect(file2, code)) {
|
|
@@ -168,45 +314,36 @@ function calculateSeverity(file1, file2, code, similarity, linesOfCode) {
|
|
|
168
314
|
}
|
|
169
315
|
if (similarity >= 0.95 && linesOfCode >= 30) {
|
|
170
316
|
return {
|
|
171
|
-
severity:
|
|
317
|
+
severity: import_core5.Severity.Critical,
|
|
172
318
|
reason: "Large nearly-identical code blocks waste tokens and create maintenance burden",
|
|
173
319
|
suggestion: "Extract to shared utility module immediately"
|
|
174
320
|
};
|
|
175
321
|
} else if (similarity >= 0.95 && linesOfCode >= 15) {
|
|
176
322
|
return {
|
|
177
|
-
severity:
|
|
323
|
+
severity: import_core5.Severity.Major,
|
|
178
324
|
reason: "Nearly identical code should be consolidated",
|
|
179
325
|
suggestion: "Move to shared utility file"
|
|
180
326
|
};
|
|
181
327
|
} else if (similarity >= 0.85) {
|
|
182
328
|
return {
|
|
183
|
-
severity:
|
|
329
|
+
severity: import_core5.Severity.Major,
|
|
184
330
|
reason: "High similarity indicates significant duplication",
|
|
185
331
|
suggestion: "Extract common logic to shared function"
|
|
186
332
|
};
|
|
187
333
|
} else if (similarity >= 0.7) {
|
|
188
334
|
return {
|
|
189
|
-
severity:
|
|
335
|
+
severity: import_core5.Severity.Minor,
|
|
190
336
|
reason: "Moderate similarity detected",
|
|
191
337
|
suggestion: "Consider extracting shared patterns if code evolves together"
|
|
192
338
|
};
|
|
193
339
|
} else {
|
|
194
340
|
return {
|
|
195
|
-
severity:
|
|
341
|
+
severity: import_core5.Severity.Minor,
|
|
196
342
|
reason: "Minor similarity detected",
|
|
197
343
|
suggestion: "Monitor but refactoring may not be worthwhile"
|
|
198
344
|
};
|
|
199
345
|
}
|
|
200
346
|
}
|
|
201
|
-
function getSeverityThreshold(severity) {
|
|
202
|
-
const thresholds = {
|
|
203
|
-
[import_core.Severity.Critical]: 0.95,
|
|
204
|
-
[import_core.Severity.Major]: 0.85,
|
|
205
|
-
[import_core.Severity.Minor]: 0.5,
|
|
206
|
-
[import_core.Severity.Info]: 0
|
|
207
|
-
};
|
|
208
|
-
return thresholds[severity] || 0;
|
|
209
|
-
}
|
|
210
347
|
|
|
211
348
|
// src/core/normalizer.ts
|
|
212
349
|
function normalizeCode(code, isPython = false) {
|
|
@@ -222,13 +359,13 @@ function normalizeCode(code, isPython = false) {
|
|
|
222
359
|
|
|
223
360
|
// src/detector.ts
|
|
224
361
|
function extractBlocks(file, content) {
|
|
225
|
-
return (0,
|
|
362
|
+
return (0, import_core6.extractCodeBlocks)(file, content);
|
|
226
363
|
}
|
|
227
364
|
function calculateSimilarity(a, b) {
|
|
228
|
-
return (0,
|
|
365
|
+
return (0, import_core6.calculateStringSimilarity)(a, b);
|
|
229
366
|
}
|
|
230
367
|
function calculateConfidence(similarity, tokens, lines) {
|
|
231
|
-
return (0,
|
|
368
|
+
return (0, import_core6.calculateHeuristicConfidence)(similarity, tokens, lines);
|
|
232
369
|
}
|
|
233
370
|
async function detectDuplicatePatterns(fileContents, options) {
|
|
234
371
|
const {
|
|
@@ -338,7 +475,7 @@ async function detectDuplicatePatterns(fileContents, options) {
|
|
|
338
475
|
}
|
|
339
476
|
|
|
340
477
|
// src/grouping.ts
|
|
341
|
-
var
|
|
478
|
+
var import_core7 = require("@aiready/core");
|
|
342
479
|
var import_path = __toESM(require("path"));
|
|
343
480
|
function groupDuplicatesByFilePair(duplicates) {
|
|
344
481
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -366,7 +503,7 @@ function groupDuplicatesByFilePair(duplicates) {
|
|
|
366
503
|
file2: { start: dup.line2, end: dup.endLine2 }
|
|
367
504
|
});
|
|
368
505
|
const currentSev = dup.severity;
|
|
369
|
-
if ((0,
|
|
506
|
+
if ((0, import_core7.getSeverityLevel)(currentSev) > (0, import_core7.getSeverityLevel)(group.severity)) {
|
|
370
507
|
group.severity = currentSev;
|
|
371
508
|
}
|
|
372
509
|
}
|
|
@@ -456,21 +593,77 @@ function filterClustersByImpact(clusters, minTokenCost = 1e3, minFiles = 3) {
|
|
|
456
593
|
(c) => c.totalTokenCost >= minTokenCost && c.files.length >= minFiles
|
|
457
594
|
);
|
|
458
595
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
596
|
+
function isPureInterfaceDefinition(code) {
|
|
597
|
+
const trimmed = code.trim();
|
|
598
|
+
if (!trimmed.startsWith("interface ") && !trimmed.startsWith("type ") && !trimmed.startsWith("export interface ") && !trimmed.startsWith("export type ") && !trimmed.startsWith("enum ") && !trimmed.startsWith("export enum ")) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
if (trimmed.includes("={") || trimmed.includes("=> {") || trimmed.includes("function ") || trimmed.includes("() {") || trimmed.includes(" implements ")) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
if (trimmed.length > 200) return false;
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
var BRAND_INDICATORS = [
|
|
608
|
+
"cyberpunk",
|
|
609
|
+
"cyber-blue",
|
|
610
|
+
"cyber-purple",
|
|
611
|
+
"slate-900",
|
|
612
|
+
"slate-400",
|
|
613
|
+
"zinc-",
|
|
614
|
+
"indigo-",
|
|
615
|
+
"neon-",
|
|
616
|
+
"glassmorphism",
|
|
617
|
+
"backdrop-blur"
|
|
618
|
+
];
|
|
619
|
+
function isBrandSpecificComponent(filePath) {
|
|
620
|
+
const lower = filePath.toLowerCase();
|
|
621
|
+
const brandingTerms = ["landing", "clawmore", "platform", "apps/"];
|
|
622
|
+
for (const term of brandingTerms) {
|
|
623
|
+
if (lower.includes(term)) return true;
|
|
624
|
+
}
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
function areBrandSpecificVariants(file1, file2, code1, code2) {
|
|
628
|
+
const f1IsBrand = isBrandSpecificComponent(file1);
|
|
629
|
+
const f2IsBrand = isBrandSpecificComponent(file2);
|
|
630
|
+
if (f1IsBrand && f2IsBrand && file1 !== file2) {
|
|
631
|
+
const hasBrandKeyword = (code) => {
|
|
632
|
+
const lowerCode = code.toLowerCase();
|
|
633
|
+
return BRAND_INDICATORS.some((ind) => lowerCode.includes(ind));
|
|
634
|
+
};
|
|
635
|
+
const code1Brand = hasBrandKeyword(code1);
|
|
636
|
+
const code2Brand = hasBrandKeyword(code2);
|
|
637
|
+
if (code1Brand && code2Brand) {
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return false;
|
|
473
642
|
}
|
|
643
|
+
function filterBrandSpecificVariants(duplicates) {
|
|
644
|
+
return duplicates.filter((dup) => {
|
|
645
|
+
if (dup.file1 === dup.file2) return true;
|
|
646
|
+
const isBrandVariant = areBrandSpecificVariants(
|
|
647
|
+
dup.file1,
|
|
648
|
+
dup.file2,
|
|
649
|
+
dup.code1,
|
|
650
|
+
dup.code2
|
|
651
|
+
);
|
|
652
|
+
if (isBrandVariant) {
|
|
653
|
+
dup.severity = import_core7.Severity.Info;
|
|
654
|
+
dup.suggestion = "Brand-specific themed component variant (intentional)";
|
|
655
|
+
}
|
|
656
|
+
const isInterfaceDef = isPureInterfaceDefinition(dup.code1) && isPureInterfaceDefinition(dup.code2);
|
|
657
|
+
if (isInterfaceDef) {
|
|
658
|
+
dup.severity = import_core7.Severity.Info;
|
|
659
|
+
dup.suggestion = "Pure interface/type definition - intentional for module independence";
|
|
660
|
+
}
|
|
661
|
+
return true;
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/config.ts
|
|
666
|
+
var import_core8 = require("@aiready/core");
|
|
474
667
|
async function getSmartDefaults(directory, userOptions) {
|
|
475
668
|
if (userOptions.useSmartDefaults === false) {
|
|
476
669
|
return {
|
|
@@ -491,7 +684,7 @@ async function getSmartDefaults(directory, userOptions) {
|
|
|
491
684
|
include: userOptions.include || ["**/*.{ts,tsx,js,jsx,py,java}"],
|
|
492
685
|
exclude: userOptions.exclude
|
|
493
686
|
};
|
|
494
|
-
const files = await (0,
|
|
687
|
+
const files = await (0, import_core8.scanFiles)(scanOptions);
|
|
495
688
|
const fileCount = files.length;
|
|
496
689
|
const estimatedBlocks = fileCount * 5;
|
|
497
690
|
const minLines = Math.max(
|
|
@@ -549,113 +742,21 @@ function logConfiguration(config, estimatedBlocks) {
|
|
|
549
742
|
}
|
|
550
743
|
console.log("");
|
|
551
744
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
minClusterFiles = 3,
|
|
568
|
-
excludePatterns = [],
|
|
569
|
-
confidenceThreshold = 0,
|
|
570
|
-
ignoreWhitelist = [],
|
|
571
|
-
...scanOptions
|
|
572
|
-
} = finalOptions;
|
|
573
|
-
const files = await (0, import_core4.scanFiles)(scanOptions);
|
|
574
|
-
const estimatedBlocks = files.length * 3;
|
|
575
|
-
logConfiguration(finalOptions, estimatedBlocks);
|
|
576
|
-
const results = [];
|
|
577
|
-
const READ_BATCH_SIZE = 50;
|
|
578
|
-
const fileContents = [];
|
|
579
|
-
for (let i = 0; i < files.length; i += READ_BATCH_SIZE) {
|
|
580
|
-
const batch = files.slice(i, i + READ_BATCH_SIZE);
|
|
581
|
-
const batchContents = await Promise.all(
|
|
582
|
-
batch.map(async (file) => ({
|
|
583
|
-
file,
|
|
584
|
-
content: await (0, import_core4.readFileContent)(file)
|
|
585
|
-
}))
|
|
586
|
-
);
|
|
587
|
-
fileContents.push(...batchContents);
|
|
588
|
-
}
|
|
589
|
-
const duplicates = await detectDuplicatePatterns(fileContents, {
|
|
590
|
-
minSimilarity,
|
|
591
|
-
minLines,
|
|
592
|
-
batchSize,
|
|
593
|
-
approx,
|
|
594
|
-
minSharedTokens,
|
|
595
|
-
maxCandidatesPerBlock,
|
|
596
|
-
streamResults,
|
|
597
|
-
excludePatterns,
|
|
598
|
-
confidenceThreshold,
|
|
599
|
-
ignoreWhitelist,
|
|
600
|
-
onProgress: options.onProgress
|
|
601
|
-
});
|
|
602
|
-
for (const file of files) {
|
|
603
|
-
const fileDuplicates = duplicates.filter(
|
|
604
|
-
(dup) => dup.file1 === file || dup.file2 === file
|
|
605
|
-
);
|
|
606
|
-
const issues = fileDuplicates.map((dup) => {
|
|
607
|
-
const otherFile = dup.file1 === file ? dup.file2 : dup.file1;
|
|
608
|
-
const severity2 = dup.similarity > 0.95 ? import_core4.Severity.Critical : dup.similarity > 0.9 ? import_core4.Severity.Major : import_core4.Severity.Minor;
|
|
609
|
-
return {
|
|
610
|
-
type: import_core4.IssueType.DuplicatePattern,
|
|
611
|
-
severity: severity2,
|
|
612
|
-
message: `${dup.patternType} pattern ${Math.round(dup.similarity * 100)}% similar to ${otherFile} (${dup.tokenCost} tokens wasted)`,
|
|
613
|
-
location: {
|
|
614
|
-
file,
|
|
615
|
-
line: dup.file1 === file ? dup.line1 : dup.line2
|
|
616
|
-
},
|
|
617
|
-
suggestion: getRefactoringSuggestion(dup.patternType, dup.similarity)
|
|
618
|
-
};
|
|
619
|
-
});
|
|
620
|
-
let filteredIssues = issues;
|
|
621
|
-
if (severity !== "all") {
|
|
622
|
-
const severityMap = {
|
|
623
|
-
critical: [import_core4.Severity.Critical],
|
|
624
|
-
high: [import_core4.Severity.Critical, import_core4.Severity.Major],
|
|
625
|
-
medium: [import_core4.Severity.Critical, import_core4.Severity.Major, import_core4.Severity.Minor]
|
|
626
|
-
};
|
|
627
|
-
const allowedSeverities = severityMap[severity] || [import_core4.Severity.Critical, import_core4.Severity.Major, import_core4.Severity.Minor];
|
|
628
|
-
filteredIssues = issues.filter(
|
|
629
|
-
(issue) => allowedSeverities.includes(issue.severity)
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
const totalTokenCost = fileDuplicates.reduce(
|
|
633
|
-
(sum, dup) => sum + dup.tokenCost,
|
|
634
|
-
0
|
|
635
|
-
);
|
|
636
|
-
results.push({
|
|
637
|
-
fileName: file,
|
|
638
|
-
issues: filteredIssues,
|
|
639
|
-
metrics: {
|
|
640
|
-
tokenCost: totalTokenCost,
|
|
641
|
-
consistencyScore: Math.max(0, 1 - fileDuplicates.length * 0.1)
|
|
642
|
-
}
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
let groups;
|
|
646
|
-
let clusters;
|
|
647
|
-
if (groupByFilePair) {
|
|
648
|
-
groups = groupDuplicatesByFilePair(duplicates);
|
|
649
|
-
}
|
|
650
|
-
if (createClusters) {
|
|
651
|
-
const allClusters = createRefactorClusters(duplicates);
|
|
652
|
-
clusters = filterClustersByImpact(
|
|
653
|
-
allClusters,
|
|
654
|
-
minClusterTokenCost,
|
|
655
|
-
minClusterFiles
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
return { results, duplicates, files, groups, clusters, config: finalOptions };
|
|
745
|
+
|
|
746
|
+
// src/summary.ts
|
|
747
|
+
var import_core9 = require("@aiready/core");
|
|
748
|
+
function getRefactoringSuggestion(patternType, similarity) {
|
|
749
|
+
const baseMessages = {
|
|
750
|
+
"api-handler": "Extract common middleware or create a base handler class",
|
|
751
|
+
validator: "Consolidate validation logic into shared schema validators (Zod/Yup)",
|
|
752
|
+
utility: "Move to a shared utilities file and reuse across modules",
|
|
753
|
+
"class-method": "Consider inheritance or composition to share behavior",
|
|
754
|
+
component: "Extract shared logic into a custom hook or HOC",
|
|
755
|
+
function: "Extract into a shared helper function",
|
|
756
|
+
unknown: "Extract common logic into a reusable module"
|
|
757
|
+
};
|
|
758
|
+
const urgency = similarity > 0.95 ? " (CRITICAL: Nearly identical code)" : similarity > 0.9 ? " (HIGH: Very similar, refactor soon)" : "";
|
|
759
|
+
return baseMessages[patternType] + urgency;
|
|
659
760
|
}
|
|
660
761
|
function generateSummary(results) {
|
|
661
762
|
const allIssues = results.flatMap((r) => r.issues || []);
|
|
@@ -675,7 +776,7 @@ function generateSummary(results) {
|
|
|
675
776
|
allIssues.forEach((issue) => {
|
|
676
777
|
const match = issue.message.match(/^(\S+(?:-\S+)*) pattern/);
|
|
677
778
|
if (match) {
|
|
678
|
-
const type = match[1];
|
|
779
|
+
const type = match[1] || "unknown";
|
|
679
780
|
patternsByType[type] = (patternsByType[type] || 0) + 1;
|
|
680
781
|
}
|
|
681
782
|
});
|
|
@@ -698,8 +799,6 @@ function generateSummary(results) {
|
|
|
698
799
|
}
|
|
699
800
|
],
|
|
700
801
|
similarity: similarityMatch ? parseInt(similarityMatch[1]) / 100 : 0,
|
|
701
|
-
confidence: similarityMatch ? parseInt(similarityMatch[1]) / 100 : 0,
|
|
702
|
-
// Fallback for summary
|
|
703
802
|
patternType: typeMatch?.[1] || "unknown",
|
|
704
803
|
tokenCost: tokenMatch ? parseInt(tokenMatch[1]) : 0
|
|
705
804
|
};
|
|
@@ -711,18 +810,55 @@ function generateSummary(results) {
|
|
|
711
810
|
topDuplicates
|
|
712
811
|
};
|
|
713
812
|
}
|
|
813
|
+
function filterBySeverity2(issues, severity) {
|
|
814
|
+
if (severity === "all") return issues;
|
|
815
|
+
const severityMap = {
|
|
816
|
+
critical: [import_core9.Severity.Critical],
|
|
817
|
+
high: [import_core9.Severity.Critical, import_core9.Severity.Major],
|
|
818
|
+
medium: [import_core9.Severity.Critical, import_core9.Severity.Major, import_core9.Severity.Minor]
|
|
819
|
+
};
|
|
820
|
+
const allowed = severityMap[severity] || [
|
|
821
|
+
import_core9.Severity.Critical,
|
|
822
|
+
import_core9.Severity.Major,
|
|
823
|
+
import_core9.Severity.Minor
|
|
824
|
+
];
|
|
825
|
+
return issues.filter((issue) => allowed.includes(issue.severity));
|
|
826
|
+
}
|
|
827
|
+
function getSeverityLabel2(severity) {
|
|
828
|
+
switch (severity) {
|
|
829
|
+
case import_core9.Severity.Critical:
|
|
830
|
+
return "CRITICAL";
|
|
831
|
+
case import_core9.Severity.Major:
|
|
832
|
+
return "HIGH";
|
|
833
|
+
case import_core9.Severity.Minor:
|
|
834
|
+
return "MEDIUM";
|
|
835
|
+
case import_core9.Severity.Info:
|
|
836
|
+
return "LOW";
|
|
837
|
+
default:
|
|
838
|
+
return "UNKNOWN";
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function calculateSeverity2(similarity) {
|
|
842
|
+
if (similarity > 0.95) return import_core9.Severity.Critical;
|
|
843
|
+
if (similarity > 0.9) return import_core9.Severity.Major;
|
|
844
|
+
return import_core9.Severity.Minor;
|
|
845
|
+
}
|
|
714
846
|
|
|
715
847
|
// src/scoring.ts
|
|
716
|
-
var
|
|
848
|
+
var import_core10 = require("@aiready/core");
|
|
717
849
|
function calculatePatternScore(duplicates, totalFilesAnalyzed, costConfig) {
|
|
718
|
-
const
|
|
719
|
-
const
|
|
720
|
-
const
|
|
850
|
+
const actionableDuplicates = duplicates.filter((d) => d.severity !== "info");
|
|
851
|
+
const totalDuplicates = actionableDuplicates.length;
|
|
852
|
+
const totalTokenCost = actionableDuplicates.reduce(
|
|
853
|
+
(sum, d) => sum + d.tokenCost,
|
|
854
|
+
0
|
|
855
|
+
);
|
|
856
|
+
const highImpactDuplicates = actionableDuplicates.filter(
|
|
721
857
|
(d) => d.tokenCost > 1e3 || d.similarity > 0.7
|
|
722
858
|
).length;
|
|
723
859
|
if (totalFilesAnalyzed === 0) {
|
|
724
860
|
return {
|
|
725
|
-
toolName:
|
|
861
|
+
toolName: import_core10.ToolName.PatternDetect,
|
|
726
862
|
score: 100,
|
|
727
863
|
rawMetrics: {
|
|
728
864
|
totalDuplicates: 0,
|
|
@@ -791,12 +927,12 @@ function calculatePatternScore(duplicates, totalFilesAnalyzed, costConfig) {
|
|
|
791
927
|
priority: totalTokenCost > 1e4 ? "high" : "medium"
|
|
792
928
|
});
|
|
793
929
|
}
|
|
794
|
-
const cfg = { ...
|
|
795
|
-
const estimatedMonthlyCost = (0,
|
|
930
|
+
const cfg = { ...import_core10.DEFAULT_COST_CONFIG, ...costConfig };
|
|
931
|
+
const estimatedMonthlyCost = (0, import_core10.calculateMonthlyCost)(totalTokenCost, cfg);
|
|
796
932
|
const issues = duplicates.map((d) => ({
|
|
797
933
|
severity: d.severity === "critical" ? "critical" : d.severity === "major" ? "major" : "minor"
|
|
798
934
|
}));
|
|
799
|
-
const productivityImpact = (0,
|
|
935
|
+
const productivityImpact = (0, import_core10.calculateProductivityImpact)(issues);
|
|
800
936
|
return {
|
|
801
937
|
toolName: "pattern-detect",
|
|
802
938
|
score: finalScore,
|
|
@@ -816,13 +952,118 @@ function calculatePatternScore(duplicates, totalFilesAnalyzed, costConfig) {
|
|
|
816
952
|
};
|
|
817
953
|
}
|
|
818
954
|
|
|
955
|
+
// src/analyzer.ts
|
|
956
|
+
async function analyzePatterns(options) {
|
|
957
|
+
const smartDefaults = await getSmartDefaults(options.rootDir || ".", options);
|
|
958
|
+
const finalOptions = { ...smartDefaults, ...options };
|
|
959
|
+
const {
|
|
960
|
+
minSimilarity = 0.4,
|
|
961
|
+
minLines = 5,
|
|
962
|
+
batchSize = 100,
|
|
963
|
+
approx = true,
|
|
964
|
+
minSharedTokens = 8,
|
|
965
|
+
maxCandidatesPerBlock = 100,
|
|
966
|
+
streamResults = false,
|
|
967
|
+
severity = "all",
|
|
968
|
+
groupByFilePair = true,
|
|
969
|
+
createClusters = true,
|
|
970
|
+
minClusterTokenCost = 1e3,
|
|
971
|
+
minClusterFiles = 3,
|
|
972
|
+
excludePatterns = [],
|
|
973
|
+
confidenceThreshold = 0,
|
|
974
|
+
ignoreWhitelist = [],
|
|
975
|
+
...scanOptions
|
|
976
|
+
} = finalOptions;
|
|
977
|
+
const files = await (0, import_core11.scanFiles)(scanOptions);
|
|
978
|
+
const estimatedBlocks = files.length * 3;
|
|
979
|
+
logConfiguration(finalOptions, estimatedBlocks);
|
|
980
|
+
const results = [];
|
|
981
|
+
const READ_BATCH_SIZE = 50;
|
|
982
|
+
const fileContents = [];
|
|
983
|
+
for (let i = 0; i < files.length; i += READ_BATCH_SIZE) {
|
|
984
|
+
const batch = files.slice(i, i + READ_BATCH_SIZE);
|
|
985
|
+
const batchContents = await Promise.all(
|
|
986
|
+
batch.map(async (file) => ({
|
|
987
|
+
file,
|
|
988
|
+
content: await (0, import_core11.readFileContent)(file)
|
|
989
|
+
}))
|
|
990
|
+
);
|
|
991
|
+
fileContents.push(...batchContents);
|
|
992
|
+
}
|
|
993
|
+
const duplicates = await detectDuplicatePatterns(fileContents, {
|
|
994
|
+
minSimilarity,
|
|
995
|
+
minLines,
|
|
996
|
+
batchSize,
|
|
997
|
+
approx,
|
|
998
|
+
minSharedTokens,
|
|
999
|
+
maxCandidatesPerBlock,
|
|
1000
|
+
streamResults,
|
|
1001
|
+
excludePatterns,
|
|
1002
|
+
confidenceThreshold,
|
|
1003
|
+
ignoreWhitelist,
|
|
1004
|
+
onProgress: options.onProgress
|
|
1005
|
+
});
|
|
1006
|
+
filterBrandSpecificVariants(duplicates);
|
|
1007
|
+
for (const file of files) {
|
|
1008
|
+
const fileDuplicates = duplicates.filter(
|
|
1009
|
+
(dup) => dup.file1 === file || dup.file2 === file
|
|
1010
|
+
);
|
|
1011
|
+
const issues = fileDuplicates.map((dup) => {
|
|
1012
|
+
const otherFile = dup.file1 === file ? dup.file2 : dup.file1;
|
|
1013
|
+
let severityLevel;
|
|
1014
|
+
if (dup.severity === "info" || dup.severity === "Info") {
|
|
1015
|
+
severityLevel = import_core11.Severity.Info;
|
|
1016
|
+
} else {
|
|
1017
|
+
severityLevel = calculateSeverity2(dup.similarity);
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
type: import_core11.IssueType.DuplicatePattern,
|
|
1021
|
+
severity: severityLevel,
|
|
1022
|
+
message: `${dup.patternType} pattern ${Math.round(dup.similarity * 100)}% similar to ${otherFile} (${dup.tokenCost} tokens wasted)`,
|
|
1023
|
+
location: {
|
|
1024
|
+
file,
|
|
1025
|
+
line: dup.file1 === file ? dup.line1 : dup.line2
|
|
1026
|
+
},
|
|
1027
|
+
suggestion: getRefactoringSuggestion(dup.patternType, dup.similarity)
|
|
1028
|
+
};
|
|
1029
|
+
});
|
|
1030
|
+
const filteredIssues = filterBySeverity2(issues, severity || "all");
|
|
1031
|
+
const totalTokenCost = fileDuplicates.reduce(
|
|
1032
|
+
(sum, dup) => sum + dup.tokenCost,
|
|
1033
|
+
0
|
|
1034
|
+
);
|
|
1035
|
+
results.push({
|
|
1036
|
+
fileName: file,
|
|
1037
|
+
issues: filteredIssues,
|
|
1038
|
+
metrics: {
|
|
1039
|
+
tokenCost: totalTokenCost,
|
|
1040
|
+
consistencyScore: Math.max(0, 1 - fileDuplicates.length * 0.1)
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
let groups;
|
|
1045
|
+
let clusters;
|
|
1046
|
+
if (groupByFilePair) {
|
|
1047
|
+
groups = groupDuplicatesByFilePair(duplicates);
|
|
1048
|
+
}
|
|
1049
|
+
if (createClusters) {
|
|
1050
|
+
const allClusters = createRefactorClusters(duplicates);
|
|
1051
|
+
clusters = filterClustersByImpact(
|
|
1052
|
+
allClusters,
|
|
1053
|
+
minClusterTokenCost,
|
|
1054
|
+
minClusterFiles
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
return { results, duplicates, files, groups, clusters, config: finalOptions };
|
|
1058
|
+
}
|
|
1059
|
+
|
|
819
1060
|
// src/provider.ts
|
|
820
|
-
var
|
|
821
|
-
id:
|
|
1061
|
+
var PATTERN_DETECT_PROVIDER = {
|
|
1062
|
+
id: import_core12.ToolName.PatternDetect,
|
|
822
1063
|
alias: ["patterns", "duplicates", "duplication"],
|
|
823
1064
|
async analyze(options) {
|
|
824
1065
|
const results = await analyzePatterns(options);
|
|
825
|
-
return
|
|
1066
|
+
return import_core12.SpokeOutputSchema.parse({
|
|
826
1067
|
results: results.results,
|
|
827
1068
|
summary: {
|
|
828
1069
|
totalFiles: results.files.length,
|
|
@@ -835,12 +1076,12 @@ var PatternDetectProvider = {
|
|
|
835
1076
|
clusters: results.clusters,
|
|
836
1077
|
config: Object.fromEntries(
|
|
837
1078
|
Object.entries(results.config).filter(
|
|
838
|
-
([key]) => !
|
|
1079
|
+
([key]) => !import_core12.GLOBAL_SCAN_OPTIONS.includes(key) || key === "rootDir"
|
|
839
1080
|
)
|
|
840
1081
|
)
|
|
841
1082
|
},
|
|
842
1083
|
metadata: {
|
|
843
|
-
toolName:
|
|
1084
|
+
toolName: import_core12.ToolName.PatternDetect,
|
|
844
1085
|
version: "0.12.5",
|
|
845
1086
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
846
1087
|
}
|
|
@@ -848,31 +1089,43 @@ var PatternDetectProvider = {
|
|
|
848
1089
|
},
|
|
849
1090
|
score(output, options) {
|
|
850
1091
|
const duplicates = output.summary.duplicates || [];
|
|
851
|
-
const scoreData = duplicates;
|
|
852
1092
|
const totalFiles = output.summary.totalFiles || output.results.length;
|
|
1093
|
+
let scoreData = duplicates;
|
|
1094
|
+
const patternOptions = options;
|
|
1095
|
+
if (patternOptions.severity && patternOptions.severity !== "all") {
|
|
1096
|
+
const severityMap = {
|
|
1097
|
+
critical: import_core12.Severity.Critical,
|
|
1098
|
+
high: import_core12.Severity.Major,
|
|
1099
|
+
// 'high' maps to Major and above
|
|
1100
|
+
medium: import_core12.Severity.Minor,
|
|
1101
|
+
all: import_core12.Severity.Info
|
|
1102
|
+
};
|
|
1103
|
+
const minSeverity = severityMap[patternOptions.severity] || import_core12.Severity.Info;
|
|
1104
|
+
scoreData = (0, import_core5.filterBySeverity)(duplicates, minSeverity);
|
|
1105
|
+
}
|
|
853
1106
|
return calculatePatternScore(scoreData, totalFiles, options.costConfig);
|
|
854
1107
|
},
|
|
855
1108
|
defaultWeight: 22
|
|
856
1109
|
};
|
|
857
1110
|
|
|
858
1111
|
// src/index.ts
|
|
859
|
-
|
|
1112
|
+
import_core13.ToolRegistry.register(PATTERN_DETECT_PROVIDER);
|
|
860
1113
|
// Annotate the CommonJS export names for ESM import in node:
|
|
861
1114
|
0 && (module.exports = {
|
|
862
|
-
|
|
863
|
-
IssueType,
|
|
864
|
-
PatternDetectProvider,
|
|
1115
|
+
PATTERN_DETECT_PROVIDER,
|
|
865
1116
|
Severity,
|
|
866
1117
|
analyzePatterns,
|
|
1118
|
+
areBrandSpecificVariants,
|
|
867
1119
|
calculatePatternScore,
|
|
868
1120
|
calculateSeverity,
|
|
869
1121
|
createRefactorClusters,
|
|
870
1122
|
detectDuplicatePatterns,
|
|
1123
|
+
filterBrandSpecificVariants,
|
|
871
1124
|
filterBySeverity,
|
|
872
1125
|
filterClustersByImpact,
|
|
873
1126
|
generateSummary,
|
|
874
1127
|
getSeverityLabel,
|
|
875
|
-
getSeverityThreshold,
|
|
876
1128
|
getSmartDefaults,
|
|
877
|
-
groupDuplicatesByFilePair
|
|
1129
|
+
groupDuplicatesByFilePair,
|
|
1130
|
+
logConfiguration
|
|
878
1131
|
});
|