@aiready/consistency 0.8.31 → 0.8.34
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/.github/FUNDING.yml +2 -2
- package/.turbo/turbo-build.log +11 -11
- package/.turbo/turbo-lint.log +5 -0
- package/.turbo/turbo-test.log +15 -15
- package/CONTRIBUTING.md +10 -3
- package/dist/chunk-EIQ5K6OO.mjs +1579 -0
- package/dist/chunk-J5IFYDVU.mjs +1579 -0
- package/dist/cli.js +136 -37
- package/dist/cli.mjs +28 -5
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +191 -50
- package/dist/index.mjs +76 -19
- package/package.json +2 -2
- package/src/__tests__/analyzer.test.ts +63 -8
- package/src/__tests__/language-filter.test.ts +6 -9
- package/src/__tests__/scoring.test.ts +35 -10
- package/src/analyzer.ts +60 -29
- package/src/analyzers/naming-ast.ts +133 -45
- package/src/analyzers/naming-constants.ts +356 -42
- package/src/analyzers/naming-python.ts +29 -9
- package/src/analyzers/naming.ts +160 -68
- package/src/analyzers/patterns.ts +59 -35
- package/src/cli.ts +42 -18
- package/src/scoring.ts +35 -31
- package/src/types.ts +2 -1
- package/src/utils/ast-parser.ts +32 -14
- package/src/utils/config-loader.ts +8 -3
- package/src/utils/context-detector.ts +76 -43
- package/src/utils/scope-tracker.ts +11 -11
package/dist/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
detectNamingConventions,
|
|
8
8
|
loadNamingConfig,
|
|
9
9
|
snakeCaseToCamelCase
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-J5IFYDVU.mjs";
|
|
11
11
|
|
|
12
12
|
// src/analyzers/naming.ts
|
|
13
13
|
import { readFileContent } from "@aiready/core";
|
|
@@ -16,7 +16,13 @@ async function analyzeNaming(files) {
|
|
|
16
16
|
const { customAbbreviations, customShortWords, disabledChecks } = await loadNamingConfig(files);
|
|
17
17
|
for (const file of files) {
|
|
18
18
|
const content = await readFileContent(file);
|
|
19
|
-
const fileIssues = analyzeFileNaming(
|
|
19
|
+
const fileIssues = analyzeFileNaming(
|
|
20
|
+
file,
|
|
21
|
+
content,
|
|
22
|
+
customAbbreviations,
|
|
23
|
+
customShortWords,
|
|
24
|
+
disabledChecks
|
|
25
|
+
);
|
|
20
26
|
issues.push(...fileIssues);
|
|
21
27
|
}
|
|
22
28
|
return issues;
|
|
@@ -25,7 +31,10 @@ function analyzeFileNaming(file, content, customAbbreviations, customShortWords,
|
|
|
25
31
|
const issues = [];
|
|
26
32
|
const isTestFile = file.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/);
|
|
27
33
|
const lines = content.split("\n");
|
|
28
|
-
const allAbbreviations = /* @__PURE__ */ new Set([
|
|
34
|
+
const allAbbreviations = /* @__PURE__ */ new Set([
|
|
35
|
+
...ACCEPTABLE_ABBREVIATIONS,
|
|
36
|
+
...customAbbreviations
|
|
37
|
+
]);
|
|
29
38
|
const allShortWords = /* @__PURE__ */ new Set([...COMMON_SHORT_WORDS, ...customShortWords]);
|
|
30
39
|
const getContextWindow = (index, windowSize = 3) => {
|
|
31
40
|
const start = Math.max(0, index - windowSize);
|
|
@@ -49,7 +58,9 @@ function analyzeFileNaming(file, content, customAbbreviations, customShortWords,
|
|
|
49
58
|
const lineNumber = index + 1;
|
|
50
59
|
const contextWindow = getContextWindow(index);
|
|
51
60
|
if (!disabledChecks.has("single-letter")) {
|
|
52
|
-
const singleLetterMatches = line.matchAll(
|
|
61
|
+
const singleLetterMatches = line.matchAll(
|
|
62
|
+
/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi
|
|
63
|
+
);
|
|
53
64
|
for (const match of singleLetterMatches) {
|
|
54
65
|
const letter = match[1].toLowerCase();
|
|
55
66
|
const isCoverageContext = /coverage|summary|metrics|pct|percent/i.test(line) || /\.(?:statements|branches|functions|lines)\.pct/i.test(line);
|
|
@@ -63,7 +74,9 @@ function analyzeFileNaming(file, content, customAbbreviations, customShortWords,
|
|
|
63
74
|
/[a-z]\s*=>/.test(line) || // s => on same line
|
|
64
75
|
// Multi-line arrow function detection: look for pattern in context window
|
|
65
76
|
new RegExp(`\\b${letter}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
|
|
66
|
-
new RegExp(
|
|
77
|
+
new RegExp(
|
|
78
|
+
`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`
|
|
79
|
+
).test(lines[index - 1] || "") && /=>/.test(contextWindow);
|
|
67
80
|
const isShortLived = isShortLivedVariable(letter, index);
|
|
68
81
|
if (!isInLoopContext && !isI18nContext && !isArrowFunctionParam && !isShortLived && !["x", "y", "z", "i", "j", "k", "l", "n", "m"].includes(letter)) {
|
|
69
82
|
if (isTestFile && ["a", "b", "c", "d", "e", "f", "s"].includes(letter)) {
|
|
@@ -81,7 +94,9 @@ function analyzeFileNaming(file, content, customAbbreviations, customShortWords,
|
|
|
81
94
|
}
|
|
82
95
|
}
|
|
83
96
|
if (!disabledChecks.has("abbreviation")) {
|
|
84
|
-
const abbreviationMatches = line.matchAll(
|
|
97
|
+
const abbreviationMatches = line.matchAll(
|
|
98
|
+
/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g
|
|
99
|
+
);
|
|
85
100
|
for (const match of abbreviationMatches) {
|
|
86
101
|
const abbrev = match[1].toLowerCase();
|
|
87
102
|
if (allShortWords.has(abbrev)) {
|
|
@@ -94,7 +109,9 @@ function analyzeFileNaming(file, content, customAbbreviations, customShortWords,
|
|
|
94
109
|
new RegExp(`\\b${abbrev}\\s*=>`).test(line) || // s => on same line
|
|
95
110
|
// Multi-line arrow function: check context window
|
|
96
111
|
new RegExp(`\\b${abbrev}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
|
|
97
|
-
new RegExp(
|
|
112
|
+
new RegExp(
|
|
113
|
+
`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`
|
|
114
|
+
).test(lines[index - 1] || "") && new RegExp(`^\\s*${abbrev}\\s*=>`).test(line);
|
|
98
115
|
if (isArrowFunctionParam) {
|
|
99
116
|
continue;
|
|
100
117
|
}
|
|
@@ -119,8 +136,13 @@ function analyzeFileNaming(file, content, customAbbreviations, customShortWords,
|
|
|
119
136
|
}
|
|
120
137
|
}
|
|
121
138
|
if (!disabledChecks.has("convention-mix") && file.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
122
|
-
const camelCaseVars = line.match(
|
|
123
|
-
|
|
139
|
+
const camelCaseVars = line.match(
|
|
140
|
+
/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*=/
|
|
141
|
+
);
|
|
142
|
+
void camelCaseVars;
|
|
143
|
+
const snakeCaseVars = line.match(
|
|
144
|
+
/\b(?:const|let|var)\s+([a-z][a-z0-9]*_[a-z0-9_]*)\s*=/
|
|
145
|
+
);
|
|
124
146
|
if (snakeCaseVars) {
|
|
125
147
|
issues.push({
|
|
126
148
|
file,
|
|
@@ -133,7 +155,9 @@ function analyzeFileNaming(file, content, customAbbreviations, customShortWords,
|
|
|
133
155
|
}
|
|
134
156
|
}
|
|
135
157
|
if (!disabledChecks.has("unclear")) {
|
|
136
|
-
const booleanMatches = line.matchAll(
|
|
158
|
+
const booleanMatches = line.matchAll(
|
|
159
|
+
/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi
|
|
160
|
+
);
|
|
137
161
|
for (const match of booleanMatches) {
|
|
138
162
|
const name = match[1];
|
|
139
163
|
if (!name.match(/^(is|has|should|can|will|did)/i)) {
|
|
@@ -152,25 +176,59 @@ function analyzeFileNaming(file, content, customAbbreviations, customShortWords,
|
|
|
152
176
|
const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
|
|
153
177
|
for (const match of functionMatches) {
|
|
154
178
|
const name = match[1];
|
|
155
|
-
const isKeyword = [
|
|
179
|
+
const isKeyword = [
|
|
180
|
+
"for",
|
|
181
|
+
"if",
|
|
182
|
+
"else",
|
|
183
|
+
"while",
|
|
184
|
+
"do",
|
|
185
|
+
"switch",
|
|
186
|
+
"case",
|
|
187
|
+
"break",
|
|
188
|
+
"continue",
|
|
189
|
+
"return",
|
|
190
|
+
"throw",
|
|
191
|
+
"try",
|
|
192
|
+
"catch",
|
|
193
|
+
"finally",
|
|
194
|
+
"with",
|
|
195
|
+
"yield",
|
|
196
|
+
"await"
|
|
197
|
+
].includes(name);
|
|
156
198
|
if (isKeyword) {
|
|
157
199
|
continue;
|
|
158
200
|
}
|
|
159
|
-
const isEntryPoint = ["main", "init", "setup", "bootstrap"].includes(
|
|
201
|
+
const isEntryPoint = ["main", "init", "setup", "bootstrap"].includes(
|
|
202
|
+
name
|
|
203
|
+
);
|
|
160
204
|
if (isEntryPoint) {
|
|
161
205
|
continue;
|
|
162
206
|
}
|
|
163
|
-
const isFactoryPattern = name.match(
|
|
207
|
+
const isFactoryPattern = name.match(
|
|
208
|
+
/(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/
|
|
209
|
+
);
|
|
164
210
|
const isEventHandler = name.match(/^on[A-Z]/);
|
|
165
211
|
const isDescriptiveLong = name.length > 15;
|
|
166
212
|
const isReactHook = name.match(/^use[A-Z]/);
|
|
167
|
-
const isDescriptivePattern = name.match(
|
|
213
|
+
const isDescriptivePattern = name.match(
|
|
214
|
+
/^(default|total|count|sum|avg|max|min|initial|current|previous|next)\w+/
|
|
215
|
+
) || name.match(
|
|
216
|
+
/\w+(Count|Total|Sum|Average|List|Map|Set|Config|Settings|Options|Props|Data|Info|Details|State|Status|Response|Result)$/
|
|
217
|
+
);
|
|
168
218
|
const isHelperPattern = name.match(/^(to|from|with|without|for|as|into)\w+/) || // toMetadata, withLogger, forPath
|
|
169
219
|
name.match(/^\w+(To|From|With|Without|For|As|Into)\w*$/);
|
|
170
|
-
const isUtilityName = [
|
|
220
|
+
const isUtilityName = [
|
|
221
|
+
"cn",
|
|
222
|
+
"proxy",
|
|
223
|
+
"sitemap",
|
|
224
|
+
"robots",
|
|
225
|
+
"gtag"
|
|
226
|
+
].includes(name);
|
|
171
227
|
const capitalCount = (name.match(/[A-Z]/g) || []).length;
|
|
172
228
|
const isCompoundWord = capitalCount >= 3;
|
|
173
|
-
const hasActionVerb = name.match(
|
|
229
|
+
const hasActionVerb = name.match(
|
|
230
|
+
/^(get|set|is|has|can|should|create|update|delete|fetch|load|save|process|handle|validate|check|find|search|filter|map|reduce|make|do|run|start|stop|build|parse|format|render|calculate|compute|generate|transform|convert|normalize|sanitize|encode|decode|compress|extract|merge|split|join|sort|compare|test|verify|ensure|apply|execute|invoke|call|emit|dispatch|trigger|listen|subscribe|unsubscribe|add|remove|clear|reset|toggle|enable|disable|open|close|connect|disconnect|send|receive|read|write|import|export|register|unregister|mount|unmount|track|store|persist|upsert|derive|classify|combine|discover|activate|require|assert|expect|mask|escape|sign|put|list|complete|page|safe|mock|pick|pluralize|text)/
|
|
231
|
+
);
|
|
174
232
|
if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isDescriptivePattern && !isCompoundWord && !isHelperPattern && !isUtilityName && !isReactHook) {
|
|
175
233
|
issues.push({
|
|
176
234
|
file,
|
|
@@ -188,10 +246,9 @@ function analyzeFileNaming(file, content, customAbbreviations, customShortWords,
|
|
|
188
246
|
}
|
|
189
247
|
|
|
190
248
|
// src/scoring.ts
|
|
191
|
-
import {
|
|
192
|
-
calculateProductivityImpact
|
|
193
|
-
} from "@aiready/core";
|
|
249
|
+
import { calculateProductivityImpact } from "@aiready/core";
|
|
194
250
|
function calculateConsistencyScore(issues, totalFilesAnalyzed, costConfig) {
|
|
251
|
+
void costConfig;
|
|
195
252
|
const criticalIssues = issues.filter((i) => i.severity === "critical").length;
|
|
196
253
|
const majorIssues = issues.filter((i) => i.severity === "major").length;
|
|
197
254
|
const minorIssues = issues.filter((i) => i.severity === "minor").length;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/consistency",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.34",
|
|
4
4
|
"description": "Detects consistency issues in naming, patterns, and architecture that confuse AI models",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@typescript-eslint/typescript-estree": "^8.53.0",
|
|
44
44
|
"chalk": "^5.3.0",
|
|
45
45
|
"commander": "^14.0.0",
|
|
46
|
-
"@aiready/core": "0.9.
|
|
46
|
+
"@aiready/core": "0.9.35"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^24.0.0",
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { analyzeConsistency } from '../analyzer';
|
|
3
|
-
import { analyzeNaming } from '../analyzers/naming';
|
|
4
3
|
import { analyzePatterns } from '../analyzers/patterns';
|
|
5
4
|
|
|
6
5
|
describe('analyzeConsistency', () => {
|
|
@@ -39,6 +38,8 @@ const x = 10;
|
|
|
39
38
|
const y = 20;
|
|
40
39
|
const result = x + y;
|
|
41
40
|
`;
|
|
41
|
+
void testCode;
|
|
42
|
+
void 0;
|
|
42
43
|
// In a real test, we'd create temp files or mock file reading
|
|
43
44
|
// For now, this is a placeholder structure
|
|
44
45
|
expect(true).toBe(true);
|
|
@@ -47,12 +48,45 @@ const result = x + y;
|
|
|
47
48
|
it('should NOT flag acceptable abbreviations', () => {
|
|
48
49
|
// These should all be acceptable and NOT flagged
|
|
49
50
|
const acceptableAbbreviations = [
|
|
50
|
-
'env',
|
|
51
|
-
'
|
|
52
|
-
'
|
|
51
|
+
'env',
|
|
52
|
+
'req',
|
|
53
|
+
'res',
|
|
54
|
+
'ctx',
|
|
55
|
+
'err',
|
|
56
|
+
'api',
|
|
57
|
+
'url',
|
|
58
|
+
'id',
|
|
59
|
+
'max',
|
|
60
|
+
'min',
|
|
61
|
+
'now',
|
|
62
|
+
'utm',
|
|
63
|
+
'has',
|
|
64
|
+
'is',
|
|
65
|
+
'can',
|
|
66
|
+
'db',
|
|
67
|
+
'fs',
|
|
68
|
+
'os',
|
|
69
|
+
'ui',
|
|
70
|
+
'tmp',
|
|
71
|
+
'src',
|
|
72
|
+
'dst',
|
|
53
73
|
// New additions from Phase 1
|
|
54
|
-
'img',
|
|
55
|
-
'
|
|
74
|
+
'img',
|
|
75
|
+
'txt',
|
|
76
|
+
'doc',
|
|
77
|
+
'md',
|
|
78
|
+
'ts',
|
|
79
|
+
'js',
|
|
80
|
+
'ddb',
|
|
81
|
+
's3',
|
|
82
|
+
'fcp',
|
|
83
|
+
'lcp',
|
|
84
|
+
'fps',
|
|
85
|
+
'po',
|
|
86
|
+
'dto',
|
|
87
|
+
'e2e',
|
|
88
|
+
'a11y',
|
|
89
|
+
'i18n',
|
|
56
90
|
];
|
|
57
91
|
// These abbreviations should not trigger warnings
|
|
58
92
|
expect(acceptableAbbreviations.length).toBeGreaterThan(0);
|
|
@@ -61,8 +95,24 @@ const result = x + y;
|
|
|
61
95
|
it('should NOT flag common short English words', () => {
|
|
62
96
|
// Full words, not abbreviations - should be accepted
|
|
63
97
|
const commonWords = [
|
|
64
|
-
'day',
|
|
65
|
-
'
|
|
98
|
+
'day',
|
|
99
|
+
'key',
|
|
100
|
+
'net',
|
|
101
|
+
'to',
|
|
102
|
+
'go',
|
|
103
|
+
'for',
|
|
104
|
+
'not',
|
|
105
|
+
'new',
|
|
106
|
+
'old',
|
|
107
|
+
'top',
|
|
108
|
+
'end',
|
|
109
|
+
'run',
|
|
110
|
+
'try',
|
|
111
|
+
'use',
|
|
112
|
+
'get',
|
|
113
|
+
'set',
|
|
114
|
+
'add',
|
|
115
|
+
'put',
|
|
66
116
|
];
|
|
67
117
|
// These are full words and should not be flagged as abbreviations
|
|
68
118
|
expect(commonWords.length).toBeGreaterThan(0);
|
|
@@ -73,6 +123,7 @@ const result = x + y;
|
|
|
73
123
|
const user_name = 'John';
|
|
74
124
|
const user_id = 123;
|
|
75
125
|
`;
|
|
126
|
+
void testCode;
|
|
76
127
|
// Test would check for convention-mix issues
|
|
77
128
|
expect(true).toBe(true);
|
|
78
129
|
});
|
|
@@ -82,6 +133,7 @@ const user_id = 123;
|
|
|
82
133
|
const enabled: boolean = true;
|
|
83
134
|
const active: boolean = false;
|
|
84
135
|
`;
|
|
136
|
+
void testCode;
|
|
85
137
|
// Should suggest prefixes like isEnabled, isActive
|
|
86
138
|
expect(true).toBe(true);
|
|
87
139
|
});
|
|
@@ -92,6 +144,7 @@ const id = '123';
|
|
|
92
144
|
const url = 'https://example.com';
|
|
93
145
|
const api = new ApiClient();
|
|
94
146
|
`;
|
|
147
|
+
void testCode;
|
|
95
148
|
// Should not flag these as issues
|
|
96
149
|
expect(true).toBe(true);
|
|
97
150
|
});
|
|
@@ -108,6 +161,7 @@ items.filter(
|
|
|
108
161
|
item.valid
|
|
109
162
|
)
|
|
110
163
|
`;
|
|
164
|
+
void multiLineArrowCode;
|
|
111
165
|
// 's' and 'item' should not be flagged as poor naming
|
|
112
166
|
expect(true).toBe(true);
|
|
113
167
|
});
|
|
@@ -119,6 +173,7 @@ const a = obj1;
|
|
|
119
173
|
const b = obj2;
|
|
120
174
|
return compare(a, b);
|
|
121
175
|
`;
|
|
176
|
+
void shortLivedCode;
|
|
122
177
|
// 'a' and 'b' should not be flagged as they're short-lived
|
|
123
178
|
expect(true).toBe(true);
|
|
124
179
|
});
|
|
@@ -13,10 +13,10 @@ describe('Language File Filtering', () => {
|
|
|
13
13
|
'/path/to/file.tsx',
|
|
14
14
|
'/path/to/file.js',
|
|
15
15
|
'/path/to/file.jsx',
|
|
16
|
-
'/path/to/file.py',
|
|
17
|
-
'/path/to/file.java',
|
|
18
|
-
'/path/to/file.rb',
|
|
19
|
-
'/path/to/file.go',
|
|
16
|
+
'/path/to/file.py', // Should be filtered out
|
|
17
|
+
'/path/to/file.java', // Should be filtered out
|
|
18
|
+
'/path/to/file.rb', // Should be filtered out
|
|
19
|
+
'/path/to/file.go', // Should be filtered out
|
|
20
20
|
];
|
|
21
21
|
|
|
22
22
|
// This test verifies that the function doesn't crash on non-JS/TS files
|
|
@@ -26,10 +26,7 @@ describe('Language File Filtering', () => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
it('should filter out Python files before parsing', async () => {
|
|
29
|
-
const files = [
|
|
30
|
-
'/path/to/script.py',
|
|
31
|
-
'/path/to/another.py',
|
|
32
|
-
];
|
|
29
|
+
const files = ['/path/to/script.py', '/path/to/another.py'];
|
|
33
30
|
|
|
34
31
|
// Should not attempt to parse Python files
|
|
35
32
|
const result = await analyzeNamingAST(files);
|
|
@@ -42,7 +39,7 @@ describe('Language File Filtering', () => {
|
|
|
42
39
|
'test.jsx',
|
|
43
40
|
'test.ts',
|
|
44
41
|
'test.tsx',
|
|
45
|
-
'TEST.JS',
|
|
42
|
+
'TEST.JS', // Test case-insensitive
|
|
46
43
|
'TEST.TS',
|
|
47
44
|
];
|
|
48
45
|
|
|
@@ -52,8 +52,12 @@ describe('Consistency Scoring', () => {
|
|
|
52
52
|
// score = 100 - 3 - 4 = 93
|
|
53
53
|
expect(result.score).toBe(93);
|
|
54
54
|
expect(result.rawMetrics.criticalIssues).toBe(2);
|
|
55
|
-
expect(result.factors.some(f => f.name === 'Critical Issues')).toBe(
|
|
56
|
-
|
|
55
|
+
expect(result.factors.some((f) => f.name === 'Critical Issues')).toBe(
|
|
56
|
+
true
|
|
57
|
+
);
|
|
58
|
+
expect(result.recommendations.some((r) => r.priority === 'high')).toBe(
|
|
59
|
+
true
|
|
60
|
+
);
|
|
57
61
|
});
|
|
58
62
|
|
|
59
63
|
it('should apply weighted severity penalties', () => {
|
|
@@ -84,8 +88,14 @@ describe('Consistency Scoring', () => {
|
|
|
84
88
|
|
|
85
89
|
it('should generate appropriate recommendations', () => {
|
|
86
90
|
const issues: ConsistencyIssue[] = [
|
|
87
|
-
...Array(3).fill({
|
|
88
|
-
|
|
91
|
+
...Array(3).fill({
|
|
92
|
+
severity: 'critical',
|
|
93
|
+
category: 'naming',
|
|
94
|
+
} as ConsistencyIssue),
|
|
95
|
+
...Array(10).fill({
|
|
96
|
+
severity: 'major',
|
|
97
|
+
category: 'patterns',
|
|
98
|
+
} as ConsistencyIssue),
|
|
89
99
|
];
|
|
90
100
|
|
|
91
101
|
const result = calculateConsistencyScore(
|
|
@@ -98,8 +108,12 @@ describe('Consistency Scoring', () => {
|
|
|
98
108
|
);
|
|
99
109
|
|
|
100
110
|
expect(result.recommendations.length).toBeGreaterThan(0);
|
|
101
|
-
expect(
|
|
102
|
-
|
|
111
|
+
expect(
|
|
112
|
+
result.recommendations.some((r) => r.action.includes('critical'))
|
|
113
|
+
).toBe(true);
|
|
114
|
+
expect(
|
|
115
|
+
result.recommendations.some((r) => r.action.includes('naming'))
|
|
116
|
+
).toBe(true);
|
|
103
117
|
});
|
|
104
118
|
|
|
105
119
|
it('should handle edge case of zero files', () => {
|
|
@@ -121,14 +135,25 @@ describe('Consistency Scoring', () => {
|
|
|
121
135
|
|
|
122
136
|
const result = calculateConsistencyScore(issues, 10);
|
|
123
137
|
|
|
124
|
-
expect(
|
|
138
|
+
expect(
|
|
139
|
+
result.recommendations.some((r) => r.action.includes('linter'))
|
|
140
|
+
).toBe(true);
|
|
125
141
|
});
|
|
126
142
|
|
|
127
143
|
it('should combine all penalty types', () => {
|
|
128
144
|
const issues: ConsistencyIssue[] = [
|
|
129
|
-
...Array(10).fill({
|
|
130
|
-
|
|
131
|
-
|
|
145
|
+
...Array(10).fill({
|
|
146
|
+
severity: 'critical',
|
|
147
|
+
category: 'naming',
|
|
148
|
+
} as ConsistencyIssue),
|
|
149
|
+
...Array(20).fill({
|
|
150
|
+
severity: 'major',
|
|
151
|
+
category: 'patterns',
|
|
152
|
+
} as ConsistencyIssue),
|
|
153
|
+
...Array(40).fill({
|
|
154
|
+
severity: 'minor',
|
|
155
|
+
category: 'naming',
|
|
156
|
+
} as ConsistencyIssue),
|
|
132
157
|
];
|
|
133
158
|
|
|
134
159
|
const result = calculateConsistencyScore(
|
package/src/analyzer.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { scanFiles } from '@aiready/core';
|
|
2
2
|
import type { AnalysisResult, Issue } from '@aiready/core';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ConsistencyOptions,
|
|
5
|
+
ConsistencyReport,
|
|
6
|
+
ConsistencyIssue,
|
|
7
|
+
} from './types';
|
|
4
8
|
import { analyzeNamingAST } from './analyzers/naming-ast';
|
|
5
9
|
import { analyzePythonNaming } from './analyzers/naming-python';
|
|
6
10
|
import { analyzePatterns } from './analyzers/patterns';
|
|
@@ -20,21 +24,26 @@ export async function analyzeConsistency(
|
|
|
20
24
|
...scanOptions
|
|
21
25
|
} = options;
|
|
22
26
|
|
|
27
|
+
// Mark intentionally-unused option to avoid lint warnings
|
|
28
|
+
void checkArchitecture;
|
|
29
|
+
|
|
23
30
|
// Scan files
|
|
24
31
|
const filePaths = await scanFiles(scanOptions);
|
|
25
32
|
|
|
26
33
|
// Separate files by language
|
|
27
|
-
const tsJsFiles = filePaths.filter(f => /\.(ts|tsx|js|jsx)$/i.test(f));
|
|
28
|
-
const pythonFiles = filePaths.filter(f => /\.py$/i.test(f));
|
|
34
|
+
const tsJsFiles = filePaths.filter((f) => /\.(ts|tsx|js|jsx)$/i.test(f));
|
|
35
|
+
const pythonFiles = filePaths.filter((f) => /\.py$/i.test(f));
|
|
29
36
|
|
|
30
37
|
// Collect issues by category - now handles multiple languages
|
|
31
38
|
let namingIssues: any[] = [];
|
|
32
39
|
if (checkNaming) {
|
|
33
|
-
const tsJsNamingIssues =
|
|
34
|
-
|
|
40
|
+
const tsJsNamingIssues =
|
|
41
|
+
tsJsFiles.length > 0 ? await analyzeNamingAST(tsJsFiles) : [];
|
|
42
|
+
const pythonNamingIssues =
|
|
43
|
+
pythonFiles.length > 0 ? await analyzePythonNaming(pythonFiles) : [];
|
|
35
44
|
namingIssues = [...tsJsNamingIssues, ...pythonNamingIssues];
|
|
36
45
|
}
|
|
37
|
-
|
|
46
|
+
|
|
38
47
|
const patternIssues = checkPatterns ? await analyzePatterns(filePaths) : [];
|
|
39
48
|
|
|
40
49
|
// Convert to AnalysisResult format
|
|
@@ -48,16 +57,19 @@ export async function analyzeConsistency(
|
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
const consistencyIssue: ConsistencyIssue = {
|
|
51
|
-
type:
|
|
60
|
+
type:
|
|
61
|
+
issue.type === 'convention-mix'
|
|
62
|
+
? 'naming-inconsistency'
|
|
63
|
+
: 'naming-quality',
|
|
52
64
|
category: 'naming',
|
|
53
65
|
severity: issue.severity,
|
|
54
66
|
message: `${issue.type}: ${issue.identifier}`,
|
|
55
67
|
location: {
|
|
56
68
|
file: issue.file,
|
|
57
69
|
line: issue.line,
|
|
58
|
-
column: issue.column
|
|
70
|
+
column: issue.column,
|
|
59
71
|
},
|
|
60
|
-
suggestion: issue.suggestion
|
|
72
|
+
suggestion: issue.suggestion,
|
|
61
73
|
};
|
|
62
74
|
|
|
63
75
|
if (!fileIssuesMap.has(issue.file)) {
|
|
@@ -79,10 +91,10 @@ export async function analyzeConsistency(
|
|
|
79
91
|
message: issue.description,
|
|
80
92
|
location: {
|
|
81
93
|
file: issue.files[0] || 'multiple files',
|
|
82
|
-
line: 1
|
|
94
|
+
line: 1,
|
|
83
95
|
},
|
|
84
96
|
examples: issue.examples,
|
|
85
|
-
suggestion: `Standardize ${issue.type} patterns across ${issue.files.length} files
|
|
97
|
+
suggestion: `Standardize ${issue.type} patterns across ${issue.files.length} files`,
|
|
86
98
|
};
|
|
87
99
|
|
|
88
100
|
// Add to first file in the pattern
|
|
@@ -101,28 +113,32 @@ export async function analyzeConsistency(
|
|
|
101
113
|
fileName,
|
|
102
114
|
issues: issues as Issue[],
|
|
103
115
|
metrics: {
|
|
104
|
-
consistencyScore: calculateConsistencyScore(issues)
|
|
105
|
-
}
|
|
116
|
+
consistencyScore: calculateConsistencyScore(issues),
|
|
117
|
+
},
|
|
106
118
|
});
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
// Sort results by severity first, then by issue count per file
|
|
110
122
|
results.sort((fileResultA, fileResultB) => {
|
|
111
123
|
const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
|
|
112
|
-
|
|
124
|
+
|
|
113
125
|
// Get highest severity in each file
|
|
114
126
|
const maxSeverityA = Math.min(
|
|
115
|
-
...fileResultA.issues.map(
|
|
127
|
+
...fileResultA.issues.map(
|
|
128
|
+
(i) => severityOrder[(i as ConsistencyIssue).severity]
|
|
129
|
+
)
|
|
116
130
|
);
|
|
117
131
|
const maxSeverityB = Math.min(
|
|
118
|
-
...fileResultB.issues.map(
|
|
132
|
+
...fileResultB.issues.map(
|
|
133
|
+
(i) => severityOrder[(i as ConsistencyIssue).severity]
|
|
134
|
+
)
|
|
119
135
|
);
|
|
120
|
-
|
|
136
|
+
|
|
121
137
|
// Sort by severity first
|
|
122
138
|
if (maxSeverityA !== maxSeverityB) {
|
|
123
139
|
return maxSeverityA - maxSeverityB;
|
|
124
140
|
}
|
|
125
|
-
|
|
141
|
+
|
|
126
142
|
// Then by issue count (descending)
|
|
127
143
|
return fileResultB.issues.length - fileResultA.issues.length;
|
|
128
144
|
});
|
|
@@ -131,8 +147,12 @@ export async function analyzeConsistency(
|
|
|
131
147
|
const recommendations = generateRecommendations(namingIssues, patternIssues);
|
|
132
148
|
|
|
133
149
|
// Compute filtered counts (respecting minSeverity) to report accurate summary
|
|
134
|
-
const namingCountFiltered = namingIssues.filter(i =>
|
|
135
|
-
|
|
150
|
+
const namingCountFiltered = namingIssues.filter((i) =>
|
|
151
|
+
shouldIncludeSeverity(i.severity, minSeverity)
|
|
152
|
+
).length;
|
|
153
|
+
const patternCountFiltered = patternIssues.filter((i) =>
|
|
154
|
+
shouldIncludeSeverity(i.severity, minSeverity)
|
|
155
|
+
).length;
|
|
136
156
|
|
|
137
157
|
// Detect naming conventions (TODO: re-implement for AST version)
|
|
138
158
|
// const conventionAnalysis = detectNamingConventions(filePaths, namingIssues);
|
|
@@ -143,10 +163,10 @@ export async function analyzeConsistency(
|
|
|
143
163
|
namingIssues: namingCountFiltered,
|
|
144
164
|
patternIssues: patternCountFiltered,
|
|
145
165
|
architectureIssues: 0,
|
|
146
|
-
filesAnalyzed: filePaths.length
|
|
166
|
+
filesAnalyzed: filePaths.length,
|
|
147
167
|
},
|
|
148
168
|
results,
|
|
149
|
-
recommendations
|
|
169
|
+
recommendations,
|
|
150
170
|
};
|
|
151
171
|
}
|
|
152
172
|
|
|
@@ -161,7 +181,10 @@ function shouldIncludeSeverity(
|
|
|
161
181
|
function calculateConsistencyScore(issues: ConsistencyIssue[]): number {
|
|
162
182
|
// Higher score = more consistent (fewer issues)
|
|
163
183
|
const weights = { critical: 10, major: 5, minor: 2, info: 1 };
|
|
164
|
-
const totalWeight = issues.reduce(
|
|
184
|
+
const totalWeight = issues.reduce(
|
|
185
|
+
(sum, issue) => sum + weights[issue.severity],
|
|
186
|
+
0
|
|
187
|
+
);
|
|
165
188
|
// Score from 0-1, where 1 is perfect
|
|
166
189
|
return Math.max(0, 1 - totalWeight / 100);
|
|
167
190
|
}
|
|
@@ -173,14 +196,18 @@ function generateRecommendations(
|
|
|
173
196
|
const recommendations: string[] = [];
|
|
174
197
|
|
|
175
198
|
if (namingIssues.length > 0) {
|
|
176
|
-
const conventionMixCount = namingIssues.filter(
|
|
199
|
+
const conventionMixCount = namingIssues.filter(
|
|
200
|
+
(i) => i.type === 'convention-mix'
|
|
201
|
+
).length;
|
|
177
202
|
if (conventionMixCount > 0) {
|
|
178
203
|
recommendations.push(
|
|
179
204
|
`Standardize naming conventions: Found ${conventionMixCount} snake_case variables in TypeScript/JavaScript (use camelCase)`
|
|
180
205
|
);
|
|
181
206
|
}
|
|
182
207
|
|
|
183
|
-
const poorNamingCount = namingIssues.filter(
|
|
208
|
+
const poorNamingCount = namingIssues.filter(
|
|
209
|
+
(i) => i.type === 'poor-naming'
|
|
210
|
+
).length;
|
|
184
211
|
if (poorNamingCount > 0) {
|
|
185
212
|
recommendations.push(
|
|
186
213
|
`Improve variable naming: Found ${poorNamingCount} single-letter or unclear variable names`
|
|
@@ -189,21 +216,23 @@ function generateRecommendations(
|
|
|
189
216
|
}
|
|
190
217
|
|
|
191
218
|
if (patternIssues.length > 0) {
|
|
192
|
-
const errorHandlingIssues = patternIssues.filter(
|
|
219
|
+
const errorHandlingIssues = patternIssues.filter(
|
|
220
|
+
(i) => i.type === 'error-handling'
|
|
221
|
+
);
|
|
193
222
|
if (errorHandlingIssues.length > 0) {
|
|
194
223
|
recommendations.push(
|
|
195
224
|
'Standardize error handling strategy across the codebase (prefer try-catch with typed errors)'
|
|
196
225
|
);
|
|
197
226
|
}
|
|
198
227
|
|
|
199
|
-
const asyncIssues = patternIssues.filter(i => i.type === 'async-style');
|
|
228
|
+
const asyncIssues = patternIssues.filter((i) => i.type === 'async-style');
|
|
200
229
|
if (asyncIssues.length > 0) {
|
|
201
230
|
recommendations.push(
|
|
202
231
|
'Use async/await consistently instead of mixing with promise chains or callbacks'
|
|
203
232
|
);
|
|
204
233
|
}
|
|
205
234
|
|
|
206
|
-
const importIssues = patternIssues.filter(i => i.type === 'import-style');
|
|
235
|
+
const importIssues = patternIssues.filter((i) => i.type === 'import-style');
|
|
207
236
|
if (importIssues.length > 0) {
|
|
208
237
|
recommendations.push(
|
|
209
238
|
'Use ES modules consistently across the project (avoid mixing with CommonJS)'
|
|
@@ -212,7 +241,9 @@ function generateRecommendations(
|
|
|
212
241
|
}
|
|
213
242
|
|
|
214
243
|
if (recommendations.length === 0) {
|
|
215
|
-
recommendations.push(
|
|
244
|
+
recommendations.push(
|
|
245
|
+
'No major consistency issues found! Your codebase follows good practices.'
|
|
246
|
+
);
|
|
216
247
|
}
|
|
217
248
|
|
|
218
249
|
return recommendations;
|