@aiready/consistency 0.3.4 → 0.4.1

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,5 +1,6 @@
1
- import { readFileContent } from '@aiready/core';
1
+ import { readFileContent, loadConfig } from '@aiready/core';
2
2
  import type { NamingIssue } from '../types';
3
+ import { dirname } from 'path';
3
4
 
4
5
  // Common short English words that are NOT abbreviations (full, valid words)
5
6
  const COMMON_SHORT_WORDS = new Set([
@@ -8,8 +9,8 @@ const COMMON_SHORT_WORDS = new Set([
8
9
  'run', 'try', 'use', 'get', 'set', 'add', 'put', 'map', 'log', 'row', 'col',
9
10
  'tab', 'box', 'div', 'nav', 'tag', 'any', 'all', 'one', 'two', 'out', 'off',
10
11
  'on', 'yes', 'no', 'now', 'max', 'min', 'sum', 'avg', 'ref', 'src', 'dst',
11
- 'raw', 'def', 'sub', 'pub', 'pre', 'mid', 'alt', 'opt', 'tmp', 'ext', 'sep',
12
- // Additional full words commonly flagged
12
+ 'raw', 'def', 'sub', 'pub', 'pre', 'mid', 'alt', 'opt', 'tmp', 'ext', 'sep', // Prepositions and conjunctions
13
+ 'and', 'from', 'how', 'pad', 'bar', 'non', // Additional full words commonly flagged
13
14
  'tax', 'cat', 'dog', 'car', 'bus', 'web', 'app', 'war', 'law', 'pay', 'buy',
14
15
  'win', 'cut', 'hit', 'hot', 'pop', 'job', 'age', 'act', 'let', 'lot', 'bad',
15
16
  'big', 'far', 'few', 'own', 'per', 'red', 'low', 'see', 'six', 'ten', 'way',
@@ -54,12 +55,15 @@ const ACCEPTABLE_ABBREVIATIONS = new Set([
54
55
  'ts', 'js', 'jsx', 'tsx', 'py', 'rb', 'vue', 're', 'fn', 'fns', 'mod', 'opts', 'dev',
55
56
  // Cloud/Infrastructure
56
57
  's3', 'ec2', 'sqs', 'sns', 'vpc', 'ami', 'iam', 'acl', 'elb', 'alb', 'nlb', 'aws',
58
+ 'ses', 'gst', 'cdk', 'btn', 'buf', 'agg', 'ocr', 'ai', 'cf', 'cfn', 'ga',
57
59
  // Metrics/Performance
58
- 'fcp', 'lcp', 'cls', 'ttfb', 'tti', 'fid', 'fps', 'qps', 'rps', 'tps',
60
+ 'fcp', 'lcp', 'cls', 'ttfb', 'tti', 'fid', 'fps', 'qps', 'rps', 'tps', 'wpm',
59
61
  // Testing & i18n
60
- 'po', 'e2e', 'a11y', 'i18n', 'l10n',
62
+ 'po', 'e2e', 'a11y', 'i18n', 'l10n', 'spy',
61
63
  // Domain-specific abbreviations (context-aware)
62
- 'sk', 'fy', 'faq', 'og', 'seo', 'cta', 'roi', 'kpi',
64
+ 'sk', 'fy', 'faq', 'og', 'seo', 'cta', 'roi', 'kpi', 'ttl', 'pct',
65
+ // Technical abbreviations
66
+ 'mac', 'hex', 'esm', 'git', 'rec', 'loc', 'dup',
63
67
  // Boolean helpers (these are intentional short names)
64
68
  'is', 'has', 'can', 'did', 'was', 'are',
65
69
  // Date/Time context (when in date contexts)
@@ -72,16 +76,32 @@ const ACCEPTABLE_ABBREVIATIONS = new Set([
72
76
  export async function analyzeNaming(files: string[]): Promise<NamingIssue[]> {
73
77
  const issues: NamingIssue[] = [];
74
78
 
79
+ // Load config from the first file's directory (or project root)
80
+ const rootDir = files.length > 0 ? dirname(files[0]) : process.cwd();
81
+ const config = loadConfig(rootDir);
82
+ const consistencyConfig = config?.tools?.['consistency'];
83
+
84
+ // Merge custom abbreviations and short words with defaults
85
+ const customAbbreviations = new Set(consistencyConfig?.acceptedAbbreviations || []);
86
+ const customShortWords = new Set(consistencyConfig?.shortWords || []);
87
+ const disabledChecks = new Set(consistencyConfig?.disableChecks || []);
88
+
75
89
  for (const file of files) {
76
90
  const content = await readFileContent(file);
77
- const fileIssues = analyzeFileNaming(file, content);
91
+ const fileIssues = analyzeFileNaming(file, content, customAbbreviations, customShortWords, disabledChecks);
78
92
  issues.push(...fileIssues);
79
93
  }
80
94
 
81
95
  return issues;
82
96
  }
83
97
 
84
- function analyzeFileNaming(file: string, content: string): NamingIssue[] {
98
+ function analyzeFileNaming(
99
+ file: string,
100
+ content: string,
101
+ customAbbreviations: Set<string>,
102
+ customShortWords: Set<string>,
103
+ disabledChecks: Set<string>
104
+ ): NamingIssue[] {
85
105
  const issues: NamingIssue[] = [];
86
106
 
87
107
  // Check if this is a test file (more lenient rules)
@@ -90,100 +110,150 @@ function analyzeFileNaming(file: string, content: string): NamingIssue[] {
90
110
  // Split into lines for line number tracking
91
111
  const lines = content.split('\n');
92
112
 
113
+ // Merge custom sets with defaults
114
+ const allAbbreviations = new Set([...ACCEPTABLE_ABBREVIATIONS, ...customAbbreviations]);
115
+ const allShortWords = new Set([...COMMON_SHORT_WORDS, ...customShortWords]);
116
+
117
+ /**
118
+ * Helper: Get context window around a line (for multi-line pattern detection)
119
+ */
120
+ const getContextWindow = (index: number, windowSize: number = 3): string => {
121
+ const start = Math.max(0, index - windowSize);
122
+ const end = Math.min(lines.length, index + windowSize + 1);
123
+ return lines.slice(start, end).join('\n');
124
+ };
125
+
126
+ /**
127
+ * Helper: Check if a variable is short-lived (used only within 3-5 lines)
128
+ */
129
+ const isShortLivedVariable = (varName: string, declarationIndex: number): boolean => {
130
+ const searchRange = 5; // Check 5 lines after declaration
131
+ const endIndex = Math.min(lines.length, declarationIndex + searchRange + 1);
132
+
133
+ let usageCount = 0;
134
+ for (let i = declarationIndex; i < endIndex; i++) {
135
+ // Match variable name as whole word
136
+ const regex = new RegExp(`\\b${varName}\\b`, 'g');
137
+ const matches = lines[i].match(regex);
138
+ if (matches) {
139
+ usageCount += matches.length;
140
+ }
141
+ }
142
+
143
+ // If variable is only used 2-3 times within 5 lines, it's short-lived
144
+ // (1 = declaration, 1-2 = actual usage)
145
+ return usageCount >= 2 && usageCount <= 3;
146
+ };
147
+
93
148
  // Check for naming patterns
94
149
  lines.forEach((line, index) => {
95
150
  const lineNumber = index + 1;
151
+ const contextWindow = getContextWindow(index);
96
152
 
97
153
  // Check for single letter variables (except i, j, k, l in loops/common contexts)
98
- const singleLetterMatches = line.matchAll(/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi);
99
- for (const match of singleLetterMatches) {
100
- const letter = match[1].toLowerCase();
101
-
102
- // Enhanced loop/iterator context detection
103
- const isInLoopContext =
104
- line.includes('for') ||
105
- /\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(line) ||
106
- line.includes('=>') || // Arrow function
107
- /\w+\s*=>\s*/.test(line); // Callback pattern
108
-
109
- // Check for i18n/translation context
110
- const isI18nContext =
111
- line.includes('useTranslation') ||
112
- line.includes('i18n.t') ||
113
- /\bt\s*\(['"]/.test(line); // t('key') pattern
114
-
115
- // Check for arrow function parameter (improved detection)
116
- const isArrowFunctionParam =
117
- /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
118
- /[a-z]\s*=>/.test(line); // s =>
119
-
120
- if (!isInLoopContext && !isI18nContext && !isArrowFunctionParam && !['x', 'y', 'z', 'i', 'j', 'k', 'l', 'n', 'm'].includes(letter)) {
121
- // Skip in test files unless it's really unclear
122
- if (isTestFile && ['a', 'b', 'c', 'd', 'e', 'f', 's'].includes(letter)) {
123
- continue;
124
- }
154
+ if (!disabledChecks.has('single-letter')) {
155
+ const singleLetterMatches = line.matchAll(/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi);
156
+ for (const match of singleLetterMatches) {
157
+ const letter = match[1].toLowerCase();
125
158
 
126
- issues.push({
127
- file,
128
- line: lineNumber,
129
- type: 'poor-naming',
130
- identifier: match[1],
131
- severity: 'minor',
132
- suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
133
- });
159
+ // Enhanced loop/iterator context detection
160
+ const isInLoopContext =
161
+ line.includes('for') ||
162
+ /\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(line) ||
163
+ line.includes('=>') || // Arrow function
164
+ /\w+\s*=>\s*/.test(line); // Callback pattern
165
+
166
+ // Check for i18n/translation context
167
+ const isI18nContext =
168
+ line.includes('useTranslation') ||
169
+ line.includes('i18n.t') ||
170
+ /\bt\s*\(['"]/.test(line); // t('key') pattern
171
+
172
+ // Check for arrow function parameter (improved detection with context window)
173
+ const isArrowFunctionParam =
174
+ /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
175
+ /[a-z]\s*=>/.test(line) || // s => on same line
176
+ // Multi-line arrow function detection: look for pattern in context window
177
+ new RegExp(`\\b${letter}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
178
+ new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || '') && /=>/.test(contextWindow); // .map(\n s =>
179
+
180
+ // Check if variable is short-lived (comparison/temporary contexts)
181
+ const isShortLived = isShortLivedVariable(letter, index);
182
+
183
+ if (!isInLoopContext && !isI18nContext && !isArrowFunctionParam && !isShortLived && !['x', 'y', 'z', 'i', 'j', 'k', 'l', 'n', 'm'].includes(letter)) {
184
+ // Skip in test files unless it's really unclear
185
+ if (isTestFile && ['a', 'b', 'c', 'd', 'e', 'f', 's'].includes(letter)) {
186
+ continue;
187
+ }
188
+
189
+ issues.push({
190
+ file,
191
+ line: lineNumber,
192
+ type: 'poor-naming',
193
+ identifier: match[1],
194
+ severity: 'minor',
195
+ suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
196
+ });
197
+ }
134
198
  }
135
199
  }
136
200
 
137
201
  // Check for overly abbreviated variables
138
- const abbreviationMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g);
139
- for (const match of abbreviationMatches) {
140
- const abbrev = match[1].toLowerCase();
141
-
142
- // Skip if it's a common short English word (full word, not abbreviation)
143
- if (COMMON_SHORT_WORDS.has(abbrev)) {
144
- continue;
145
- }
146
-
147
- // Skip acceptable abbreviations
148
- if (ACCEPTABLE_ABBREVIATIONS.has(abbrev)) {
149
- continue;
150
- }
151
-
152
- // Check for arrow function parameter context
153
- const isArrowFunctionParam =
154
- /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
155
- new RegExp(`\\b${abbrev}\\s*=>`).test(line); // s =>
156
-
157
- if (isArrowFunctionParam) {
158
- continue;
159
- }
160
-
161
- // For very short names (1-2 letters), check for date/time context
162
- if (abbrev.length <= 2) {
163
- const isDateTimeContext = /date|time|day|hour|minute|second|timestamp/i.test(line);
164
- if (isDateTimeContext && ['d', 't', 'dt'].includes(abbrev)) {
202
+ if (!disabledChecks.has('abbreviation')) {
203
+ const abbreviationMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g);
204
+ for (const match of abbreviationMatches) {
205
+ const abbrev = match[1].toLowerCase();
206
+
207
+ // Skip if it's a common short English word (full word, not abbreviation)
208
+ if (allShortWords.has(abbrev)) {
165
209
  continue;
166
210
  }
167
211
 
168
- // Check for user/auth context
169
- const isUserContext = /user|auth|account/i.test(line);
170
- if (isUserContext && abbrev === 'u') {
212
+ // Skip acceptable abbreviations (including custom ones)
213
+ if (allAbbreviations.has(abbrev)) {
214
+ continue;
215
+ }
216
+
217
+ // Check for arrow function parameter context (with multi-line detection)
218
+ const isArrowFunctionParam =
219
+ /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
220
+ new RegExp(`\\b${abbrev}\\s*=>`).test(line) || // s => on same line
221
+ // Multi-line arrow function: check context window
222
+ (new RegExp(`\\b${abbrev}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow)) || // (s)\n =>
223
+ (new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || '') &&
224
+ new RegExp(`^\\s*${abbrev}\\s*=>`).test(line)); // .map(\n s =>
225
+
226
+ if (isArrowFunctionParam) {
171
227
  continue;
172
228
  }
229
+
230
+ // For very short names (1-2 letters), check for date/time context
231
+ if (abbrev.length <= 2) {
232
+ const isDateTimeContext = /date|time|day|hour|minute|second|timestamp/i.test(line);
233
+ if (isDateTimeContext && ['d', 't', 'dt'].includes(abbrev)) {
234
+ continue;
235
+ }
236
+
237
+ // Check for user/auth context
238
+ const isUserContext = /user|auth|account/i.test(line);
239
+ if (isUserContext && abbrev === 'u') {
240
+ continue;
241
+ }
242
+ }
243
+
244
+ issues.push({
245
+ file,
246
+ line: lineNumber,
247
+ type: 'abbreviation',
248
+ identifier: match[1],
249
+ severity: 'info',
250
+ suggestion: `Consider using full word instead of abbreviation '${match[1]}'`
251
+ });
173
252
  }
174
-
175
- issues.push({
176
- file,
177
- line: lineNumber,
178
- type: 'abbreviation',
179
- identifier: match[1],
180
- severity: 'info',
181
- suggestion: `Consider using full word instead of abbreviation '${match[1]}'`
182
- });
183
253
  }
184
254
 
185
255
  // Check for snake_case vs camelCase mixing in TypeScript/JavaScript
186
- if (file.match(/\.(ts|tsx|js|jsx)$/)) {
256
+ if (!disabledChecks.has('convention-mix') && file.match(/\.(ts|tsx|js|jsx)$/)) {
187
257
  const camelCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*=/);
188
258
  const snakeCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-z0-9]*_[a-z0-9_]*)\s*=/);
189
259
 
@@ -200,69 +270,83 @@ function analyzeFileNaming(file: string, content: string): NamingIssue[] {
200
270
  }
201
271
 
202
272
  // Check for unclear boolean names (should start with is/has/should/can)
203
- const booleanMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi);
204
- for (const match of booleanMatches) {
205
- const name = match[1];
206
- if (!name.match(/^(is|has|should|can|will|did)/i)) {
207
- issues.push({
208
- file,
209
- line: lineNumber,
210
- type: 'unclear',
211
- identifier: name,
212
- severity: 'info',
213
- suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity`
214
- });
273
+ if (!disabledChecks.has('unclear')) {
274
+ const booleanMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi);
275
+ for (const match of booleanMatches) {
276
+ const name = match[1];
277
+ if (!name.match(/^(is|has|should|can|will|did)/i)) {
278
+ issues.push({
279
+ file,
280
+ line: lineNumber,
281
+ type: 'unclear',
282
+ identifier: name,
283
+ severity: 'info',
284
+ suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity`
285
+ });
286
+ }
215
287
  }
216
288
  }
217
289
 
218
290
  // Check for function names that don't indicate action
219
- const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
220
- for (const match of functionMatches) {
221
- const name = match[1];
222
-
223
- // Skip JavaScript/TypeScript keywords that shouldn't be function names
224
- const isKeyword = ['for', 'if', 'else', 'while', 'do', 'switch', 'case', 'break', 'continue', 'return', 'throw', 'try', 'catch', 'finally', 'with', 'yield', 'await'].includes(name);
225
- if (isKeyword) {
226
- continue;
227
- }
228
-
229
- // Skip common entry point names
230
- const isEntryPoint = ['main', 'init', 'setup', 'bootstrap'].includes(name);
231
- if (isEntryPoint) {
232
- continue;
233
- }
234
-
235
- // Functions should typically start with verbs, but allow:
236
- // 1. Factory/builder patterns (ends with Factory, Builder, etc.)
237
- // 2. Descriptive compound names that explain what they return
238
- // 3. Event handlers (onClick, onSubmit, etc.)
239
- // 4. Descriptive aggregate/collection patterns
240
- // 5. Very long descriptive names (>15 chars)
241
- // 6. Compound words with 3+ capitals
242
-
243
- const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator)$/);
244
- const isEventHandler = name.match(/^on[A-Z]/);
245
- const isDescriptiveLong = name.length > 15; // Reduced from 20 to 15
246
-
247
- // Check for descriptive patterns
248
- const isDescriptivePattern = name.match(/^(default|total|count|sum|avg|max|min|initial|current|previous|next)\w+/) ||
249
- name.match(/\w+(Count|Total|Sum|Average|List|Map|Set|Config|Settings|Options|Props)$/);
250
-
251
- // Count capital letters for compound detection
252
- const capitalCount = (name.match(/[A-Z]/g) || []).length;
253
- const isCompoundWord = capitalCount >= 3; // daysSinceLastCommit has 4 capitals
254
-
255
- const hasActionVerb = name.match(/^(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)/);
256
-
257
- if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isDescriptivePattern && !isCompoundWord) {
258
- issues.push({
259
- file,
260
- line: lineNumber,
261
- type: 'unclear',
262
- identifier: name,
263
- severity: 'info',
264
- suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
265
- });
291
+ if (!disabledChecks.has('unclear')) {
292
+ const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
293
+ for (const match of functionMatches) {
294
+ const name = match[1];
295
+
296
+ // Skip JavaScript/TypeScript keywords that shouldn't be function names
297
+ const isKeyword = ['for', 'if', 'else', 'while', 'do', 'switch', 'case', 'break', 'continue', 'return', 'throw', 'try', 'catch', 'finally', 'with', 'yield', 'await'].includes(name);
298
+ if (isKeyword) {
299
+ continue;
300
+ }
301
+
302
+ // Skip common entry point names
303
+ const isEntryPoint = ['main', 'init', 'setup', 'bootstrap'].includes(name);
304
+ if (isEntryPoint) {
305
+ continue;
306
+ }
307
+
308
+ // Functions should typically start with verbs, but allow:
309
+ // 1. Factory/builder patterns (ends with Factory, Builder, etc.)
310
+ // 2. Descriptive compound names that explain what they return
311
+ // 3. Event handlers (onClick, onSubmit, etc.)
312
+ // 4. Descriptive aggregate/collection patterns
313
+ // 5. Very long descriptive names (>15 chars)
314
+ // 6. Compound words with 3+ capitals
315
+ // 7. Helper/utility functions (common patterns)
316
+ // 8. React hooks (useX pattern)
317
+
318
+ const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/);
319
+ const isEventHandler = name.match(/^on[A-Z]/);
320
+ const isDescriptiveLong = name.length > 15; // Reduced from 20 to 15
321
+ const isReactHook = name.match(/^use[A-Z]/); // React hooks
322
+
323
+ // Check for descriptive patterns
324
+ const isDescriptivePattern = name.match(/^(default|total|count|sum|avg|max|min|initial|current|previous|next)\w+/) ||
325
+ name.match(/\w+(Count|Total|Sum|Average|List|Map|Set|Config|Settings|Options|Props|Data|Info|Details|State|Status|Response|Result)$/);
326
+
327
+ // Helper/utility function patterns
328
+ const isHelperPattern = name.match(/^(to|from|with|without|for|as|into)\w+/) || // toMetadata, withLogger, forPath
329
+ name.match(/^\w+(To|From|With|Without|For|As|Into)\w*$/); // metadataTo, pathFrom
330
+
331
+ // Common utility names that are descriptive
332
+ const isUtilityName = ['cn', 'proxy', 'sitemap', 'robots', 'gtag'].includes(name);
333
+
334
+ // Count capital letters for compound detection
335
+ const capitalCount = (name.match(/[A-Z]/g) || []).length;
336
+ const isCompoundWord = capitalCount >= 3; // daysSinceLastCommit has 4 capitals
337
+
338
+ const hasActionVerb = name.match(/^(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)/);
339
+
340
+ if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isDescriptivePattern && !isCompoundWord && !isHelperPattern && !isUtilityName && !isReactHook) {
341
+ issues.push({
342
+ file,
343
+ line: lineNumber,
344
+ type: 'unclear',
345
+ identifier: name,
346
+ severity: 'info',
347
+ suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
348
+ });
349
+ }
266
350
  }
267
351
  }
268
352
  });