@aiready/consistency 0.16.2 → 0.16.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,251 +1,74 @@
1
- import { readFileContent } from '@aiready/core';
1
+ import { readFileSync } from 'fs';
2
+ import { Severity } from '@aiready/core';
2
3
  import type { PatternIssue } from '../types';
3
4
 
4
5
  /**
5
- * Analyzes code pattern consistency
6
+ * Detect inconsistent code patterns across files
6
7
  */
7
- export async function analyzePatterns(
8
- files: string[]
9
- ): Promise<PatternIssue[]> {
8
+ export async function analyzePatterns(filePaths: string[]): Promise<PatternIssue[]> {
10
9
  const issues: PatternIssue[] = [];
11
-
12
- // Analyze error handling patterns
13
- const errorHandlingIssues = await analyzeErrorHandling(files);
14
- issues.push(...errorHandlingIssues);
15
-
16
- // Analyze async/await patterns
17
- const asyncIssues = await analyzeAsyncPatterns(files);
18
- issues.push(...asyncIssues);
19
-
20
- // Analyze import styles
21
- const importIssues = await analyzeImportStyles(files);
22
- issues.push(...importIssues);
23
-
24
- return issues;
25
- }
26
-
27
- async function analyzeErrorHandling(files: string[]): Promise<PatternIssue[]> {
28
- const patterns = {
29
- tryCatch: [] as string[],
30
- throwsError: [] as string[],
31
- returnsNull: [] as string[],
32
- returnsError: [] as string[],
10
+ const contents = new Map<string, string>();
11
+
12
+ // 1. Error handling style
13
+ const tryCatchPattern = /try\s*\{/g;
14
+ const catchPattern = /catch\s*\(\s*(\w+)\s*\)/g;
15
+
16
+ const styleStats = {
17
+ tryCatch: 0,
18
+ thenCatch: 0,
19
+ asyncAwait: 0,
20
+ commonJs: 0,
21
+ esm: 0,
33
22
  };
34
23
 
35
- for (const file of files) {
36
- const content = await readFileContent(file);
37
-
38
- if (content.includes('try {') || content.includes('} catch')) {
39
- patterns.tryCatch.push(file);
40
- }
41
- if (content.match(/throw new \w*Error/)) {
42
- patterns.throwsError.push(file);
43
- }
44
- if (content.match(/return null/)) {
45
- patterns.returnsNull.push(file);
46
- }
47
- if (content.match(/return \{ error:/)) {
48
- patterns.returnsError.push(file);
24
+ for (const filePath of filePaths) {
25
+ try {
26
+ const content = readFileSync(filePath, 'utf-8');
27
+ contents.set(filePath, content);
28
+
29
+ if (content.match(tryCatchPattern)) styleStats.tryCatch++;
30
+ if (content.match(/\.catch\s*\(/)) styleStats.thenCatch++;
31
+ if (content.match(/\bawait\b/)) styleStats.asyncAwait++;
32
+
33
+ if (content.match(/\brequire\s*\(/)) styleStats.commonJs++;
34
+ if (content.match(/\bimport\b.*\bfrom\b/)) styleStats.esm++;
35
+ } catch (err) {
36
+ void err;
49
37
  }
50
38
  }
51
39
 
52
- const issues: PatternIssue[] = [];
40
+ const totalFiles = filePaths.length;
53
41
 
54
- // Check for mixed error handling strategies
55
- const strategiesUsed = Object.values(patterns).filter(
56
- (p) => p.length > 0
57
- ).length;
58
- if (strategiesUsed > 2) {
42
+ // Report inconsistencies if there's a significant mix
43
+ if (styleStats.tryCatch > 0 && styleStats.thenCatch > 0) {
44
+ const dominant = styleStats.tryCatch >= styleStats.thenCatch ? 'try-catch' : '.catch()';
45
+ const minority = dominant === 'try-catch' ? '.catch()' : 'try-catch';
46
+
59
47
  issues.push({
60
- files: [
61
- ...new Set([
62
- ...patterns.tryCatch,
63
- ...patterns.throwsError,
64
- ...patterns.returnsNull,
65
- ...patterns.returnsError,
66
- ]),
67
- ],
48
+ files: filePaths.filter(f => {
49
+ const c = contents.get(f) || '';
50
+ return minority === 'try-catch' ? c.match(tryCatchPattern) : c.match(/\.catch\s*\(/);
51
+ }),
68
52
  type: 'error-handling',
69
- description: 'Inconsistent error handling strategies across codebase',
70
- examples: [
71
- patterns.tryCatch.length > 0
72
- ? `Try-catch used in ${patterns.tryCatch.length} files`
73
- : '',
74
- patterns.throwsError.length > 0
75
- ? `Throws errors in ${patterns.throwsError.length} files`
76
- : '',
77
- patterns.returnsNull.length > 0
78
- ? `Returns null in ${patterns.returnsNull.length} files`
79
- : '',
80
- patterns.returnsError.length > 0
81
- ? `Returns error objects in ${patterns.returnsError.length} files`
82
- : '',
83
- ].filter((e) => e),
84
- severity: 'major',
85
- });
86
- }
87
-
88
- return issues;
89
- }
90
-
91
- async function analyzeAsyncPatterns(files: string[]): Promise<PatternIssue[]> {
92
- const patterns = {
93
- asyncAwait: [] as string[],
94
- promises: [] as string[],
95
- callbacks: [] as string[],
96
- };
97
-
98
- for (const file of files) {
99
- const content = await readFileContent(file);
100
-
101
- if (content.match(/async\s+(function|\(|[a-zA-Z])/)) {
102
- patterns.asyncAwait.push(file);
103
- }
104
- if (content.match(/\.then\(/) || content.match(/\.catch\(/)) {
105
- patterns.promises.push(file);
106
- }
107
- if (content.match(/callback\s*\(/) || content.match(/\(\s*err\s*,/)) {
108
- patterns.callbacks.push(file);
109
- }
110
- }
111
-
112
- const issues: PatternIssue[] = [];
113
-
114
- // Modern codebases should prefer async/await
115
- if (patterns.callbacks.length > 0 && patterns.asyncAwait.length > 0) {
116
- issues.push({
117
- files: [...patterns.callbacks, ...patterns.asyncAwait],
118
- type: 'async-style',
119
- description: 'Mixed async patterns: callbacks and async/await',
120
- examples: [
121
- `Callbacks found in: ${patterns.callbacks.slice(0, 3).join(', ')}`,
122
- `Async/await used in: ${patterns.asyncAwait.slice(0, 3).join(', ')}`,
123
- ],
124
- severity: 'minor',
125
- });
126
- }
127
-
128
- // Mixing .then() chains with async/await
129
- if (
130
- patterns.promises.length > patterns.asyncAwait.length * 0.3 &&
131
- patterns.asyncAwait.length > 0
132
- ) {
133
- issues.push({
134
- files: patterns.promises,
135
- type: 'async-style',
136
- description:
137
- 'Consider using async/await instead of promise chains for consistency',
138
- examples: patterns.promises.slice(0, 5),
139
- severity: 'info',
53
+ description: `Mixed error handling styles: codebase primarily uses ${dominant}, but found ${minority} in some files.`,
54
+ examples: [dominant, minority],
55
+ severity: Severity.Minor,
140
56
  });
141
57
  }
142
58
 
143
- return issues;
144
- }
145
-
146
- async function analyzeImportStyles(files: string[]): Promise<PatternIssue[]> {
147
- const patterns = {
148
- esModules: [] as string[],
149
- commonJS: [] as string[],
150
- mixed: [] as string[],
151
- };
152
-
153
- for (const file of files) {
154
- const content = await readFileContent(file);
155
- const hasESM = content.match(/^import\s+/m);
156
-
157
- // Check for actual CommonJS require() calls, excluding:
158
- // - String literals: "require('...') or 'require('...')
159
- // - Regex patterns: /require\(/
160
- // - Comments: // require( or /* require( */
161
- const hasCJS = hasActualRequireCalls(content);
162
-
163
- if (hasESM && hasCJS) {
164
- patterns.mixed.push(file);
165
- } else if (hasESM) {
166
- patterns.esModules.push(file);
167
- } else if (hasCJS) {
168
- patterns.commonJS.push(file);
169
- }
170
- }
171
-
172
- const issues: PatternIssue[] = [];
173
-
174
- // Check for mixed import styles in same file
175
- if (patterns.mixed.length > 0) {
59
+ if (styleStats.commonJs > 0 && styleStats.esm > 0) {
60
+ const minority = styleStats.esm >= styleStats.commonJs ? 'CommonJS (require)' : 'ESM (import)';
176
61
  issues.push({
177
- files: patterns.mixed,
62
+ files: filePaths.filter(f => {
63
+ const c = contents.get(f) || '';
64
+ return minority === 'CommonJS (require)' ? c.match(/\brequire\s*\(/) : c.match(/\bimport\b/);
65
+ }),
178
66
  type: 'import-style',
179
- description: 'Mixed ES modules and CommonJS imports in same files',
180
- examples: patterns.mixed.slice(0, 5),
181
- severity: 'major',
67
+ description: `Mixed module systems: found both ESM and CommonJS.`,
68
+ examples: ['import X from "y"', 'const X = require("y")'],
69
+ severity: Severity.Major,
182
70
  });
183
71
  }
184
72
 
185
- // Check for inconsistent styles across project
186
- if (patterns.esModules.length > 0 && patterns.commonJS.length > 0) {
187
- const ratio =
188
- patterns.commonJS.length /
189
- (patterns.esModules.length + patterns.commonJS.length);
190
- if (ratio > 0.2 && ratio < 0.8) {
191
- issues.push({
192
- files: [...patterns.esModules, ...patterns.commonJS],
193
- type: 'import-style',
194
- description: 'Inconsistent import styles across project',
195
- examples: [
196
- `ES modules: ${patterns.esModules.length} files`,
197
- `CommonJS: ${patterns.commonJS.length} files`,
198
- ],
199
- severity: 'minor',
200
- });
201
- }
202
- }
203
-
204
73
  return issues;
205
74
  }
206
-
207
- /**
208
- * Detects actual require() calls, excluding false positives
209
- * Filters out require() in:
210
- * - String literals (single/double/template quotes)
211
- * - Regex patterns
212
- * - Single-line comments (//)
213
- * - Multi-line comments
214
- */
215
- function hasActualRequireCalls(content: string): boolean {
216
- // Simple heuristic: remove obvious false positives
217
- // 1. Remove single-line comments
218
- let cleaned = content.replace(/\/\/.*$/gm, '');
219
-
220
- // 2. Remove multi-line comments (non-greedy)
221
- cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, '');
222
-
223
- // 3. Remove string literals - use simpler regex to avoid backtracking
224
- // Match strings but don't try to be perfect, just remove obvious ones
225
- cleaned = cleaned.replace(/"[^"\n]*"/g, '""');
226
- cleaned = cleaned.replace(/'[^'\n]*'/g, "''");
227
- cleaned = cleaned.replace(/`[^`]*`/g, '``');
228
-
229
- // 4. Simple regex detection: if we see /require in the line, likely a regex pattern
230
- // Remove lines that look like regex patterns with require
231
- cleaned = cleaned.replace(/\/[^/\n]*require[^/\n]*\/[gimsuvy]*/g, '');
232
-
233
- // Now check for require( in the cleaned content
234
- return /require\s*\(/.test(cleaned);
235
- }
236
-
237
- /**
238
- * Analyzes API design consistency
239
- */
240
- export async function analyzeAPIDesign(
241
- files: string[]
242
- ): Promise<PatternIssue[]> {
243
- // This would analyze:
244
- // - Function parameter order consistency
245
- // - Return type patterns
246
- // - Options object vs individual parameters
247
- // For now, return empty array
248
- // Parameter currently unused; reference to avoid lint warnings
249
- void files;
250
- return [];
251
- }
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ScanOptions, AnalysisResult, Issue } from '@aiready/core';
1
+ import type { ScanOptions, AnalysisResult, Issue, Severity, IssueType } from '@aiready/core';
2
2
 
3
3
  export interface ConsistencyOptions extends ScanOptions {
4
4
  /** Check naming conventions and quality */
@@ -8,15 +8,15 @@ export interface ConsistencyOptions extends ScanOptions {
8
8
  /** Check architectural consistency */
9
9
  checkArchitecture?: boolean;
10
10
  /** Minimum severity to report */
11
- minSeverity?: 'info' | 'minor' | 'major' | 'critical';
11
+ minSeverity?: Severity;
12
12
  }
13
13
 
14
14
  export interface ConsistencyIssue extends Issue {
15
15
  type:
16
- | 'naming-inconsistency'
17
- | 'naming-quality'
18
- | 'pattern-inconsistency'
19
- | 'architecture-inconsistency';
16
+ | IssueType.NamingInconsistency
17
+ | IssueType.NamingQuality
18
+ | IssueType.PatternInconsistency
19
+ | IssueType.ArchitectureInconsistency;
20
20
  category: 'naming' | 'patterns' | 'architecture';
21
21
  /** Examples of the inconsistency found */
22
22
  examples?: string[];
@@ -28,10 +28,10 @@ export interface NamingIssue {
28
28
  file: string;
29
29
  line: number;
30
30
  column?: number;
31
- type: 'poor-naming' | 'convention-mix' | 'abbreviation' | 'unclear';
31
+ type: string;
32
32
  identifier: string;
33
33
  suggestion?: string;
34
- severity: 'critical' | 'major' | 'minor' | 'info';
34
+ severity: Severity;
35
35
  category?: 'naming';
36
36
  }
37
37
 
@@ -40,14 +40,14 @@ export interface PatternIssue {
40
40
  type: 'error-handling' | 'async-style' | 'import-style' | 'api-design';
41
41
  description: string;
42
42
  examples: string[];
43
- severity: 'critical' | 'major' | 'minor' | 'info';
43
+ severity: Severity;
44
44
  }
45
45
 
46
46
  export interface ArchitectureIssue {
47
47
  type: 'file-organization' | 'module-structure' | 'export-style';
48
48
  description: string;
49
49
  affectedPaths: string[];
50
- severity: 'critical' | 'major' | 'minor' | 'info';
50
+ severity: Severity;
51
51
  }
52
52
 
53
53
  export interface ConsistencyReport {
@@ -1,5 +1,6 @@
1
1
  import { TSESTree } from '@typescript-eslint/typescript-estree';
2
2
  import { traverseAST } from './ast-parser';
3
+ import { Severity } from '@aiready/core';
3
4
 
4
5
  export type FileType = 'test' | 'production' | 'config' | 'types';
5
6
  export type CodeLayer = 'api' | 'business' | 'data' | 'utility' | 'unknown';
@@ -227,38 +228,50 @@ export function buildCodeContext(
227
228
  * Get context-adjusted severity based on code context
228
229
  */
229
230
  export function adjustSeverity(
230
- baseSeverity: 'info' | 'minor' | 'major' | 'critical',
231
+ baseSeverity: Severity | string,
231
232
  context: CodeContext,
232
233
  issueType: string
233
- ): 'info' | 'minor' | 'major' | 'critical' {
234
+ ): Severity {
235
+ const getEnum = (s: any): Severity => {
236
+ if (s === Severity.Critical || s === 'critical') return Severity.Critical;
237
+ if (s === Severity.Major || s === 'major') return Severity.Major;
238
+ if (s === Severity.Minor || s === 'minor') return Severity.Minor;
239
+ return Severity.Info;
240
+ };
241
+
242
+ let currentSev = getEnum(baseSeverity);
243
+
234
244
  // Test files: Be more lenient
235
245
  if (context.isTestFile) {
236
- if (baseSeverity === 'minor') return 'info';
237
- if (baseSeverity === 'major') return 'minor';
246
+ if (currentSev === Severity.Minor) currentSev = Severity.Info;
247
+ if (currentSev === Severity.Major) currentSev = Severity.Minor;
238
248
  }
239
249
 
240
250
  // Type definition files: Be more lenient (often use short generic names)
241
251
  if (context.isTypeDefinition) {
242
- if (baseSeverity === 'minor') return 'info';
252
+ if (currentSev === Severity.Minor) currentSev = Severity.Info;
243
253
  }
244
254
 
245
255
  // API layer: Be stricter (public interface)
246
256
  if (context.codeLayer === 'api') {
247
- if (baseSeverity === 'info' && issueType === 'unclear') return 'minor';
248
- if (baseSeverity === 'minor' && issueType === 'unclear') return 'major';
257
+ if (currentSev === Severity.Info && issueType === 'unclear')
258
+ currentSev = Severity.Minor;
259
+ if (currentSev === Severity.Minor && issueType === 'unclear')
260
+ currentSev = Severity.Major;
249
261
  }
250
262
 
251
263
  // High complexity: Be stricter (need clearer names)
252
264
  if (context.complexity > 10) {
253
- if (baseSeverity === 'info') return 'minor';
265
+ if (currentSev === Severity.Info) currentSev = Severity.Minor;
254
266
  }
255
267
 
256
268
  // Utility/helper layer: Allow shorter names
257
269
  if (context.codeLayer === 'utility') {
258
- if (baseSeverity === 'minor' && issueType === 'abbreviation') return 'info';
270
+ if (currentSev === Severity.Minor && issueType === 'abbreviation')
271
+ currentSev = Severity.Info;
259
272
  }
260
273
 
261
- return baseSeverity;
274
+ return currentSev;
262
275
  }
263
276
 
264
277
  /**