@aiready/consistency 0.2.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,182 @@
1
+ import { scanFiles } from '@aiready/core';
2
+ import type { AnalysisResult, Issue } from '@aiready/core';
3
+ import type { ConsistencyOptions, ConsistencyReport, ConsistencyIssue } from './types';
4
+ import { analyzeNaming, detectNamingConventions } from './analyzers/naming';
5
+ import { analyzePatterns } from './analyzers/patterns';
6
+
7
+ /**
8
+ * Main consistency analyzer that orchestrates all analysis types
9
+ */
10
+ export async function analyzeConsistency(
11
+ options: ConsistencyOptions
12
+ ): Promise<ConsistencyReport> {
13
+ const {
14
+ checkNaming = true,
15
+ checkPatterns = true,
16
+ checkArchitecture = false, // Not implemented yet
17
+ minSeverity = 'info',
18
+ ...scanOptions
19
+ } = options;
20
+
21
+ // Scan files
22
+ const filePaths = await scanFiles(scanOptions);
23
+
24
+ // Collect issues by category
25
+ const namingIssues = checkNaming ? await analyzeNaming(filePaths) : [];
26
+ const patternIssues = checkPatterns ? await analyzePatterns(filePaths) : [];
27
+
28
+ // Convert to AnalysisResult format
29
+ const results: AnalysisResult[] = [];
30
+ const fileIssuesMap = new Map<string, ConsistencyIssue[]>();
31
+
32
+ // Process naming issues
33
+ for (const issue of namingIssues) {
34
+ if (!shouldIncludeSeverity(issue.severity, minSeverity)) {
35
+ continue;
36
+ }
37
+
38
+ const consistencyIssue: ConsistencyIssue = {
39
+ type: issue.type === 'convention-mix' ? 'naming-inconsistency' : 'naming-quality',
40
+ category: 'naming',
41
+ severity: issue.severity,
42
+ message: `${issue.type}: ${issue.identifier}`,
43
+ location: {
44
+ file: issue.file,
45
+ line: issue.line,
46
+ column: issue.column
47
+ },
48
+ suggestion: issue.suggestion
49
+ };
50
+
51
+ if (!fileIssuesMap.has(issue.file)) {
52
+ fileIssuesMap.set(issue.file, []);
53
+ }
54
+ fileIssuesMap.get(issue.file)!.push(consistencyIssue);
55
+ }
56
+
57
+ // Process pattern issues
58
+ for (const issue of patternIssues) {
59
+ if (!shouldIncludeSeverity(issue.severity, minSeverity)) {
60
+ continue;
61
+ }
62
+
63
+ const consistencyIssue: ConsistencyIssue = {
64
+ type: 'pattern-inconsistency',
65
+ category: 'patterns',
66
+ severity: issue.severity,
67
+ message: issue.description,
68
+ location: {
69
+ file: issue.files[0] || 'multiple files',
70
+ line: 1
71
+ },
72
+ examples: issue.examples,
73
+ suggestion: `Standardize ${issue.type} patterns across ${issue.files.length} files`
74
+ };
75
+
76
+ // Add to first file in the pattern
77
+ const firstFile = issue.files[0];
78
+ if (firstFile && !fileIssuesMap.has(firstFile)) {
79
+ fileIssuesMap.set(firstFile, []);
80
+ }
81
+ if (firstFile) {
82
+ fileIssuesMap.get(firstFile)!.push(consistencyIssue);
83
+ }
84
+ }
85
+
86
+ // Convert to AnalysisResult format
87
+ for (const [fileName, issues] of fileIssuesMap) {
88
+ results.push({
89
+ fileName,
90
+ issues: issues as Issue[],
91
+ metrics: {
92
+ consistencyScore: calculateConsistencyScore(issues)
93
+ }
94
+ });
95
+ }
96
+
97
+ // Generate recommendations
98
+ const recommendations = generateRecommendations(namingIssues, patternIssues);
99
+
100
+ // Detect naming conventions
101
+ const conventionAnalysis = detectNamingConventions(filePaths, namingIssues);
102
+
103
+ return {
104
+ summary: {
105
+ totalIssues: namingIssues.length + patternIssues.length,
106
+ namingIssues: namingIssues.length,
107
+ patternIssues: patternIssues.length,
108
+ architectureIssues: 0,
109
+ filesAnalyzed: filePaths.length
110
+ },
111
+ results,
112
+ recommendations
113
+ };
114
+ }
115
+
116
+ function shouldIncludeSeverity(
117
+ severity: 'critical' | 'major' | 'minor' | 'info',
118
+ minSeverity: 'critical' | 'major' | 'minor' | 'info'
119
+ ): boolean {
120
+ const severityLevels = { info: 0, minor: 1, major: 2, critical: 3 };
121
+ return severityLevels[severity] >= severityLevels[minSeverity];
122
+ }
123
+
124
+ function calculateConsistencyScore(issues: ConsistencyIssue[]): number {
125
+ // Higher score = more consistent (fewer issues)
126
+ const weights = { critical: 10, major: 5, minor: 2, info: 1 };
127
+ const totalWeight = issues.reduce((sum, issue) => sum + weights[issue.severity], 0);
128
+ // Score from 0-1, where 1 is perfect
129
+ return Math.max(0, 1 - totalWeight / 100);
130
+ }
131
+
132
+ function generateRecommendations(
133
+ namingIssues: any[],
134
+ patternIssues: any[]
135
+ ): string[] {
136
+ const recommendations: string[] = [];
137
+
138
+ if (namingIssues.length > 0) {
139
+ const conventionMixCount = namingIssues.filter(i => i.type === 'convention-mix').length;
140
+ if (conventionMixCount > 0) {
141
+ recommendations.push(
142
+ `Standardize naming conventions: Found ${conventionMixCount} snake_case variables in TypeScript/JavaScript (use camelCase)`
143
+ );
144
+ }
145
+
146
+ const poorNamingCount = namingIssues.filter(i => i.type === 'poor-naming').length;
147
+ if (poorNamingCount > 0) {
148
+ recommendations.push(
149
+ `Improve variable naming: Found ${poorNamingCount} single-letter or unclear variable names`
150
+ );
151
+ }
152
+ }
153
+
154
+ if (patternIssues.length > 0) {
155
+ const errorHandlingIssues = patternIssues.filter(i => i.type === 'error-handling');
156
+ if (errorHandlingIssues.length > 0) {
157
+ recommendations.push(
158
+ 'Standardize error handling strategy across the codebase (prefer try-catch with typed errors)'
159
+ );
160
+ }
161
+
162
+ const asyncIssues = patternIssues.filter(i => i.type === 'async-style');
163
+ if (asyncIssues.length > 0) {
164
+ recommendations.push(
165
+ 'Use async/await consistently instead of mixing with promise chains or callbacks'
166
+ );
167
+ }
168
+
169
+ const importIssues = patternIssues.filter(i => i.type === 'import-style');
170
+ if (importIssues.length > 0) {
171
+ recommendations.push(
172
+ 'Use ES modules consistently across the project (avoid mixing with CommonJS)'
173
+ );
174
+ }
175
+ }
176
+
177
+ if (recommendations.length === 0) {
178
+ recommendations.push('No major consistency issues found! Your codebase follows good practices.');
179
+ }
180
+
181
+ return recommendations;
182
+ }
@@ -0,0 +1,134 @@
1
+ import { readFileContent } from '@aiready/core';
2
+ import type { NamingIssue } from '../types';
3
+
4
+ /**
5
+ * Analyzes naming conventions and quality
6
+ */
7
+ export async function analyzeNaming(files: string[]): Promise<NamingIssue[]> {
8
+ const issues: NamingIssue[] = [];
9
+
10
+ for (const file of files) {
11
+ const content = await readFileContent(file);
12
+ const fileIssues = analyzeFileNaming(file, content);
13
+ issues.push(...fileIssues);
14
+ }
15
+
16
+ return issues;
17
+ }
18
+
19
+ function analyzeFileNaming(file: string, content: string): NamingIssue[] {
20
+ const issues: NamingIssue[] = [];
21
+
22
+ // Split into lines for line number tracking
23
+ const lines = content.split('\n');
24
+
25
+ // Check for naming patterns
26
+ lines.forEach((line, index) => {
27
+ const lineNumber = index + 1;
28
+
29
+ // Check for single letter variables (except i, j, k in loops)
30
+ const singleLetterMatches = line.matchAll(/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi);
31
+ for (const match of singleLetterMatches) {
32
+ issues.push({
33
+ file,
34
+ line: lineNumber,
35
+ type: 'poor-naming',
36
+ identifier: match[1],
37
+ severity: 'minor',
38
+ suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
39
+ });
40
+ }
41
+
42
+ // Check for overly abbreviated variables
43
+ const abbreviationMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g);
44
+ for (const match of abbreviationMatches) {
45
+ const abbrev = match[1];
46
+ // Skip common acceptable abbreviations
47
+ if (!['id', 'url', 'api', 'db', 'fs', 'os', 'ui'].includes(abbrev.toLowerCase())) {
48
+ issues.push({
49
+ file,
50
+ line: lineNumber,
51
+ type: 'abbreviation',
52
+ identifier: abbrev,
53
+ severity: 'info',
54
+ suggestion: `Consider using full word instead of abbreviation '${abbrev}'`
55
+ });
56
+ }
57
+ }
58
+
59
+ // Check for snake_case vs camelCase mixing in TypeScript/JavaScript
60
+ if (file.match(/\.(ts|tsx|js|jsx)$/)) {
61
+ const camelCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*=/);
62
+ const snakeCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-z0-9]*_[a-z0-9_]*)\s*=/);
63
+
64
+ if (snakeCaseVars) {
65
+ issues.push({
66
+ file,
67
+ line: lineNumber,
68
+ type: 'convention-mix',
69
+ identifier: snakeCaseVars[1],
70
+ severity: 'minor',
71
+ suggestion: `Use camelCase '${snakeCaseToCamelCase(snakeCaseVars[1])}' instead of snake_case in TypeScript/JavaScript`
72
+ });
73
+ }
74
+ }
75
+
76
+ // Check for unclear boolean names (should start with is/has/should/can)
77
+ const booleanMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi);
78
+ for (const match of booleanMatches) {
79
+ const name = match[1];
80
+ if (!name.match(/^(is|has|should|can|will|did)/i)) {
81
+ issues.push({
82
+ file,
83
+ line: lineNumber,
84
+ type: 'unclear',
85
+ identifier: name,
86
+ severity: 'info',
87
+ suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity`
88
+ });
89
+ }
90
+ }
91
+
92
+ // Check for function names that don't indicate action
93
+ const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
94
+ for (const match of functionMatches) {
95
+ const name = match[1];
96
+ // Functions should typically start with verbs
97
+ if (!name.match(/^(get|set|is|has|can|should|create|update|delete|fetch|load|save|process|handle|validate|check|find|search|filter|map|reduce)/)) {
98
+ issues.push({
99
+ file,
100
+ line: lineNumber,
101
+ type: 'unclear',
102
+ identifier: name,
103
+ severity: 'info',
104
+ suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
105
+ });
106
+ }
107
+ }
108
+ });
109
+
110
+ return issues;
111
+ }
112
+
113
+ function snakeCaseToCamelCase(str: string): string {
114
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
115
+ }
116
+
117
+ /**
118
+ * Detects naming convention patterns across the codebase
119
+ */
120
+ export function detectNamingConventions(files: string[], allIssues: NamingIssue[]): {
121
+ dominantConvention: 'camelCase' | 'snake_case' | 'PascalCase' | 'mixed';
122
+ conventionScore: number;
123
+ } {
124
+ // Count conventions
125
+ const camelCaseCount = allIssues.filter(i => i.type === 'convention-mix').length;
126
+ const totalChecks = files.length * 10; // Rough estimate
127
+
128
+ if (camelCaseCount / totalChecks > 0.3) {
129
+ return { dominantConvention: 'mixed', conventionScore: 0.5 };
130
+ }
131
+
132
+ // For TypeScript/JavaScript, default to camelCase
133
+ return { dominantConvention: 'camelCase', conventionScore: 0.9 };
134
+ }
@@ -0,0 +1,192 @@
1
+ import { readFileContent } from '@aiready/core';
2
+ import type { PatternIssue } from '../types';
3
+
4
+ /**
5
+ * Analyzes code pattern consistency
6
+ */
7
+ export async function analyzePatterns(files: string[]): Promise<PatternIssue[]> {
8
+ const issues: PatternIssue[] = [];
9
+
10
+ // Analyze error handling patterns
11
+ const errorHandlingIssues = await analyzeErrorHandling(files);
12
+ issues.push(...errorHandlingIssues);
13
+
14
+ // Analyze async/await patterns
15
+ const asyncIssues = await analyzeAsyncPatterns(files);
16
+ issues.push(...asyncIssues);
17
+
18
+ // Analyze import styles
19
+ const importIssues = await analyzeImportStyles(files);
20
+ issues.push(...importIssues);
21
+
22
+ return issues;
23
+ }
24
+
25
+ async function analyzeErrorHandling(files: string[]): Promise<PatternIssue[]> {
26
+ const patterns = {
27
+ tryCatch: [] as string[],
28
+ throwsError: [] as string[],
29
+ returnsNull: [] as string[],
30
+ returnsError: [] as string[]
31
+ };
32
+
33
+ for (const file of files) {
34
+ const content = await readFileContent(file);
35
+
36
+ if (content.includes('try {') || content.includes('} catch')) {
37
+ patterns.tryCatch.push(file);
38
+ }
39
+ if (content.match(/throw new \w*Error/)) {
40
+ patterns.throwsError.push(file);
41
+ }
42
+ if (content.match(/return null/)) {
43
+ patterns.returnsNull.push(file);
44
+ }
45
+ if (content.match(/return \{ error:/)) {
46
+ patterns.returnsError.push(file);
47
+ }
48
+ }
49
+
50
+ const issues: PatternIssue[] = [];
51
+
52
+ // Check for mixed error handling strategies
53
+ const strategiesUsed = Object.values(patterns).filter(p => p.length > 0).length;
54
+ if (strategiesUsed > 2) {
55
+ issues.push({
56
+ files: [...new Set([
57
+ ...patterns.tryCatch,
58
+ ...patterns.throwsError,
59
+ ...patterns.returnsNull,
60
+ ...patterns.returnsError
61
+ ])],
62
+ type: 'error-handling',
63
+ description: 'Inconsistent error handling strategies across codebase',
64
+ examples: [
65
+ patterns.tryCatch.length > 0 ? `Try-catch used in ${patterns.tryCatch.length} files` : '',
66
+ patterns.throwsError.length > 0 ? `Throws errors in ${patterns.throwsError.length} files` : '',
67
+ patterns.returnsNull.length > 0 ? `Returns null in ${patterns.returnsNull.length} files` : '',
68
+ patterns.returnsError.length > 0 ? `Returns error objects in ${patterns.returnsError.length} files` : ''
69
+ ].filter(e => e),
70
+ severity: 'major'
71
+ });
72
+ }
73
+
74
+ return issues;
75
+ }
76
+
77
+ async function analyzeAsyncPatterns(files: string[]): Promise<PatternIssue[]> {
78
+ const patterns = {
79
+ asyncAwait: [] as string[],
80
+ promises: [] as string[],
81
+ callbacks: [] as string[]
82
+ };
83
+
84
+ for (const file of files) {
85
+ const content = await readFileContent(file);
86
+
87
+ if (content.match(/async\s+(function|\(|[a-zA-Z])/)) {
88
+ patterns.asyncAwait.push(file);
89
+ }
90
+ if (content.match(/\.then\(/) || content.match(/\.catch\(/)) {
91
+ patterns.promises.push(file);
92
+ }
93
+ if (content.match(/callback\s*\(/) || content.match(/\(\s*err\s*,/)) {
94
+ patterns.callbacks.push(file);
95
+ }
96
+ }
97
+
98
+ const issues: PatternIssue[] = [];
99
+
100
+ // Modern codebases should prefer async/await
101
+ if (patterns.callbacks.length > 0 && patterns.asyncAwait.length > 0) {
102
+ issues.push({
103
+ files: [...patterns.callbacks, ...patterns.asyncAwait],
104
+ type: 'async-style',
105
+ description: 'Mixed async patterns: callbacks and async/await',
106
+ examples: [
107
+ `Callbacks found in: ${patterns.callbacks.slice(0, 3).join(', ')}`,
108
+ `Async/await used in: ${patterns.asyncAwait.slice(0, 3).join(', ')}`
109
+ ],
110
+ severity: 'minor'
111
+ });
112
+ }
113
+
114
+ // Mixing .then() chains with async/await
115
+ if (patterns.promises.length > patterns.asyncAwait.length * 0.3 && patterns.asyncAwait.length > 0) {
116
+ issues.push({
117
+ files: patterns.promises,
118
+ type: 'async-style',
119
+ description: 'Consider using async/await instead of promise chains for consistency',
120
+ examples: patterns.promises.slice(0, 5),
121
+ severity: 'info'
122
+ });
123
+ }
124
+
125
+ return issues;
126
+ }
127
+
128
+ async function analyzeImportStyles(files: string[]): Promise<PatternIssue[]> {
129
+ const patterns = {
130
+ esModules: [] as string[],
131
+ commonJS: [] as string[],
132
+ mixed: [] as string[]
133
+ };
134
+
135
+ for (const file of files) {
136
+ const content = await readFileContent(file);
137
+ const hasESM = content.match(/^import\s+/m);
138
+ const hasCJS = content.match(/require\s*\(/);
139
+
140
+ if (hasESM && hasCJS) {
141
+ patterns.mixed.push(file);
142
+ } else if (hasESM) {
143
+ patterns.esModules.push(file);
144
+ } else if (hasCJS) {
145
+ patterns.commonJS.push(file);
146
+ }
147
+ }
148
+
149
+ const issues: PatternIssue[] = [];
150
+
151
+ // Check for mixed import styles in same file
152
+ if (patterns.mixed.length > 0) {
153
+ issues.push({
154
+ files: patterns.mixed,
155
+ type: 'import-style',
156
+ description: 'Mixed ES modules and CommonJS imports in same files',
157
+ examples: patterns.mixed.slice(0, 5),
158
+ severity: 'major'
159
+ });
160
+ }
161
+
162
+ // Check for inconsistent styles across project
163
+ if (patterns.esModules.length > 0 && patterns.commonJS.length > 0) {
164
+ const ratio = patterns.commonJS.length / (patterns.esModules.length + patterns.commonJS.length);
165
+ if (ratio > 0.2 && ratio < 0.8) {
166
+ issues.push({
167
+ files: [...patterns.esModules, ...patterns.commonJS],
168
+ type: 'import-style',
169
+ description: 'Inconsistent import styles across project',
170
+ examples: [
171
+ `ES modules: ${patterns.esModules.length} files`,
172
+ `CommonJS: ${patterns.commonJS.length} files`
173
+ ],
174
+ severity: 'minor'
175
+ });
176
+ }
177
+ }
178
+
179
+ return issues;
180
+ }
181
+
182
+ /**
183
+ * Analyzes API design consistency
184
+ */
185
+ export async function analyzeAPIDesign(files: string[]): Promise<PatternIssue[]> {
186
+ // This would analyze:
187
+ // - Function parameter order consistency
188
+ // - Return type patterns
189
+ // - Options object vs individual parameters
190
+ // For now, return empty array
191
+ return [];
192
+ }