@aiready/consistency 0.4.1 → 0.6.0

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.
@@ -0,0 +1,278 @@
1
+ import { TSESTree } from '@typescript-eslint/typescript-estree';
2
+ import { traverseAST } from './ast-parser';
3
+
4
+ export type FileType = 'test' | 'production' | 'config' | 'types';
5
+ export type CodeLayer = 'api' | 'business' | 'data' | 'utility' | 'unknown';
6
+
7
+ export interface CodeContext {
8
+ fileType: FileType;
9
+ codeLayer: CodeLayer;
10
+ complexity: number;
11
+ isTestFile: boolean;
12
+ isTypeDefinition: boolean;
13
+ }
14
+
15
+ /**
16
+ * Detect the file type based on file path and content
17
+ */
18
+ export function detectFileType(filePath: string, ast: TSESTree.Program): FileType {
19
+ const path = filePath.toLowerCase();
20
+
21
+ // Test files
22
+ if (path.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/) || path.includes('__tests__')) {
23
+ return 'test';
24
+ }
25
+
26
+ // Type definition files
27
+ if (path.endsWith('.d.ts') || path.includes('types')) {
28
+ return 'types';
29
+ }
30
+
31
+ // Config files
32
+ if (path.match(/config|\.config\.|rc\.|setup/) || path.includes('configuration')) {
33
+ return 'config';
34
+ }
35
+
36
+ return 'production';
37
+ }
38
+
39
+ /**
40
+ * Detect the code layer based on imports and exports
41
+ */
42
+ export function detectCodeLayer(ast: TSESTree.Program): CodeLayer {
43
+ let hasAPIIndicators = 0;
44
+ let hasBusinessIndicators = 0;
45
+ let hasDataIndicators = 0;
46
+ let hasUtilityIndicators = 0;
47
+
48
+ traverseAST(ast, {
49
+ enter: (node) => {
50
+ // Check imports
51
+ if (node.type === 'ImportDeclaration') {
52
+ const source = node.source.value as string;
53
+
54
+ if (source.match(/express|fastify|koa|@nestjs|axios|fetch|http/i)) {
55
+ hasAPIIndicators++;
56
+ }
57
+ if (source.match(/database|prisma|typeorm|sequelize|mongoose|pg|mysql/i)) {
58
+ hasDataIndicators++;
59
+ }
60
+ }
61
+
62
+ // Check function names for layer indicators
63
+ if (node.type === 'FunctionDeclaration' && node.id) {
64
+ const name = node.id.name;
65
+
66
+ // API layer patterns
67
+ if (name.match(/^(get|post|put|delete|patch|handle|api|route|controller)/i)) {
68
+ hasAPIIndicators++;
69
+ }
70
+
71
+ // Business logic patterns
72
+ if (name.match(/^(calculate|process|validate|transform|compute|analyze)/i)) {
73
+ hasBusinessIndicators++;
74
+ }
75
+
76
+ // Data layer patterns
77
+ if (name.match(/^(find|create|update|delete|save|fetch|query|insert)/i)) {
78
+ hasDataIndicators++;
79
+ }
80
+
81
+ // Utility patterns
82
+ if (name.match(/^(format|parse|convert|normalize|sanitize|encode|decode)/i)) {
83
+ hasUtilityIndicators++;
84
+ }
85
+ }
86
+
87
+ // Check for exports
88
+ if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
89
+ // Functions exported with "api", "handler", "route" suggest API layer
90
+ if (node.type === 'ExportNamedDeclaration' && node.declaration) {
91
+ if (node.declaration.type === 'FunctionDeclaration' && node.declaration.id) {
92
+ const name = node.declaration.id.name;
93
+ if (name.match(/handler|route|api|controller/i)) {
94
+ hasAPIIndicators += 2; // Stronger signal
95
+ }
96
+ }
97
+ }
98
+ }
99
+ },
100
+ });
101
+
102
+ // Determine layer based on indicators
103
+ const scores = {
104
+ api: hasAPIIndicators,
105
+ business: hasBusinessIndicators,
106
+ data: hasDataIndicators,
107
+ utility: hasUtilityIndicators,
108
+ };
109
+
110
+ const maxScore = Math.max(...Object.values(scores));
111
+ if (maxScore === 0) {
112
+ return 'unknown';
113
+ }
114
+
115
+ // Return the layer with highest score
116
+ if (scores.api === maxScore) return 'api';
117
+ if (scores.data === maxScore) return 'data';
118
+ if (scores.business === maxScore) return 'business';
119
+ if (scores.utility === maxScore) return 'utility';
120
+
121
+ return 'unknown';
122
+ }
123
+
124
+ /**
125
+ * Calculate cyclomatic complexity for a function
126
+ */
127
+ export function calculateComplexity(node: TSESTree.Node): number {
128
+ let complexity = 1; // Base complexity
129
+
130
+ traverseAST(node, {
131
+ enter: (childNode) => {
132
+ // Each decision point adds 1 to complexity
133
+ switch (childNode.type) {
134
+ case 'IfStatement':
135
+ case 'ConditionalExpression': // ternary
136
+ case 'SwitchCase':
137
+ case 'ForStatement':
138
+ case 'ForInStatement':
139
+ case 'ForOfStatement':
140
+ case 'WhileStatement':
141
+ case 'DoWhileStatement':
142
+ case 'CatchClause':
143
+ complexity++;
144
+ break;
145
+ case 'LogicalExpression':
146
+ // && and || add complexity
147
+ if (childNode.operator === '&&' || childNode.operator === '||') {
148
+ complexity++;
149
+ }
150
+ break;
151
+ }
152
+ },
153
+ });
154
+
155
+ return complexity;
156
+ }
157
+
158
+ /**
159
+ * Build a complete context for a file
160
+ */
161
+ export function buildCodeContext(filePath: string, ast: TSESTree.Program): CodeContext {
162
+ const fileType = detectFileType(filePath, ast);
163
+ const codeLayer = detectCodeLayer(ast);
164
+
165
+ // Calculate average complexity of functions in file
166
+ let totalComplexity = 0;
167
+ let functionCount = 0;
168
+
169
+ traverseAST(ast, {
170
+ enter: (node) => {
171
+ if (
172
+ node.type === 'FunctionDeclaration' ||
173
+ node.type === 'FunctionExpression' ||
174
+ node.type === 'ArrowFunctionExpression'
175
+ ) {
176
+ totalComplexity += calculateComplexity(node);
177
+ functionCount++;
178
+ }
179
+ },
180
+ });
181
+
182
+ const avgComplexity = functionCount > 0 ? totalComplexity / functionCount : 1;
183
+
184
+ return {
185
+ fileType,
186
+ codeLayer,
187
+ complexity: Math.round(avgComplexity),
188
+ isTestFile: fileType === 'test',
189
+ isTypeDefinition: fileType === 'types',
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Get context-adjusted severity based on code context
195
+ */
196
+ export function adjustSeverity(
197
+ baseSeverity: 'info' | 'minor' | 'major' | 'critical',
198
+ context: CodeContext,
199
+ issueType: string
200
+ ): 'info' | 'minor' | 'major' | 'critical' {
201
+ // Test files: Be more lenient
202
+ if (context.isTestFile) {
203
+ if (baseSeverity === 'minor') return 'info';
204
+ if (baseSeverity === 'major') return 'minor';
205
+ }
206
+
207
+ // Type definition files: Be more lenient (often use short generic names)
208
+ if (context.isTypeDefinition) {
209
+ if (baseSeverity === 'minor') return 'info';
210
+ }
211
+
212
+ // API layer: Be stricter (public interface)
213
+ if (context.codeLayer === 'api') {
214
+ if (baseSeverity === 'info' && issueType === 'unclear') return 'minor';
215
+ if (baseSeverity === 'minor' && issueType === 'unclear') return 'major';
216
+ }
217
+
218
+ // High complexity: Be stricter (need clearer names)
219
+ if (context.complexity > 10) {
220
+ if (baseSeverity === 'info') return 'minor';
221
+ }
222
+
223
+ // Utility/helper layer: Allow shorter names
224
+ if (context.codeLayer === 'utility') {
225
+ if (baseSeverity === 'minor' && issueType === 'abbreviation') return 'info';
226
+ }
227
+
228
+ return baseSeverity;
229
+ }
230
+
231
+ /**
232
+ * Check if a short variable name is acceptable in this context
233
+ */
234
+ export function isAcceptableInContext(
235
+ name: string,
236
+ context: CodeContext,
237
+ options: {
238
+ isLoopVariable?: boolean;
239
+ isParameter?: boolean;
240
+ isDestructured?: boolean;
241
+ complexity?: number;
242
+ }
243
+ ): boolean {
244
+ // Loop variables always acceptable
245
+ if (options.isLoopVariable && ['i', 'j', 'k', 'l', 'n', 'm'].includes(name)) {
246
+ return true;
247
+ }
248
+
249
+ // Test files: More lenient
250
+ if (context.isTestFile) {
251
+ // Common test patterns: a/b for comparison, x/y for coordinates
252
+ if (['a', 'b', 'c', 'x', 'y', 'z'].includes(name) && options.isParameter) {
253
+ return true;
254
+ }
255
+ }
256
+
257
+ // Math/graphics context: x, y, z acceptable
258
+ if (context.codeLayer === 'utility' && ['x', 'y', 'z'].includes(name)) {
259
+ return true;
260
+ }
261
+
262
+ // Destructured from well-named source: More lenient
263
+ if (options.isDestructured) {
264
+ // Coverage metrics s/b/f/l always acceptable when destructured
265
+ if (['s', 'b', 'f', 'l'].includes(name)) {
266
+ return true;
267
+ }
268
+ }
269
+
270
+ // Simple functions (complexity < 3): Allow short parameter names
271
+ if (options.isParameter && (options.complexity ?? context.complexity) < 3) {
272
+ if (name.length >= 2) {
273
+ return true; // Two-letter names OK in simple functions
274
+ }
275
+ }
276
+
277
+ return false;
278
+ }
@@ -0,0 +1,221 @@
1
+ import { TSESTree } from '@typescript-eslint/typescript-estree';
2
+
3
+ export type ScopeType = 'global' | 'function' | 'block' | 'loop' | 'class';
4
+
5
+ export interface VariableInfo {
6
+ name: string;
7
+ node: TSESTree.Node;
8
+ declarationLine: number;
9
+ references: TSESTree.Node[];
10
+ type?: TSESTree.TypeNode | null;
11
+ isParameter: boolean;
12
+ isDestructured: boolean;
13
+ isLoopVariable: boolean;
14
+ }
15
+
16
+ export interface Scope {
17
+ type: ScopeType;
18
+ node: TSESTree.Node;
19
+ parent: Scope | null;
20
+ children: Scope[];
21
+ variables: Map<string, VariableInfo>;
22
+ }
23
+
24
+ export class ScopeTracker {
25
+ private currentScope: Scope;
26
+ private readonly rootScope: Scope;
27
+ private readonly allScopes: Scope[] = [];
28
+
29
+ constructor(rootNode: TSESTree.Program) {
30
+ this.rootScope = {
31
+ type: 'global',
32
+ node: rootNode,
33
+ parent: null,
34
+ children: [],
35
+ variables: new Map(),
36
+ };
37
+ this.currentScope = this.rootScope;
38
+ this.allScopes.push(this.rootScope);
39
+ }
40
+
41
+ /**
42
+ * Enter a new scope
43
+ */
44
+ enterScope(type: ScopeType, node: TSESTree.Node): void {
45
+ const newScope: Scope = {
46
+ type,
47
+ node,
48
+ parent: this.currentScope,
49
+ children: [],
50
+ variables: new Map(),
51
+ };
52
+
53
+ this.currentScope.children.push(newScope);
54
+ this.currentScope = newScope;
55
+ this.allScopes.push(newScope);
56
+ }
57
+
58
+ /**
59
+ * Exit current scope and return to parent
60
+ */
61
+ exitScope(): void {
62
+ if (this.currentScope.parent) {
63
+ this.currentScope = this.currentScope.parent;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Declare a variable in the current scope
69
+ */
70
+ declareVariable(
71
+ name: string,
72
+ node: TSESTree.Node,
73
+ line: number,
74
+ options: {
75
+ type?: TSESTree.TypeNode | null;
76
+ isParameter?: boolean;
77
+ isDestructured?: boolean;
78
+ isLoopVariable?: boolean;
79
+ } = {}
80
+ ): void {
81
+ const varInfo: VariableInfo = {
82
+ name,
83
+ node,
84
+ declarationLine: line,
85
+ references: [],
86
+ type: options.type,
87
+ isParameter: options.isParameter ?? false,
88
+ isDestructured: options.isDestructured ?? false,
89
+ isLoopVariable: options.isLoopVariable ?? false,
90
+ };
91
+
92
+ this.currentScope.variables.set(name, varInfo);
93
+ }
94
+
95
+ /**
96
+ * Add a reference to a variable
97
+ */
98
+ addReference(name: string, node: TSESTree.Node): void {
99
+ const varInfo = this.findVariable(name);
100
+ if (varInfo) {
101
+ varInfo.references.push(node);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Find a variable in current or parent scopes
107
+ */
108
+ findVariable(name: string): VariableInfo | null {
109
+ let scope: Scope | null = this.currentScope;
110
+
111
+ while (scope) {
112
+ const varInfo = scope.variables.get(name);
113
+ if (varInfo) {
114
+ return varInfo;
115
+ }
116
+ scope = scope.parent;
117
+ }
118
+
119
+ return null;
120
+ }
121
+
122
+ /**
123
+ * Get all variables in current scope (not including parent scopes)
124
+ */
125
+ getCurrentScopeVariables(): VariableInfo[] {
126
+ return Array.from(this.currentScope.variables.values());
127
+ }
128
+
129
+ /**
130
+ * Get all variables across all scopes
131
+ */
132
+ getAllVariables(): VariableInfo[] {
133
+ const allVars: VariableInfo[] = [];
134
+
135
+ for (const scope of this.allScopes) {
136
+ allVars.push(...Array.from(scope.variables.values()));
137
+ }
138
+
139
+ return allVars;
140
+ }
141
+
142
+ /**
143
+ * Calculate actual usage count (references minus declaration)
144
+ */
145
+ getUsageCount(varInfo: VariableInfo): number {
146
+ return varInfo.references.length;
147
+ }
148
+
149
+ /**
150
+ * Check if a variable is short-lived (used within N lines)
151
+ */
152
+ isShortLived(varInfo: VariableInfo, maxLines: number = 5): boolean {
153
+ if (varInfo.references.length === 0) {
154
+ return false; // Unused variable
155
+ }
156
+
157
+ const declarationLine = varInfo.declarationLine;
158
+ const maxUsageLine = Math.max(
159
+ ...varInfo.references.map(ref => ref.loc?.start.line ?? declarationLine)
160
+ );
161
+
162
+ return (maxUsageLine - declarationLine) <= maxLines;
163
+ }
164
+
165
+ /**
166
+ * Check if a variable is used in a limited scope (e.g., only in one callback)
167
+ */
168
+ isLocallyScoped(varInfo: VariableInfo): boolean {
169
+ // If all references are within a small scope (e.g., arrow function), it's locally scoped
170
+ if (varInfo.references.length === 0) return false;
171
+
172
+ // Check if usage span is small
173
+ const lines = varInfo.references.map(ref => ref.loc?.start.line ?? 0);
174
+ const minLine = Math.min(...lines);
175
+ const maxLine = Math.max(...lines);
176
+
177
+ return (maxLine - minLine) <= 3;
178
+ }
179
+
180
+ /**
181
+ * Get current scope type
182
+ */
183
+ getCurrentScopeType(): ScopeType {
184
+ return this.currentScope.type;
185
+ }
186
+
187
+ /**
188
+ * Check if currently in a loop scope
189
+ */
190
+ isInLoop(): boolean {
191
+ let scope: Scope | null = this.currentScope;
192
+ while (scope) {
193
+ if (scope.type === 'loop') {
194
+ return true;
195
+ }
196
+ scope = scope.parent;
197
+ }
198
+ return false;
199
+ }
200
+
201
+ /**
202
+ * Check if currently in a function scope
203
+ */
204
+ isInFunction(): boolean {
205
+ let scope: Scope | null = this.currentScope;
206
+ while (scope) {
207
+ if (scope.type === 'function') {
208
+ return true;
209
+ }
210
+ scope = scope.parent;
211
+ }
212
+ return false;
213
+ }
214
+
215
+ /**
216
+ * Get the root scope
217
+ */
218
+ getRootScope(): Scope {
219
+ return this.rootScope;
220
+ }
221
+ }