@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
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectDuplicatePatterns
|
|
3
|
+
} from "./chunk-6VDL7TAS.mjs";
|
|
4
|
+
import {
|
|
5
|
+
calculateSeverity
|
|
6
|
+
} from "./chunk-PQXOORR4.mjs";
|
|
7
|
+
|
|
8
|
+
// src/grouping.ts
|
|
9
|
+
import { Severity, 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
|
+
function isPureInterfaceDefinition(code) {
|
|
128
|
+
const trimmed = code.trim();
|
|
129
|
+
if (!trimmed.startsWith("interface ") && !trimmed.startsWith("type ") && !trimmed.startsWith("export interface ") && !trimmed.startsWith("export type ") && !trimmed.startsWith("enum ") && !trimmed.startsWith("export enum ")) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
if (trimmed.includes("={") || trimmed.includes("=> {") || trimmed.includes("function ") || trimmed.includes("() {") || trimmed.includes(" implements ")) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (trimmed.length > 200) return false;
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
var BRAND_INDICATORS = [
|
|
139
|
+
"cyberpunk",
|
|
140
|
+
"cyber-blue",
|
|
141
|
+
"cyber-purple",
|
|
142
|
+
"slate-900",
|
|
143
|
+
"slate-400",
|
|
144
|
+
"zinc-",
|
|
145
|
+
"indigo-",
|
|
146
|
+
"neon-",
|
|
147
|
+
"glassmorphism",
|
|
148
|
+
"backdrop-blur"
|
|
149
|
+
];
|
|
150
|
+
function isBrandSpecificComponent(filePath) {
|
|
151
|
+
const lower = filePath.toLowerCase();
|
|
152
|
+
const brandingTerms = ["landing", "clawmore", "platform", "apps/"];
|
|
153
|
+
for (const term of brandingTerms) {
|
|
154
|
+
if (lower.includes(term)) return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
function areBrandSpecificVariants(file1, file2, code1, code2) {
|
|
159
|
+
const f1IsBrand = isBrandSpecificComponent(file1);
|
|
160
|
+
const f2IsBrand = isBrandSpecificComponent(file2);
|
|
161
|
+
if (f1IsBrand && f2IsBrand && file1 !== file2) {
|
|
162
|
+
const hasBrandKeyword = (code) => {
|
|
163
|
+
const lowerCode = code.toLowerCase();
|
|
164
|
+
return BRAND_INDICATORS.some((ind) => lowerCode.includes(ind));
|
|
165
|
+
};
|
|
166
|
+
const code1Brand = hasBrandKeyword(code1);
|
|
167
|
+
const code2Brand = hasBrandKeyword(code2);
|
|
168
|
+
if (code1Brand && code2Brand) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
function filterBrandSpecificVariants(duplicates) {
|
|
175
|
+
return duplicates.filter((dup) => {
|
|
176
|
+
if (dup.file1 === dup.file2) return true;
|
|
177
|
+
const isBrandVariant = areBrandSpecificVariants(
|
|
178
|
+
dup.file1,
|
|
179
|
+
dup.file2,
|
|
180
|
+
dup.code1,
|
|
181
|
+
dup.code2
|
|
182
|
+
);
|
|
183
|
+
if (isBrandVariant) {
|
|
184
|
+
dup.severity = Severity.Info;
|
|
185
|
+
dup.suggestion = "Brand-specific themed component variant (intentional)";
|
|
186
|
+
}
|
|
187
|
+
const isInterfaceDef = isPureInterfaceDefinition(dup.code1) && isPureInterfaceDefinition(dup.code2);
|
|
188
|
+
if (isInterfaceDef) {
|
|
189
|
+
dup.severity = Severity.Info;
|
|
190
|
+
dup.suggestion = "Pure interface/type definition - intentional for module independence";
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/analyzer.ts
|
|
197
|
+
import { scanFiles, readFileContent, Severity as Severity2, IssueType as IssueType2 } from "@aiready/core";
|
|
198
|
+
function getRefactoringSuggestion(patternType, similarity) {
|
|
199
|
+
const baseMessages = {
|
|
200
|
+
"api-handler": "Extract common middleware or create a base handler class",
|
|
201
|
+
validator: "Consolidate validation logic into shared schema validators (Zod/Yup)",
|
|
202
|
+
utility: "Move to a shared utilities file and reuse across modules",
|
|
203
|
+
"class-method": "Consider inheritance or composition to share behavior",
|
|
204
|
+
component: "Extract shared logic into a custom hook or HOC",
|
|
205
|
+
function: "Extract into a shared helper function",
|
|
206
|
+
unknown: "Extract common logic into a reusable module"
|
|
207
|
+
};
|
|
208
|
+
const urgency = similarity > 0.95 ? " (CRITICAL: Nearly identical code)" : similarity > 0.9 ? " (HIGH: Very similar, refactor soon)" : "";
|
|
209
|
+
return baseMessages[patternType] + urgency;
|
|
210
|
+
}
|
|
211
|
+
async function getSmartDefaults(directory, userOptions) {
|
|
212
|
+
if (userOptions.useSmartDefaults === false) {
|
|
213
|
+
return {
|
|
214
|
+
rootDir: directory,
|
|
215
|
+
minSimilarity: 0.6,
|
|
216
|
+
minLines: 8,
|
|
217
|
+
batchSize: 100,
|
|
218
|
+
approx: true,
|
|
219
|
+
minSharedTokens: 12,
|
|
220
|
+
maxCandidatesPerBlock: 5,
|
|
221
|
+
streamResults: false,
|
|
222
|
+
severity: "all",
|
|
223
|
+
includeTests: false
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const scanOptions = {
|
|
227
|
+
rootDir: directory,
|
|
228
|
+
include: userOptions.include || ["**/*.{ts,tsx,js,jsx,py,java}"],
|
|
229
|
+
exclude: userOptions.exclude
|
|
230
|
+
};
|
|
231
|
+
const files = await scanFiles(scanOptions);
|
|
232
|
+
const fileCount = files.length;
|
|
233
|
+
const estimatedBlocks = fileCount * 5;
|
|
234
|
+
const minLines = Math.max(
|
|
235
|
+
6,
|
|
236
|
+
Math.min(20, 6 + Math.floor(estimatedBlocks / 1e3) * 2)
|
|
237
|
+
);
|
|
238
|
+
const minSimilarity = Math.min(0.75, 0.45 + estimatedBlocks / 1e4 * 0.3);
|
|
239
|
+
const batchSize = estimatedBlocks > 1e3 ? 200 : 100;
|
|
240
|
+
const severity = estimatedBlocks > 3e3 ? "high" : "all";
|
|
241
|
+
const maxCandidatesPerBlock = Math.max(
|
|
242
|
+
5,
|
|
243
|
+
Math.min(100, Math.floor(1e6 / estimatedBlocks))
|
|
244
|
+
);
|
|
245
|
+
const defaults = {
|
|
246
|
+
rootDir: directory,
|
|
247
|
+
minSimilarity,
|
|
248
|
+
minLines,
|
|
249
|
+
batchSize,
|
|
250
|
+
approx: true,
|
|
251
|
+
minSharedTokens: 10,
|
|
252
|
+
maxCandidatesPerBlock,
|
|
253
|
+
streamResults: false,
|
|
254
|
+
severity,
|
|
255
|
+
includeTests: false
|
|
256
|
+
};
|
|
257
|
+
const result = { ...defaults };
|
|
258
|
+
for (const key of Object.keys(defaults)) {
|
|
259
|
+
if (key in userOptions && userOptions[key] !== void 0) {
|
|
260
|
+
result[key] = userOptions[key];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
function logConfiguration(config, estimatedBlocks) {
|
|
266
|
+
if (config.suppressToolConfig) return;
|
|
267
|
+
console.log("\u{1F4CB} Configuration:");
|
|
268
|
+
console.log(` Repository size: ~${estimatedBlocks} code blocks`);
|
|
269
|
+
console.log(` Similarity threshold: ${config.minSimilarity}`);
|
|
270
|
+
console.log(` Minimum lines: ${config.minLines}`);
|
|
271
|
+
console.log(` Approximate mode: ${config.approx ? "enabled" : "disabled"}`);
|
|
272
|
+
console.log(` Max candidates per block: ${config.maxCandidatesPerBlock}`);
|
|
273
|
+
console.log(` Min shared tokens: ${config.minSharedTokens}`);
|
|
274
|
+
console.log(` Severity filter: ${config.severity}`);
|
|
275
|
+
console.log(` Include tests: ${config.includeTests}`);
|
|
276
|
+
if (config.excludePatterns && config.excludePatterns.length > 0) {
|
|
277
|
+
console.log(` Exclude patterns: ${config.excludePatterns.length} active`);
|
|
278
|
+
}
|
|
279
|
+
if (config.confidenceThreshold && config.confidenceThreshold > 0) {
|
|
280
|
+
console.log(` Confidence threshold: ${config.confidenceThreshold}`);
|
|
281
|
+
}
|
|
282
|
+
if (config.ignoreWhitelist && config.ignoreWhitelist.length > 0) {
|
|
283
|
+
console.log(
|
|
284
|
+
` Ignore whitelist: ${config.ignoreWhitelist.length} entries`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
console.log("");
|
|
288
|
+
}
|
|
289
|
+
async function analyzePatterns(options) {
|
|
290
|
+
const smartDefaults = await getSmartDefaults(options.rootDir || ".", options);
|
|
291
|
+
const finalOptions = { ...smartDefaults, ...options };
|
|
292
|
+
const {
|
|
293
|
+
minSimilarity = 0.4,
|
|
294
|
+
minLines = 5,
|
|
295
|
+
batchSize = 100,
|
|
296
|
+
approx = true,
|
|
297
|
+
minSharedTokens = 8,
|
|
298
|
+
maxCandidatesPerBlock = 100,
|
|
299
|
+
streamResults = false,
|
|
300
|
+
severity = "all",
|
|
301
|
+
groupByFilePair = true,
|
|
302
|
+
createClusters = true,
|
|
303
|
+
minClusterTokenCost = 1e3,
|
|
304
|
+
minClusterFiles = 3,
|
|
305
|
+
excludePatterns = [],
|
|
306
|
+
confidenceThreshold = 0,
|
|
307
|
+
ignoreWhitelist = [],
|
|
308
|
+
...scanOptions
|
|
309
|
+
} = finalOptions;
|
|
310
|
+
const files = await scanFiles(scanOptions);
|
|
311
|
+
const estimatedBlocks = files.length * 3;
|
|
312
|
+
logConfiguration(finalOptions, estimatedBlocks);
|
|
313
|
+
const results = [];
|
|
314
|
+
const READ_BATCH_SIZE = 50;
|
|
315
|
+
const fileContents = [];
|
|
316
|
+
for (let i = 0; i < files.length; i += READ_BATCH_SIZE) {
|
|
317
|
+
const batch = files.slice(i, i + READ_BATCH_SIZE);
|
|
318
|
+
const batchContents = await Promise.all(
|
|
319
|
+
batch.map(async (file) => ({
|
|
320
|
+
file,
|
|
321
|
+
content: await readFileContent(file)
|
|
322
|
+
}))
|
|
323
|
+
);
|
|
324
|
+
fileContents.push(...batchContents);
|
|
325
|
+
}
|
|
326
|
+
const duplicates = await detectDuplicatePatterns(fileContents, {
|
|
327
|
+
minSimilarity,
|
|
328
|
+
minLines,
|
|
329
|
+
batchSize,
|
|
330
|
+
approx,
|
|
331
|
+
minSharedTokens,
|
|
332
|
+
maxCandidatesPerBlock,
|
|
333
|
+
streamResults,
|
|
334
|
+
excludePatterns,
|
|
335
|
+
confidenceThreshold,
|
|
336
|
+
ignoreWhitelist,
|
|
337
|
+
onProgress: options.onProgress
|
|
338
|
+
});
|
|
339
|
+
filterBrandSpecificVariants(duplicates);
|
|
340
|
+
for (const file of files) {
|
|
341
|
+
const fileDuplicates = duplicates.filter(
|
|
342
|
+
(dup) => dup.file1 === file || dup.file2 === file
|
|
343
|
+
);
|
|
344
|
+
const issues = fileDuplicates.map((dup) => {
|
|
345
|
+
const otherFile = dup.file1 === file ? dup.file2 : dup.file1;
|
|
346
|
+
let severity2;
|
|
347
|
+
if (dup.severity === "info" || dup.severity === "Info") {
|
|
348
|
+
severity2 = Severity2.Info;
|
|
349
|
+
} else {
|
|
350
|
+
severity2 = dup.similarity > 0.95 ? Severity2.Critical : dup.similarity > 0.9 ? Severity2.Major : Severity2.Minor;
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
type: IssueType2.DuplicatePattern,
|
|
354
|
+
severity: severity2,
|
|
355
|
+
message: `${dup.patternType} pattern ${Math.round(dup.similarity * 100)}% similar to ${otherFile} (${dup.tokenCost} tokens wasted)`,
|
|
356
|
+
location: {
|
|
357
|
+
file,
|
|
358
|
+
line: dup.file1 === file ? dup.line1 : dup.line2
|
|
359
|
+
},
|
|
360
|
+
suggestion: getRefactoringSuggestion(dup.patternType, dup.similarity)
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
let filteredIssues = issues;
|
|
364
|
+
if (severity !== "all") {
|
|
365
|
+
const severityMap = {
|
|
366
|
+
critical: [Severity2.Critical],
|
|
367
|
+
high: [Severity2.Critical, Severity2.Major],
|
|
368
|
+
medium: [Severity2.Critical, Severity2.Major, Severity2.Minor]
|
|
369
|
+
};
|
|
370
|
+
const allowedSeverities = severityMap[severity] || [Severity2.Critical, Severity2.Major, Severity2.Minor];
|
|
371
|
+
filteredIssues = issues.filter(
|
|
372
|
+
(issue) => allowedSeverities.includes(issue.severity)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const totalTokenCost = fileDuplicates.reduce(
|
|
376
|
+
(sum, dup) => sum + dup.tokenCost,
|
|
377
|
+
0
|
|
378
|
+
);
|
|
379
|
+
results.push({
|
|
380
|
+
fileName: file,
|
|
381
|
+
issues: filteredIssues,
|
|
382
|
+
metrics: {
|
|
383
|
+
tokenCost: totalTokenCost,
|
|
384
|
+
consistencyScore: Math.max(0, 1 - fileDuplicates.length * 0.1)
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
let groups;
|
|
389
|
+
let clusters;
|
|
390
|
+
if (groupByFilePair) {
|
|
391
|
+
groups = groupDuplicatesByFilePair(duplicates);
|
|
392
|
+
}
|
|
393
|
+
if (createClusters) {
|
|
394
|
+
const allClusters = createRefactorClusters(duplicates);
|
|
395
|
+
clusters = filterClustersByImpact(
|
|
396
|
+
allClusters,
|
|
397
|
+
minClusterTokenCost,
|
|
398
|
+
minClusterFiles
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
return { results, duplicates, files, groups, clusters, config: finalOptions };
|
|
402
|
+
}
|
|
403
|
+
function generateSummary(results) {
|
|
404
|
+
const allIssues = results.flatMap((r) => r.issues || []);
|
|
405
|
+
const totalTokenCost = results.reduce(
|
|
406
|
+
(sum, r) => sum + (r.metrics?.tokenCost || 0),
|
|
407
|
+
0
|
|
408
|
+
);
|
|
409
|
+
const patternsByType = {
|
|
410
|
+
"api-handler": 0,
|
|
411
|
+
validator: 0,
|
|
412
|
+
utility: 0,
|
|
413
|
+
"class-method": 0,
|
|
414
|
+
component: 0,
|
|
415
|
+
function: 0,
|
|
416
|
+
unknown: 0
|
|
417
|
+
};
|
|
418
|
+
allIssues.forEach((issue) => {
|
|
419
|
+
const match = issue.message.match(/^(\S+(?:-\S+)*) pattern/);
|
|
420
|
+
if (match) {
|
|
421
|
+
const type = match[1];
|
|
422
|
+
patternsByType[type] = (patternsByType[type] || 0) + 1;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
const topDuplicates = allIssues.slice(0, 10).map((issue) => {
|
|
426
|
+
const similarityMatch = issue.message.match(/(\d+)% similar/);
|
|
427
|
+
const tokenMatch = issue.message.match(/\((\d+) tokens/);
|
|
428
|
+
const typeMatch = issue.message.match(/^(\S+(?:-\S+)*) pattern/);
|
|
429
|
+
const fileMatch = issue.message.match(/similar to (.+?) \(/);
|
|
430
|
+
return {
|
|
431
|
+
files: [
|
|
432
|
+
{
|
|
433
|
+
path: issue.location.file,
|
|
434
|
+
startLine: issue.location.line,
|
|
435
|
+
endLine: 0
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
path: fileMatch?.[1] || "unknown",
|
|
439
|
+
startLine: 0,
|
|
440
|
+
endLine: 0
|
|
441
|
+
}
|
|
442
|
+
],
|
|
443
|
+
similarity: similarityMatch ? parseInt(similarityMatch[1]) / 100 : 0,
|
|
444
|
+
confidence: similarityMatch ? parseInt(similarityMatch[1]) / 100 : 0,
|
|
445
|
+
// Fallback for summary
|
|
446
|
+
patternType: typeMatch?.[1] || "unknown",
|
|
447
|
+
tokenCost: tokenMatch ? parseInt(tokenMatch[1]) : 0
|
|
448
|
+
};
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
totalPatterns: allIssues.length,
|
|
452
|
+
totalTokenCost,
|
|
453
|
+
patternsByType,
|
|
454
|
+
topDuplicates
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export {
|
|
459
|
+
groupDuplicatesByFilePair,
|
|
460
|
+
createRefactorClusters,
|
|
461
|
+
filterClustersByImpact,
|
|
462
|
+
areBrandSpecificVariants,
|
|
463
|
+
filterBrandSpecificVariants,
|
|
464
|
+
getSmartDefaults,
|
|
465
|
+
analyzePatterns,
|
|
466
|
+
generateSummary
|
|
467
|
+
};
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// src/context-rules.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
IssueType,
|
|
4
|
+
getSeverityLabel,
|
|
5
|
+
filterBySeverity,
|
|
6
|
+
Severity
|
|
7
|
+
} from "@aiready/core";
|
|
3
8
|
var CONTEXT_RULES = [
|
|
4
9
|
// Test Fixtures - Intentional duplication for test isolation
|
|
5
10
|
{
|
|
@@ -92,6 +97,53 @@ var CONTEXT_RULES = [
|
|
|
92
97
|
severity: Severity.Info,
|
|
93
98
|
reason: "Tool implementations share structural boilerplate but have distinct business logic",
|
|
94
99
|
suggestion: "Tool duplication is acceptable for boilerplate interface wrappers"
|
|
100
|
+
},
|
|
101
|
+
// Common UI Event Handlers - Boilerplate by nature
|
|
102
|
+
{
|
|
103
|
+
name: "common-ui-handlers",
|
|
104
|
+
detect: (file, code) => {
|
|
105
|
+
const isUIFile = file.includes("/components/") || file.includes(".tsx") || file.includes(".jsx") || file.includes("/hooks/");
|
|
106
|
+
const hasCommonHandler = code.includes("handleClickOutside") || code.includes("handleClickInside") || code.includes("handleEscape") || code.includes("handleKeyDown") || code.includes("handleKeyUp") || code.includes("handleMouseDown") || code.includes("handleMouseUp") || code.includes("handleFocus") || code.includes("handleBlur") || code.includes("handleResize") || code.includes("handleScroll") || code.includes("handleSubmit") || code.includes("handleCancel") || code.includes("handleClose") || code.includes("handleOpen") || code.includes("handleToggle");
|
|
107
|
+
return isUIFile && hasCommonHandler;
|
|
108
|
+
},
|
|
109
|
+
severity: Severity.Minor,
|
|
110
|
+
reason: "Common UI event handlers are boilerplate patterns that repeat across components",
|
|
111
|
+
suggestion: "Consider extracting to shared hooks (useClickOutside, useEscapeKey) only if causing maintenance issues"
|
|
112
|
+
},
|
|
113
|
+
// Utility Functions - Small helpers often intentionally similar
|
|
114
|
+
{
|
|
115
|
+
name: "utility-functions",
|
|
116
|
+
detect: (file, code) => {
|
|
117
|
+
const isUtilFile = file.includes("/utils/") || file.includes("/helpers/") || file.includes("/lib/") || file.endsWith(".util.ts") || file.endsWith(".helper.ts");
|
|
118
|
+
const hasUtilPattern = code.includes("function format") || code.includes("function parse") || code.includes("function validate") || code.includes("function sanitize") || code.includes("function normalize") || code.includes("function convert") || code.includes("function transform") || code.includes("function extract") || code.includes("function build") || code.includes("function create") || code.includes("function get") || code.includes("function set") || code.includes("function is") || code.includes("function has") || code.includes("function can");
|
|
119
|
+
return isUtilFile && hasUtilPattern;
|
|
120
|
+
},
|
|
121
|
+
severity: Severity.Minor,
|
|
122
|
+
reason: "Utility functions are often intentionally similar across modules for clarity",
|
|
123
|
+
suggestion: "Consider extracting to shared utilities only if causing significant duplication"
|
|
124
|
+
},
|
|
125
|
+
// React/Vue Hooks - Standard patterns
|
|
126
|
+
{
|
|
127
|
+
name: "shared-hooks",
|
|
128
|
+
detect: (file, code) => {
|
|
129
|
+
const isHookFile = file.includes("/hooks/") || file.endsWith(".hook.ts") || file.endsWith(".hook.tsx");
|
|
130
|
+
const hasHookPattern = code.includes("function use") || code.includes("export function use") || code.includes("const use") || code.includes("export const use");
|
|
131
|
+
return isHookFile && hasHookPattern;
|
|
132
|
+
},
|
|
133
|
+
severity: Severity.Minor,
|
|
134
|
+
reason: "Hooks follow standard patterns and are often intentionally similar across components",
|
|
135
|
+
suggestion: "Consider extracting common hook logic only if hooks become complex"
|
|
136
|
+
},
|
|
137
|
+
// Validation Functions - Inherently similar patterns
|
|
138
|
+
{
|
|
139
|
+
name: "validation-functions",
|
|
140
|
+
detect: (file, code) => {
|
|
141
|
+
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");
|
|
142
|
+
return hasValidationPattern;
|
|
143
|
+
},
|
|
144
|
+
severity: Severity.Minor,
|
|
145
|
+
reason: "Validation functions are inherently similar and often intentionally duplicated for domain clarity",
|
|
146
|
+
suggestion: "Consider extracting to shared validators only if validation logic becomes complex"
|
|
95
147
|
}
|
|
96
148
|
];
|
|
97
149
|
function calculateSeverity(file1, file2, code, similarity, linesOfCode) {
|
|
@@ -137,29 +189,6 @@ function calculateSeverity(file1, file2, code, similarity, linesOfCode) {
|
|
|
137
189
|
};
|
|
138
190
|
}
|
|
139
191
|
}
|
|
140
|
-
function getSeverityLabel(severity) {
|
|
141
|
-
const labels = {
|
|
142
|
-
[Severity.Critical]: "\u{1F534} CRITICAL",
|
|
143
|
-
[Severity.Major]: "\u{1F7E1} MAJOR",
|
|
144
|
-
[Severity.Minor]: "\u{1F535} MINOR",
|
|
145
|
-
[Severity.Info]: "\u2139\uFE0F INFO"
|
|
146
|
-
};
|
|
147
|
-
return labels[severity];
|
|
148
|
-
}
|
|
149
|
-
function filterBySeverity(duplicates, minSeverity) {
|
|
150
|
-
const severityOrder = [
|
|
151
|
-
Severity.Info,
|
|
152
|
-
Severity.Minor,
|
|
153
|
-
Severity.Major,
|
|
154
|
-
Severity.Critical
|
|
155
|
-
];
|
|
156
|
-
const minIndex = severityOrder.indexOf(minSeverity);
|
|
157
|
-
if (minIndex === -1) return duplicates;
|
|
158
|
-
return duplicates.filter((dup) => {
|
|
159
|
-
const dupIndex = severityOrder.indexOf(dup.severity);
|
|
160
|
-
return dupIndex >= minIndex;
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
192
|
function getSeverityThreshold(severity) {
|
|
164
193
|
const thresholds = {
|
|
165
194
|
[Severity.Critical]: 0.95,
|
|
@@ -171,9 +200,10 @@ function getSeverityThreshold(severity) {
|
|
|
171
200
|
}
|
|
172
201
|
|
|
173
202
|
export {
|
|
174
|
-
|
|
175
|
-
calculateSeverity,
|
|
203
|
+
IssueType,
|
|
176
204
|
getSeverityLabel,
|
|
177
205
|
filterBySeverity,
|
|
206
|
+
CONTEXT_RULES,
|
|
207
|
+
calculateSeverity,
|
|
178
208
|
getSeverityThreshold
|
|
179
209
|
};
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "./chunk-J2G742QF.mjs";
|
|
7
7
|
|
|
8
8
|
// src/grouping.ts
|
|
9
|
-
import { getSeverityLevel } from "@aiready/core";
|
|
9
|
+
import { Severity, getSeverityLevel } from "@aiready/core";
|
|
10
10
|
import path from "path";
|
|
11
11
|
function groupDuplicatesByFilePair(duplicates) {
|
|
12
12
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -124,9 +124,77 @@ function filterClustersByImpact(clusters, minTokenCost = 1e3, minFiles = 3) {
|
|
|
124
124
|
(c) => c.totalTokenCost >= minTokenCost && c.files.length >= minFiles
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
|
+
function isPureInterfaceDefinition(code) {
|
|
128
|
+
const trimmed = code.trim();
|
|
129
|
+
if (!trimmed.startsWith("interface ") && !trimmed.startsWith("type ") && !trimmed.startsWith("export interface ") && !trimmed.startsWith("export type ") && !trimmed.startsWith("enum ") && !trimmed.startsWith("export enum ")) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
if (trimmed.includes("={") || trimmed.includes("=> {") || trimmed.includes("function ") || trimmed.includes("() {") || trimmed.includes(" implements ")) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (trimmed.length > 200) return false;
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
var BRAND_INDICATORS = [
|
|
139
|
+
"cyberpunk",
|
|
140
|
+
"cyber-blue",
|
|
141
|
+
"cyber-purple",
|
|
142
|
+
"slate-900",
|
|
143
|
+
"slate-400",
|
|
144
|
+
"zinc-",
|
|
145
|
+
"indigo-",
|
|
146
|
+
"neon-",
|
|
147
|
+
"glassmorphism",
|
|
148
|
+
"backdrop-blur"
|
|
149
|
+
];
|
|
150
|
+
function isBrandSpecificComponent(filePath) {
|
|
151
|
+
const lower = filePath.toLowerCase();
|
|
152
|
+
const brandingTerms = ["landing", "clawmore", "platform", "apps/"];
|
|
153
|
+
for (const term of brandingTerms) {
|
|
154
|
+
if (lower.includes(term)) return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
function areBrandSpecificVariants(file1, file2, code1, code2) {
|
|
159
|
+
const f1IsBrand = isBrandSpecificComponent(file1);
|
|
160
|
+
const f2IsBrand = isBrandSpecificComponent(file2);
|
|
161
|
+
if (f1IsBrand && f2IsBrand && file1 !== file2) {
|
|
162
|
+
const hasBrandKeyword = (code) => {
|
|
163
|
+
const lowerCode = code.toLowerCase();
|
|
164
|
+
return BRAND_INDICATORS.some((ind) => lowerCode.includes(ind));
|
|
165
|
+
};
|
|
166
|
+
const code1Brand = hasBrandKeyword(code1);
|
|
167
|
+
const code2Brand = hasBrandKeyword(code2);
|
|
168
|
+
if (code1Brand && code2Brand) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
function filterBrandSpecificVariants(duplicates) {
|
|
175
|
+
return duplicates.filter((dup) => {
|
|
176
|
+
if (dup.file1 === dup.file2) return true;
|
|
177
|
+
const isBrandVariant = areBrandSpecificVariants(
|
|
178
|
+
dup.file1,
|
|
179
|
+
dup.file2,
|
|
180
|
+
dup.code1,
|
|
181
|
+
dup.code2
|
|
182
|
+
);
|
|
183
|
+
if (isBrandVariant) {
|
|
184
|
+
dup.severity = Severity.Info;
|
|
185
|
+
dup.suggestion = "Brand-specific themed component variant (intentional)";
|
|
186
|
+
}
|
|
187
|
+
const isInterfaceDef = isPureInterfaceDefinition(dup.code1) && isPureInterfaceDefinition(dup.code2);
|
|
188
|
+
if (isInterfaceDef) {
|
|
189
|
+
dup.severity = Severity.Info;
|
|
190
|
+
dup.suggestion = "Pure interface/type definition - intentional for module independence";
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
});
|
|
194
|
+
}
|
|
127
195
|
|
|
128
196
|
// src/analyzer.ts
|
|
129
|
-
import { scanFiles, readFileContent, Severity as Severity2, IssueType } from "@aiready/core";
|
|
197
|
+
import { scanFiles, readFileContent, Severity as Severity2, IssueType as IssueType2 } from "@aiready/core";
|
|
130
198
|
function getRefactoringSuggestion(patternType, similarity) {
|
|
131
199
|
const baseMessages = {
|
|
132
200
|
"api-handler": "Extract common middleware or create a base handler class",
|
|
@@ -268,15 +336,21 @@ async function analyzePatterns(options) {
|
|
|
268
336
|
ignoreWhitelist,
|
|
269
337
|
onProgress: options.onProgress
|
|
270
338
|
});
|
|
339
|
+
filterBrandSpecificVariants(duplicates);
|
|
271
340
|
for (const file of files) {
|
|
272
341
|
const fileDuplicates = duplicates.filter(
|
|
273
342
|
(dup) => dup.file1 === file || dup.file2 === file
|
|
274
343
|
);
|
|
275
344
|
const issues = fileDuplicates.map((dup) => {
|
|
276
345
|
const otherFile = dup.file1 === file ? dup.file2 : dup.file1;
|
|
277
|
-
|
|
346
|
+
let severity2;
|
|
347
|
+
if (dup.severity === "info" || dup.severity === "Info") {
|
|
348
|
+
severity2 = Severity2.Info;
|
|
349
|
+
} else {
|
|
350
|
+
severity2 = dup.similarity > 0.95 ? Severity2.Critical : dup.similarity > 0.9 ? Severity2.Major : Severity2.Minor;
|
|
351
|
+
}
|
|
278
352
|
return {
|
|
279
|
-
type:
|
|
353
|
+
type: IssueType2.DuplicatePattern,
|
|
280
354
|
severity: severity2,
|
|
281
355
|
message: `${dup.patternType} pattern ${Math.round(dup.similarity * 100)}% similar to ${otherFile} (${dup.tokenCost} tokens wasted)`,
|
|
282
356
|
location: {
|
|
@@ -385,6 +459,8 @@ export {
|
|
|
385
459
|
groupDuplicatesByFilePair,
|
|
386
460
|
createRefactorClusters,
|
|
387
461
|
filterClustersByImpact,
|
|
462
|
+
areBrandSpecificVariants,
|
|
463
|
+
filterBrandSpecificVariants,
|
|
388
464
|
getSmartDefaults,
|
|
389
465
|
analyzePatterns,
|
|
390
466
|
generateSummary
|