@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.
package/dist/index.mjs CHANGED
@@ -1,250 +1,78 @@
1
1
  import {
2
- ACCEPTABLE_ABBREVIATIONS,
3
- COMMON_SHORT_WORDS,
4
2
  analyzeConsistency,
5
3
  analyzeNamingAST,
6
- analyzePatterns,
7
- detectNamingConventions,
8
- loadNamingConfig,
9
- snakeCaseToCamelCase
10
- } from "./chunk-J5IFYDVU.mjs";
4
+ analyzePatterns
5
+ } from "./chunk-HJCP36VW.mjs";
11
6
 
12
7
  // src/analyzers/naming.ts
13
- import { readFileContent } from "@aiready/core";
14
- async function analyzeNaming(files) {
15
- const issues = [];
16
- const { customAbbreviations, customShortWords, disabledChecks } = await loadNamingConfig(files);
17
- for (const file of files) {
18
- const content = await readFileContent(file);
19
- const fileIssues = analyzeFileNaming(
20
- file,
21
- content,
22
- customAbbreviations,
23
- customShortWords,
24
- disabledChecks
25
- );
26
- issues.push(...fileIssues);
27
- }
28
- return issues;
29
- }
30
- function analyzeFileNaming(file, content, customAbbreviations, customShortWords, disabledChecks) {
8
+ import { readFileSync } from "fs";
9
+ import { Severity } from "@aiready/core";
10
+ async function analyzeNaming(filePaths) {
31
11
  const issues = [];
32
- const isTestFile = file.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/);
33
- const lines = content.split("\n");
34
- const allAbbreviations = /* @__PURE__ */ new Set([
35
- ...ACCEPTABLE_ABBREVIATIONS,
36
- ...customAbbreviations
37
- ]);
38
- const allShortWords = /* @__PURE__ */ new Set([...COMMON_SHORT_WORDS, ...customShortWords]);
39
- const getContextWindow = (index, windowSize = 3) => {
40
- const start = Math.max(0, index - windowSize);
41
- const end = Math.min(lines.length, index + windowSize + 1);
42
- return lines.slice(start, end).join("\n");
43
- };
44
- const isShortLivedVariable = (varName, declarationIndex) => {
45
- const searchRange = 5;
46
- const endIndex = Math.min(lines.length, declarationIndex + searchRange + 1);
47
- let usageCount = 0;
48
- for (let i = declarationIndex; i < endIndex; i++) {
49
- const regex = new RegExp(`\\b${varName}\\b`, "g");
50
- const matches = lines[i].match(regex);
51
- if (matches) {
52
- usageCount += matches.length;
53
- }
54
- }
55
- return usageCount >= 2 && usageCount <= 3;
56
- };
57
- lines.forEach((line, index) => {
58
- const lineNumber = index + 1;
59
- const contextWindow = getContextWindow(index);
60
- if (!disabledChecks.has("single-letter")) {
61
- const singleLetterMatches = line.matchAll(
62
- /\b(?:const|let|var)\s+([a-hm-z])\s*=/gi
63
- );
64
- for (const match of singleLetterMatches) {
65
- const letter = match[1].toLowerCase();
66
- const isCoverageContext = /coverage|summary|metrics|pct|percent/i.test(line) || /\.(?:statements|branches|functions|lines)\.pct/i.test(line);
67
- if (isCoverageContext && ["s", "b", "f", "l"].includes(letter)) {
68
- continue;
69
- }
70
- const isInLoopContext = line.includes("for") || /\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(line) || line.includes("=>") || // Arrow function
71
- /\w+\s*=>\s*/.test(line);
72
- const isI18nContext = line.includes("useTranslation") || line.includes("i18n.t") || /\bt\s*\(['"]/.test(line);
73
- const isArrowFunctionParam = /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
74
- /[a-z]\s*=>/.test(line) || // s => on same line
75
- // Multi-line arrow function detection: look for pattern in context window
76
- new RegExp(`\\b${letter}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
77
- new RegExp(
78
- `\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`
79
- ).test(lines[index - 1] || "") && /=>/.test(contextWindow);
80
- const isShortLived = isShortLivedVariable(letter, index);
81
- if (!isInLoopContext && !isI18nContext && !isArrowFunctionParam && !isShortLived && !["x", "y", "z", "i", "j", "k", "l", "n", "m"].includes(letter)) {
82
- if (isTestFile && ["a", "b", "c", "d", "e", "f", "s"].includes(letter)) {
83
- continue;
84
- }
12
+ for (const filePath of filePaths) {
13
+ try {
14
+ const content = readFileSync(filePath, "utf-8");
15
+ const lines = content.split("\n");
16
+ lines.forEach((line, index) => {
17
+ const singleLetterMatch = line.match(/\b(const|let|var)\s+([a-hj-km-np-zA-Z])\s*=/);
18
+ if (singleLetterMatch) {
85
19
  issues.push({
86
- file,
87
- line: lineNumber,
20
+ file: filePath,
21
+ line: index + 1,
88
22
  type: "poor-naming",
89
- identifier: match[1],
90
- severity: "minor",
91
- suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
23
+ identifier: singleLetterMatch[2],
24
+ severity: Severity.Minor,
25
+ suggestion: "Use a more descriptive name than a single letter"
92
26
  });
93
27
  }
94
- }
95
- }
96
- if (!disabledChecks.has("abbreviation")) {
97
- const abbreviationMatches = line.matchAll(
98
- /\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g
99
- );
100
- for (const match of abbreviationMatches) {
101
- const abbrev = match[1].toLowerCase();
102
- if (allShortWords.has(abbrev)) {
103
- continue;
104
- }
105
- if (allAbbreviations.has(abbrev)) {
106
- continue;
107
- }
108
- const isArrowFunctionParam = /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
109
- new RegExp(`\\b${abbrev}\\s*=>`).test(line) || // s => on same line
110
- // Multi-line arrow function: check context window
111
- new RegExp(`\\b${abbrev}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
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);
115
- if (isArrowFunctionParam) {
116
- continue;
117
- }
118
- if (abbrev.length <= 2) {
119
- const isDateTimeContext = /date|time|day|hour|minute|second|timestamp/i.test(line);
120
- if (isDateTimeContext && ["d", "t", "dt"].includes(abbrev)) {
121
- continue;
122
- }
123
- const isUserContext = /user|auth|account/i.test(line);
124
- if (isUserContext && abbrev === "u") {
125
- continue;
28
+ if (filePath.match(/\.(ts|tsx|js|jsx)$/)) {
29
+ const snakeCaseMatch = line.match(/\b(const|let|var|function)\s+([a-z]+_[a-z0-9_]+)\b/);
30
+ if (snakeCaseMatch) {
31
+ issues.push({
32
+ file: filePath,
33
+ line: index + 1,
34
+ type: "convention-mix",
35
+ identifier: snakeCaseMatch[2],
36
+ severity: Severity.Info,
37
+ suggestion: "Use camelCase instead of snake_case in TypeScript/JavaScript"
38
+ });
126
39
  }
127
40
  }
128
- issues.push({
129
- file,
130
- line: lineNumber,
131
- type: "abbreviation",
132
- identifier: match[1],
133
- severity: "info",
134
- suggestion: `Consider using full word instead of abbreviation '${match[1]}'`
135
- });
136
- }
137
- }
138
- if (!disabledChecks.has("convention-mix") && file.match(/\.(ts|tsx|js|jsx)$/)) {
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
- );
146
- if (snakeCaseVars) {
147
- issues.push({
148
- file,
149
- line: lineNumber,
150
- type: "convention-mix",
151
- identifier: snakeCaseVars[1],
152
- severity: "minor",
153
- suggestion: `Use camelCase '${snakeCaseToCamelCase(snakeCaseVars[1])}' instead of snake_case in TypeScript/JavaScript`
154
- });
155
- }
156
- }
157
- if (!disabledChecks.has("unclear")) {
158
- const booleanMatches = line.matchAll(
159
- /\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi
160
- );
161
- for (const match of booleanMatches) {
162
- const name = match[1];
163
- if (!name.match(/^(is|has|should|can|will|did)/i)) {
164
- issues.push({
165
- file,
166
- line: lineNumber,
167
- type: "unclear",
168
- identifier: name,
169
- severity: "info",
170
- suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity`
171
- });
172
- }
173
- }
174
- }
175
- if (!disabledChecks.has("unclear")) {
176
- const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
177
- for (const match of functionMatches) {
178
- const name = match[1];
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);
198
- if (isKeyword) {
199
- continue;
200
- }
201
- const isEntryPoint = ["main", "init", "setup", "bootstrap"].includes(
202
- name
203
- );
204
- if (isEntryPoint) {
205
- continue;
206
- }
207
- const isFactoryPattern = name.match(
208
- /(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/
209
- );
210
- const isEventHandler = name.match(/^on[A-Z]/);
211
- const isDescriptiveLong = name.length > 15;
212
- const isReactHook = name.match(/^use[A-Z]/);
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
- );
218
- const isHelperPattern = name.match(/^(to|from|with|without|for|as|into)\w+/) || // toMetadata, withLogger, forPath
219
- name.match(/^\w+(To|From|With|Without|For|As|Into)\w*$/);
220
- const isUtilityName = [
221
- "cn",
222
- "proxy",
223
- "sitemap",
224
- "robots",
225
- "gtag"
226
- ].includes(name);
227
- const capitalCount = (name.match(/[A-Z]/g) || []).length;
228
- const isCompoundWord = capitalCount >= 3;
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
- );
232
- if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isDescriptivePattern && !isCompoundWord && !isHelperPattern && !isUtilityName && !isReactHook) {
233
- issues.push({
234
- file,
235
- line: lineNumber,
236
- type: "unclear",
237
- identifier: name,
238
- severity: "info",
239
- suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
240
- });
41
+ const shortNameMatch = line.match(/\b(const|let|var)\s+([a-zA-Z0-9]{2,3})\s*=/);
42
+ if (shortNameMatch) {
43
+ const name = shortNameMatch[2].toLowerCase();
44
+ const vagueNames = ["obj", "val", "tmp", "res", "ret", "data"];
45
+ if (vagueNames.includes(name)) {
46
+ issues.push({
47
+ file: filePath,
48
+ line: index + 1,
49
+ type: "poor-naming",
50
+ identifier: name,
51
+ severity: Severity.Minor,
52
+ suggestion: `Avoid vague names like '${name}'`
53
+ });
54
+ }
241
55
  }
242
- }
56
+ });
57
+ } catch (err) {
58
+ void err;
243
59
  }
244
- });
60
+ }
245
61
  return issues;
246
62
  }
247
63
 
64
+ // src/analyzers/naming-constants.ts
65
+ function detectNamingConventions(files, allIssues) {
66
+ const camelCaseCount = allIssues.filter(
67
+ (i) => i.type === "convention-mix"
68
+ ).length;
69
+ const totalChecks = files.length * 10;
70
+ if (camelCaseCount / totalChecks > 0.3) {
71
+ return { dominantConvention: "mixed", conventionScore: 0.5 };
72
+ }
73
+ return { dominantConvention: "camelCase", conventionScore: 0.9 };
74
+ }
75
+
248
76
  // src/scoring.ts
249
77
  import { calculateProductivityImpact } from "@aiready/core";
250
78
  function calculateConsistencyScore(issues, totalFilesAnalyzed, costConfig) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/consistency",
3
- "version": "0.16.2",
3
+ "version": "0.16.3",
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.19.2"
46
+ "@aiready/core": "0.19.3"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "^24.0.0",
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
2
  import { analyzeConsistency } from '../index';
3
- import { validateSpokeOutput } from '../../../core/src/types/contract';
3
+ import { validateSpokeOutput, SpokeOutputSchema } from '@aiready/core';
4
4
 
5
5
  // Mock core functions
6
6
  vi.mock('@aiready/core', async (importOriginal) => {
@@ -21,14 +21,30 @@ describe('Consistency Spoke Contract Validation', () => {
21
21
  const fullOutput = {
22
22
  results: results.results,
23
23
  summary: results.summary,
24
+ metadata: {
25
+ toolName: 'consistency',
26
+ version: '0.1.0',
27
+ timestamp: new Date().toISOString(),
28
+ },
24
29
  };
25
30
 
31
+ // 1. Legacy validation
26
32
  const validation = validateSpokeOutput('consistency', fullOutput);
27
33
 
28
34
  if (!validation.valid) {
29
- console.error('Contract Validation Errors:', validation.errors);
35
+ console.error('Contract Validation Errors (Legacy):', validation.errors);
30
36
  }
31
37
 
32
38
  expect(validation.valid).toBe(true);
39
+
40
+ // 2. Zod validation
41
+ const zodResult = SpokeOutputSchema.safeParse(fullOutput);
42
+ if (!zodResult.success) {
43
+ console.error(
44
+ 'Contract Validation Errors (Zod):',
45
+ zodResult.error.format()
46
+ );
47
+ }
48
+ expect(zodResult.success).toBe(true);
33
49
  });
34
50
  });
package/src/analyzer.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { scanFiles } from '@aiready/core';
1
+ import { scanFiles, Severity, IssueType } from '@aiready/core';
2
2
  import type { AnalysisResult, Issue } from '@aiready/core';
3
3
  import type {
4
4
  ConsistencyOptions,
@@ -20,7 +20,7 @@ export async function analyzeConsistency(
20
20
  checkNaming = true,
21
21
  checkPatterns = true,
22
22
  checkArchitecture = false, // Not implemented yet
23
- minSeverity = 'info',
23
+ minSeverity = Severity.Info,
24
24
  ...scanOptions
25
25
  } = options;
26
26
 
@@ -59,10 +59,10 @@ export async function analyzeConsistency(
59
59
  const consistencyIssue: ConsistencyIssue = {
60
60
  type:
61
61
  issue.type === 'convention-mix'
62
- ? 'naming-inconsistency'
63
- : 'naming-quality',
62
+ ? IssueType.NamingInconsistency
63
+ : IssueType.NamingQuality,
64
64
  category: 'naming',
65
- severity: issue.severity,
65
+ severity: getSeverityEnum(issue.severity),
66
66
  message: `${issue.type}: ${issue.identifier}`,
67
67
  location: {
68
68
  file: issue.file,
@@ -85,9 +85,9 @@ export async function analyzeConsistency(
85
85
  }
86
86
 
87
87
  const consistencyIssue: ConsistencyIssue = {
88
- type: 'pattern-inconsistency',
88
+ type: IssueType.PatternInconsistency,
89
89
  category: 'patterns',
90
- severity: issue.severity,
90
+ severity: getSeverityEnum(issue.severity),
91
91
  message: issue.description,
92
92
  location: {
93
93
  file: issue.files[0] || 'multiple files',
@@ -120,18 +120,19 @@ export async function analyzeConsistency(
120
120
 
121
121
  // Sort results by severity first, then by issue count per file
122
122
  results.sort((fileResultA, fileResultB) => {
123
- const severityOrder = { critical: 0, major: 1, minor: 2, info: 3 };
124
-
125
123
  // Get highest severity in each file
126
124
  const maxSeverityA = Math.min(
127
- ...fileResultA.issues.map(
128
- (i) => severityOrder[(i as ConsistencyIssue).severity]
129
- )
125
+ ...fileResultA.issues.map((i) => {
126
+ const val = getSeverityLevel((i as ConsistencyIssue).severity);
127
+ // Map 4->0, 3->1, 2->2, 1->3
128
+ return val === 4 ? 0 : val === 3 ? 1 : val === 2 ? 2 : 3;
129
+ })
130
130
  );
131
131
  const maxSeverityB = Math.min(
132
- ...fileResultB.issues.map(
133
- (i) => severityOrder[(i as ConsistencyIssue).severity]
134
- )
132
+ ...fileResultB.issues.map((i) => {
133
+ const val = getSeverityLevel((i as ConsistencyIssue).severity);
134
+ return val === 4 ? 0 : val === 3 ? 1 : val === 2 ? 2 : 3;
135
+ })
135
136
  );
136
137
 
137
138
  // Sort by severity first
@@ -154,9 +155,6 @@ export async function analyzeConsistency(
154
155
  shouldIncludeSeverity(i.severity, minSeverity)
155
156
  ).length;
156
157
 
157
- // Detect naming conventions (TODO: re-implement for AST version)
158
- // const conventionAnalysis = detectNamingConventions(filePaths, namingIssues);
159
-
160
158
  return {
161
159
  summary: {
162
160
  totalIssues: namingCountFiltered + patternCountFiltered,
@@ -170,21 +168,44 @@ export async function analyzeConsistency(
170
168
  };
171
169
  }
172
170
 
171
+ function getSeverityLevel(s: any): number {
172
+ if (s === Severity.Critical || s === 'critical') return 4;
173
+ if (s === Severity.Major || s === 'major') return 3;
174
+ if (s === Severity.Minor || s === 'minor') return 2;
175
+ if (s === Severity.Info || s === 'info') return 1;
176
+ return 0;
177
+ }
178
+
179
+ function getSeverityEnum(s: any): Severity {
180
+ const val = getSeverityLevel(s);
181
+ switch (val) {
182
+ case 4: return Severity.Critical;
183
+ case 3: return Severity.Major;
184
+ case 2: return Severity.Minor;
185
+ case 1: return Severity.Info;
186
+ default: return Severity.Info;
187
+ }
188
+ }
189
+
173
190
  function shouldIncludeSeverity(
174
- severity: 'critical' | 'major' | 'minor' | 'info',
175
- minSeverity: 'critical' | 'major' | 'minor' | 'info'
191
+ severity: Severity | string,
192
+ minSeverity: Severity | string
176
193
  ): boolean {
177
- const severityLevels = { info: 0, minor: 1, major: 2, critical: 3 };
178
- return severityLevels[severity] >= severityLevels[minSeverity];
194
+ return getSeverityLevel(severity) >= getSeverityLevel(minSeverity);
179
195
  }
180
196
 
181
197
  function calculateConsistencyScore(issues: ConsistencyIssue[]): number {
182
- // Higher score = more consistent (fewer issues)
183
- const weights = { critical: 10, major: 5, minor: 2, info: 1 };
184
- const totalWeight = issues.reduce(
185
- (sum, issue) => sum + weights[issue.severity],
186
- 0
187
- );
198
+ let totalWeight = 0;
199
+ for (const issue of issues) {
200
+ const val = getSeverityLevel(issue.severity);
201
+ switch (val) {
202
+ case 4: totalWeight += 10; break;
203
+ case 3: totalWeight += 5; break;
204
+ case 2: totalWeight += 2; break;
205
+ case 1: totalWeight += 1; break;
206
+ default: totalWeight += 1;
207
+ }
208
+ }
188
209
  // Score from 0-1, where 1 is perfect
189
210
  return Math.max(0, 1 - totalWeight / 100);
190
211
  }