@aiready/testability 0.1.5 → 0.1.6

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/cli.mjs CHANGED
@@ -2,16 +2,24 @@
2
2
  import {
3
3
  analyzeTestability,
4
4
  calculateTestabilityScore
5
- } from "./chunk-CYZ7DTWN.mjs";
5
+ } from "./chunk-YLYLRZRS.mjs";
6
6
 
7
7
  // src/cli.ts
8
8
  import { Command } from "commander";
9
9
  import chalk from "chalk";
10
10
  import { writeFileSync, mkdirSync, existsSync } from "fs";
11
11
  import { dirname } from "path";
12
- import { loadConfig, mergeConfigWithDefaults, resolveOutputPath } from "@aiready/core";
12
+ import {
13
+ loadConfig,
14
+ mergeConfigWithDefaults,
15
+ resolveOutputPath
16
+ } from "@aiready/core";
13
17
  var program = new Command();
14
- program.name("aiready-testability").description("Measure how safely AI-generated changes can be verified in your codebase").version("0.1.0").addHelpText("after", `
18
+ program.name("aiready-testability").description(
19
+ "Measure how safely AI-generated changes can be verified in your codebase"
20
+ ).version("0.1.0").addHelpText(
21
+ "after",
22
+ `
15
23
  DIMENSIONS MEASURED:
16
24
  Test Coverage Ratio of test files to source files
17
25
  Function Purity Pure functions are trivially AI-testable
@@ -29,7 +37,15 @@ EXAMPLES:
29
37
  aiready-testability . # Full analysis
30
38
  aiready-testability src/ --output json # JSON report
31
39
  aiready-testability . --min-coverage 0.5 # Stricter 50% threshold
32
- `).argument("<directory>", "Directory to analyze").option("--min-coverage <ratio>", "Minimum acceptable test/source ratio (default: 0.3)", "0.3").option("--test-patterns <patterns>", "Additional test file patterns (comma-separated)").option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-o, --output <format>", "Output format: console|json", "console").option("--output-file <path>", "Output file path (for json)").action(async (directory, options) => {
40
+ `
41
+ ).argument("<directory>", "Directory to analyze").option(
42
+ "--min-coverage <ratio>",
43
+ "Minimum acceptable test/source ratio (default: 0.3)",
44
+ "0.3"
45
+ ).option(
46
+ "--test-patterns <patterns>",
47
+ "Additional test file patterns (comma-separated)"
48
+ ).option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-o, --output <format>", "Output format: console|json", "console").option("--output-file <path>", "Output file path (for json)").action(async (directory, options) => {
33
49
  console.log(chalk.blue("\u{1F9EA} Analyzing testability...\n"));
34
50
  const startTime = Date.now();
35
51
  const config = await loadConfig(directory);
@@ -98,18 +114,32 @@ function displayConsoleReport(report, scoring, elapsed) {
98
114
  const safetyRating = summary.aiChangeSafetyRating;
99
115
  console.log(chalk.bold("\n\u{1F9EA} Testability Analysis\n"));
100
116
  if (safetyRating === "blind-risk") {
101
- console.log(chalk.bgRed.white.bold(
102
- " \u{1F480} BLIND RISK \u2014 NO TESTS DETECTED. AI-GENERATED CHANGES CANNOT BE VERIFIED. "
103
- ));
117
+ console.log(
118
+ chalk.bgRed.white.bold(
119
+ " \u{1F480} BLIND RISK \u2014 NO TESTS DETECTED. AI-GENERATED CHANGES CANNOT BE VERIFIED. "
120
+ )
121
+ );
104
122
  console.log();
105
123
  } else if (safetyRating === "high-risk") {
106
- console.log(chalk.red.bold(` \u{1F534} HIGH RISK \u2014 Insufficient test coverage. AI changes may introduce silent bugs.`));
124
+ console.log(
125
+ chalk.red.bold(
126
+ ` \u{1F534} HIGH RISK \u2014 Insufficient test coverage. AI changes may introduce silent bugs.`
127
+ )
128
+ );
107
129
  console.log();
108
130
  }
109
- console.log(`AI Change Safety: ${safetyColor(safetyRating)(`${safetyIcon(safetyRating)} ${safetyRating.toUpperCase()}`)}`);
110
- console.log(`Score: ${chalk.bold(summary.score + "/100")} (${summary.rating})`);
111
- console.log(`Source Files: ${chalk.cyan(rawData.sourceFiles)} Test Files: ${chalk.cyan(rawData.testFiles)}`);
112
- console.log(`Coverage Ratio: ${chalk.bold(Math.round(summary.coverageRatio * 100) + "%")}`);
131
+ console.log(
132
+ `AI Change Safety: ${safetyColor(safetyRating)(`${safetyIcon(safetyRating)} ${safetyRating.toUpperCase()}`)}`
133
+ );
134
+ console.log(
135
+ `Score: ${chalk.bold(summary.score + "/100")} (${summary.rating})`
136
+ );
137
+ console.log(
138
+ `Source Files: ${chalk.cyan(rawData.sourceFiles)} Test Files: ${chalk.cyan(rawData.testFiles)}`
139
+ );
140
+ console.log(
141
+ `Coverage Ratio: ${chalk.bold(Math.round(summary.coverageRatio * 100) + "%")}`
142
+ );
113
143
  console.log(`Analysis Time: ${chalk.gray(elapsed + "s")}
114
144
  `);
115
145
  console.log(chalk.bold("\u{1F4D0} Dimension Scores\n"));
@@ -129,7 +159,10 @@ function displayConsoleReport(report, scoring, elapsed) {
129
159
  for (const issue of issues) {
130
160
  const sev = issue.severity === "critical" ? chalk.red : issue.severity === "major" ? chalk.yellow : chalk.blue;
131
161
  console.log(`${sev(issue.severity.toUpperCase())} ${issue.message}`);
132
- if (issue.suggestion) console.log(` ${chalk.dim("\u2192")} ${chalk.italic(issue.suggestion)}`);
162
+ if (issue.suggestion)
163
+ console.log(
164
+ ` ${chalk.dim("\u2192")} ${chalk.italic(issue.suggestion)}`
165
+ );
133
166
  console.log();
134
167
  }
135
168
  }
package/dist/index.js CHANGED
@@ -31,7 +31,14 @@ var import_path = require("path");
31
31
  var import_typescript_estree = require("@typescript-eslint/typescript-estree");
32
32
  var import_core = require("@aiready/core");
33
33
  var SRC_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
34
- var DEFAULT_EXCLUDES = ["node_modules", "dist", ".git", "coverage", ".turbo", "build"];
34
+ var DEFAULT_EXCLUDES = [
35
+ "node_modules",
36
+ "dist",
37
+ ".git",
38
+ "coverage",
39
+ ".turbo",
40
+ "build"
41
+ ];
35
42
  var TEST_PATTERNS = [
36
43
  /\.(test|spec)\.(ts|tsx|js|jsx)$/,
37
44
  /__tests__\//,
@@ -107,8 +114,12 @@ function isPureFunction(fn) {
107
114
  let hasSideEffect = false;
108
115
  function walk(node) {
109
116
  if (node.type === "ReturnStatement" && node.argument) hasReturn = true;
110
- if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression") hasSideEffect = true;
111
- if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && ["console", "process", "window", "document", "fs"].includes(node.callee.object.name)) hasSideEffect = true;
117
+ if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
118
+ hasSideEffect = true;
119
+ if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && ["console", "process", "window", "document", "fs"].includes(
120
+ node.callee.object.name
121
+ ))
122
+ hasSideEffect = true;
112
123
  for (const key of Object.keys(node)) {
113
124
  if (key === "parent") continue;
114
125
  const child = node[key];
@@ -132,7 +143,8 @@ function hasExternalStateMutation(fn) {
132
143
  let found = false;
133
144
  function walk(node) {
134
145
  if (found) return;
135
- if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression") found = true;
146
+ if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
147
+ found = true;
136
148
  for (const key of Object.keys(node)) {
137
149
  if (key === "parent") continue;
138
150
  const child = node[key];
@@ -207,7 +219,16 @@ function detectTestFramework(rootDir) {
207
219
  ...pkg.dependencies ?? {},
208
220
  ...pkg.devDependencies ?? {}
209
221
  };
210
- const testFrameworks = ["jest", "vitest", "mocha", "jasmine", "ava", "tap", "pytest", "unittest"];
222
+ const testFrameworks = [
223
+ "jest",
224
+ "vitest",
225
+ "mocha",
226
+ "jasmine",
227
+ "ava",
228
+ "tap",
229
+ "pytest",
230
+ "unittest"
231
+ ];
211
232
  return testFrameworks.some((fw) => allDeps[fw]);
212
233
  } catch {
213
234
  return false;
@@ -215,7 +236,9 @@ function detectTestFramework(rootDir) {
215
236
  }
216
237
  async function analyzeTestability(options) {
217
238
  const allFiles = collectFiles(options.rootDir, options);
218
- const sourceFiles = allFiles.filter((f) => !isTestFile(f, options.testPatterns));
239
+ const sourceFiles = allFiles.filter(
240
+ (f) => !isTestFile(f, options.testPatterns)
241
+ );
219
242
  const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
220
243
  const aggregated = {
221
244
  pureFunctions: 0,
@@ -340,11 +363,13 @@ function calculateTestabilityScore(report) {
340
363
  description: `${rawData.externalStateMutations} functions mutate external state`
341
364
  }
342
365
  ];
343
- const recs = recommendations.map((action) => ({
344
- action,
345
- estimatedImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
346
- priority: summary.aiChangeSafetyRating === "blind-risk" || summary.aiChangeSafetyRating === "high-risk" ? "high" : "medium"
347
- }));
366
+ const recs = recommendations.map(
367
+ (action) => ({
368
+ action,
369
+ estimatedImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
370
+ priority: summary.aiChangeSafetyRating === "blind-risk" || summary.aiChangeSafetyRating === "high-risk" ? "high" : "medium"
371
+ })
372
+ );
348
373
  return {
349
374
  toolName: "testability",
350
375
  score: summary.score,
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  analyzeTestability,
3
3
  calculateTestabilityScore
4
- } from "./chunk-CYZ7DTWN.mjs";
4
+ } from "./chunk-YLYLRZRS.mjs";
5
5
  export {
6
6
  analyzeTestability,
7
7
  calculateTestabilityScore
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/testability",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Measures how safely and verifiably AI-generated changes can be made to your codebase",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -39,8 +39,8 @@
39
39
  "@typescript-eslint/typescript-estree": "^8.53.0",
40
40
  "chalk": "^5.3.0",
41
41
  "commander": "^14.0.0",
42
- "glob": "^11.0.0",
43
- "@aiready/core": "0.9.32"
42
+ "glob": "^13.0.0",
43
+ "@aiready/core": "0.9.33"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^24.0.0",
@@ -26,8 +26,14 @@ describe('Testability Analyzer', () => {
26
26
  describe('Test Coverage Ratio', () => {
27
27
  it('should calculate ratio of test files to source files', async () => {
28
28
  createTestFile('src/math.ts', 'export const add = (a, b) => a + b;');
29
- createTestFile('src/math.test.ts', 'import { add } from "./math"; test("add", () => {});');
30
- createTestFile('src/string.ts', 'export const upper = (s) => s.toUpperCase();');
29
+ createTestFile(
30
+ 'src/math.test.ts',
31
+ 'import { add } from "./math"; test("add", () => {});'
32
+ );
33
+ createTestFile(
34
+ 'src/string.ts',
35
+ 'export const upper = (s) => s.toUpperCase();'
36
+ );
31
37
 
32
38
  const report = await analyzeTestability({ rootDir: tmpDir });
33
39
 
@@ -38,7 +44,9 @@ describe('Testability Analyzer', () => {
38
44
 
39
45
  describe('Pure Functions and State Mutations', () => {
40
46
  it('should detect state mutations inside functions', async () => {
41
- createTestFile('src/mutations.ts', `
47
+ createTestFile(
48
+ 'src/mutations.ts',
49
+ `
42
50
  const globalState = { value: 0 };
43
51
 
44
52
  export function impureAdd(a: number) {
@@ -49,7 +57,8 @@ describe('Testability Analyzer', () => {
49
57
  export function pureAdd(a: number, b: number) {
50
58
  return a + b;
51
59
  }
52
- `);
60
+ `
61
+ );
53
62
 
54
63
  const report = await analyzeTestability({ rootDir: tmpDir });
55
64
 
@@ -60,7 +69,9 @@ describe('Testability Analyzer', () => {
60
69
 
61
70
  describe('Bloated Interfaces', () => {
62
71
  it('should detect interfaces with too many methods', async () => {
63
- createTestFile('src/interfaces.ts', `
72
+ createTestFile(
73
+ 'src/interfaces.ts',
74
+ `
64
75
  export interface BloatedService {
65
76
  m1(): void;
66
77
  m2(): void;
@@ -74,7 +85,8 @@ describe('Testability Analyzer', () => {
74
85
  m10(): void;
75
86
  m11(): void;
76
87
  }
77
- `);
88
+ `
89
+ );
78
90
 
79
91
  const report = await analyzeTestability({ rootDir: tmpDir });
80
92
 
package/src/analyzer.ts CHANGED
@@ -15,14 +15,25 @@ import { join, extname, basename } from 'path';
15
15
  import { parse } from '@typescript-eslint/typescript-estree';
16
16
  import type { TSESTree } from '@typescript-eslint/types';
17
17
  import { calculateTestabilityIndex } from '@aiready/core';
18
- import type { TestabilityOptions, TestabilityIssue, TestabilityReport } from './types';
18
+ import type {
19
+ TestabilityOptions,
20
+ TestabilityIssue,
21
+ TestabilityReport,
22
+ } from './types';
19
23
 
20
24
  // ---------------------------------------------------------------------------
21
25
  // File classification
22
26
  // ---------------------------------------------------------------------------
23
27
 
24
28
  const SRC_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);
25
- const DEFAULT_EXCLUDES = ['node_modules', 'dist', '.git', 'coverage', '.turbo', 'build'];
29
+ const DEFAULT_EXCLUDES = [
30
+ 'node_modules',
31
+ 'dist',
32
+ '.git',
33
+ 'coverage',
34
+ '.turbo',
35
+ 'build',
36
+ ];
26
37
  const TEST_PATTERNS = [
27
38
  /\.(test|spec)\.(ts|tsx|js|jsx)$/,
28
39
  /__tests__\//,
@@ -32,8 +43,8 @@ const TEST_PATTERNS = [
32
43
  ];
33
44
 
34
45
  function isTestFile(filePath: string, extra?: string[]): boolean {
35
- if (TEST_PATTERNS.some(p => p.test(filePath))) return true;
36
- if (extra) return extra.some(p => filePath.includes(p));
46
+ if (TEST_PATTERNS.some((p) => p.test(filePath))) return true;
47
+ if (extra) return extra.some((p) => filePath.includes(p));
37
48
  return false;
38
49
  }
39
50
 
@@ -44,7 +55,7 @@ function isSourceFile(filePath: string): boolean {
44
55
  function collectFiles(
45
56
  dir: string,
46
57
  options: TestabilityOptions,
47
- depth = 0,
58
+ depth = 0
48
59
  ): string[] {
49
60
  if (depth > (options.maxDepth ?? 20)) return [];
50
61
  const excludes = [...DEFAULT_EXCLUDES, ...(options.exclude ?? [])];
@@ -56,14 +67,18 @@ function collectFiles(
56
67
  return files;
57
68
  }
58
69
  for (const entry of entries) {
59
- if (excludes.some(ex => entry === ex || entry.includes(ex))) continue;
70
+ if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
60
71
  const full = join(dir, entry);
61
72
  let stat;
62
- try { stat = statSync(full); } catch { continue; }
73
+ try {
74
+ stat = statSync(full);
75
+ } catch {
76
+ continue;
77
+ }
63
78
  if (stat.isDirectory()) {
64
79
  files.push(...collectFiles(full, options, depth + 1));
65
80
  } else if (stat.isFile() && isSourceFile(full)) {
66
- if (!options.include || options.include.some(p => full.includes(p))) {
81
+ if (!options.include || options.include.some((p) => full.includes(p))) {
67
82
  files.push(full);
68
83
  }
69
84
  }
@@ -85,20 +100,27 @@ interface FileAnalysis {
85
100
  externalStateMutations: number;
86
101
  }
87
102
 
88
- function countMethodsInInterface(node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration): number {
103
+ function countMethodsInInterface(
104
+ node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration
105
+ ): number {
89
106
  // Count method signatures
90
107
  if (node.type === 'TSInterfaceDeclaration') {
91
- return node.body.body.filter(m =>
92
- m.type === 'TSMethodSignature' || m.type === 'TSPropertySignature'
108
+ return node.body.body.filter(
109
+ (m) => m.type === 'TSMethodSignature' || m.type === 'TSPropertySignature'
93
110
  ).length;
94
111
  }
95
- if (node.type === 'TSTypeAliasDeclaration' && node.typeAnnotation.type === 'TSTypeLiteral') {
112
+ if (
113
+ node.type === 'TSTypeAliasDeclaration' &&
114
+ node.typeAnnotation.type === 'TSTypeLiteral'
115
+ ) {
96
116
  return node.typeAnnotation.members.length;
97
117
  }
98
118
  return 0;
99
119
  }
100
120
 
101
- function hasDependencyInjection(node: TSESTree.ClassDeclaration | TSESTree.ClassExpression): boolean {
121
+ function hasDependencyInjection(
122
+ node: TSESTree.ClassDeclaration | TSESTree.ClassExpression
123
+ ): boolean {
102
124
  // Look for a constructor with typed parameters (the most common DI pattern)
103
125
  for (const member of node.body.body) {
104
126
  if (
@@ -109,10 +131,12 @@ function hasDependencyInjection(node: TSESTree.ClassDeclaration | TSESTree.Class
109
131
  const fn = member.value;
110
132
  if (fn.params && fn.params.length > 0) {
111
133
  // If constructor takes parameters that are typed class/interface references, that's DI
112
- const typedParams = fn.params.filter(p => {
134
+ const typedParams = fn.params.filter((p) => {
113
135
  const param = p as any;
114
- return param.typeAnnotation != null ||
115
- param.parameter?.typeAnnotation != null;
136
+ return (
137
+ param.typeAnnotation != null ||
138
+ param.parameter?.typeAnnotation != null
139
+ );
116
140
  });
117
141
  if (typedParams.length > 0) return true;
118
142
  }
@@ -122,7 +146,10 @@ function hasDependencyInjection(node: TSESTree.ClassDeclaration | TSESTree.Class
122
146
  }
123
147
 
124
148
  function isPureFunction(
125
- fn: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression,
149
+ fn:
150
+ | TSESTree.FunctionDeclaration
151
+ | TSESTree.FunctionExpression
152
+ | TSESTree.ArrowFunctionExpression
126
153
  ): boolean {
127
154
  let hasReturn = false;
128
155
  let hasSideEffect = false;
@@ -132,14 +159,18 @@ function isPureFunction(
132
159
  if (
133
160
  node.type === 'AssignmentExpression' &&
134
161
  node.left.type === 'MemberExpression'
135
- ) hasSideEffect = true;
162
+ )
163
+ hasSideEffect = true;
136
164
  // Calls to console, process, global objects
137
165
  if (
138
166
  node.type === 'CallExpression' &&
139
167
  node.callee.type === 'MemberExpression' &&
140
168
  node.callee.object.type === 'Identifier' &&
141
- ['console', 'process', 'window', 'document', 'fs'].includes(node.callee.object.name)
142
- ) hasSideEffect = true;
169
+ ['console', 'process', 'window', 'document', 'fs'].includes(
170
+ node.callee.object.name
171
+ )
172
+ )
173
+ hasSideEffect = true;
143
174
 
144
175
  // Recurse
145
176
  for (const key of Object.keys(node)) {
@@ -147,7 +178,7 @@ function isPureFunction(
147
178
  const child = (node as any)[key];
148
179
  if (child && typeof child === 'object') {
149
180
  if (Array.isArray(child)) {
150
- child.forEach(c => c?.type && walk(c));
181
+ child.forEach((c) => c?.type && walk(c));
151
182
  } else if (child.type) {
152
183
  walk(child);
153
184
  }
@@ -156,7 +187,7 @@ function isPureFunction(
156
187
  }
157
188
 
158
189
  if (fn.body?.type === 'BlockStatement') {
159
- fn.body.body.forEach(s => walk(s));
190
+ fn.body.body.forEach((s) => walk(s));
160
191
  } else if (fn.body) {
161
192
  hasReturn = true; // arrow expression body
162
193
  }
@@ -165,7 +196,10 @@ function isPureFunction(
165
196
  }
166
197
 
167
198
  function hasExternalStateMutation(
168
- fn: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression,
199
+ fn:
200
+ | TSESTree.FunctionDeclaration
201
+ | TSESTree.FunctionExpression
202
+ | TSESTree.ArrowFunctionExpression
169
203
  ): boolean {
170
204
  let found = false;
171
205
  function walk(node: TSESTree.Node) {
@@ -173,17 +207,18 @@ function hasExternalStateMutation(
173
207
  if (
174
208
  node.type === 'AssignmentExpression' &&
175
209
  node.left.type === 'MemberExpression'
176
- ) found = true;
210
+ )
211
+ found = true;
177
212
  for (const key of Object.keys(node)) {
178
213
  if (key === 'parent') continue;
179
214
  const child = (node as any)[key];
180
215
  if (child && typeof child === 'object') {
181
- if (Array.isArray(child)) child.forEach(c => c?.type && walk(c));
216
+ if (Array.isArray(child)) child.forEach((c) => c?.type && walk(c));
182
217
  else if (child.type) walk(child);
183
218
  }
184
219
  }
185
220
  }
186
- if (fn.body?.type === 'BlockStatement') fn.body.body.forEach(s => walk(s));
221
+ if (fn.body?.type === 'BlockStatement') fn.body.body.forEach((s) => walk(s));
187
222
  return found;
188
223
  }
189
224
 
@@ -199,7 +234,11 @@ function analyzeFileTestability(filePath: string): FileAnalysis {
199
234
  };
200
235
 
201
236
  let code: string;
202
- try { code = readFileSync(filePath, 'utf-8'); } catch { return result; }
237
+ try {
238
+ code = readFileSync(filePath, 'utf-8');
239
+ } catch {
240
+ return result;
241
+ }
203
242
 
204
243
  let ast: TSESTree.Program;
205
244
  try {
@@ -208,7 +247,9 @@ function analyzeFileTestability(filePath: string): FileAnalysis {
208
247
  range: false,
209
248
  loc: false,
210
249
  });
211
- } catch { return result; }
250
+ } catch {
251
+ return result;
252
+ }
212
253
 
213
254
  function visit(node: TSESTree.Node) {
214
255
  if (
@@ -226,7 +267,10 @@ function analyzeFileTestability(filePath: string): FileAnalysis {
226
267
  if (hasDependencyInjection(node)) result.injectionPatterns++;
227
268
  }
228
269
 
229
- if (node.type === 'TSInterfaceDeclaration' || node.type === 'TSTypeAliasDeclaration') {
270
+ if (
271
+ node.type === 'TSInterfaceDeclaration' ||
272
+ node.type === 'TSTypeAliasDeclaration'
273
+ ) {
230
274
  result.totalInterfaces++;
231
275
  const methodCount = countMethodsInInterface(node as any);
232
276
  if (methodCount > 10) result.bloatedInterfaces++;
@@ -237,7 +281,7 @@ function analyzeFileTestability(filePath: string): FileAnalysis {
237
281
  if (key === 'parent') continue;
238
282
  const child = (node as any)[key];
239
283
  if (child && typeof child === 'object') {
240
- if (Array.isArray(child)) child.forEach(c => c?.type && visit(c));
284
+ if (Array.isArray(child)) child.forEach((c) => c?.type && visit(c));
241
285
  else if (child.type) visit(child);
242
286
  }
243
287
  }
@@ -260,9 +304,20 @@ function detectTestFramework(rootDir: string): boolean {
260
304
  ...(pkg.dependencies ?? {}),
261
305
  ...(pkg.devDependencies ?? {}),
262
306
  };
263
- const testFrameworks = ['jest', 'vitest', 'mocha', 'jasmine', 'ava', 'tap', 'pytest', 'unittest'];
264
- return testFrameworks.some(fw => allDeps[fw]);
265
- } catch { return false; }
307
+ const testFrameworks = [
308
+ 'jest',
309
+ 'vitest',
310
+ 'mocha',
311
+ 'jasmine',
312
+ 'ava',
313
+ 'tap',
314
+ 'pytest',
315
+ 'unittest',
316
+ ];
317
+ return testFrameworks.some((fw) => allDeps[fw]);
318
+ } catch {
319
+ return false;
320
+ }
266
321
  }
267
322
 
268
323
  // ---------------------------------------------------------------------------
@@ -270,12 +325,14 @@ function detectTestFramework(rootDir: string): boolean {
270
325
  // ---------------------------------------------------------------------------
271
326
 
272
327
  export async function analyzeTestability(
273
- options: TestabilityOptions,
328
+ options: TestabilityOptions
274
329
  ): Promise<TestabilityReport> {
275
330
  const allFiles = collectFiles(options.rootDir, options);
276
331
 
277
- const sourceFiles = allFiles.filter(f => !isTestFile(f, options.testPatterns));
278
- const testFiles = allFiles.filter(f => isTestFile(f, options.testPatterns));
332
+ const sourceFiles = allFiles.filter(
333
+ (f) => !isTestFile(f, options.testPatterns)
334
+ );
335
+ const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
279
336
 
280
337
  const aggregated: FileAnalysis = {
281
338
  pureFunctions: 0,
@@ -312,21 +369,25 @@ export async function analyzeTestability(
312
369
  // Build issues
313
370
  const issues: TestabilityIssue[] = [];
314
371
  const minCoverage = options.minCoverageRatio ?? 0.3;
315
- const actualRatio = sourceFiles.length > 0 ? testFiles.length / sourceFiles.length : 0;
372
+ const actualRatio =
373
+ sourceFiles.length > 0 ? testFiles.length / sourceFiles.length : 0;
316
374
 
317
375
  if (!hasTestFramework) {
318
376
  issues.push({
319
377
  type: 'low-testability',
320
378
  dimension: 'framework',
321
379
  severity: 'critical',
322
- message: 'No testing framework detected in package.json — AI changes cannot be verified at all.',
380
+ message:
381
+ 'No testing framework detected in package.json — AI changes cannot be verified at all.',
323
382
  location: { file: options.rootDir, line: 0 },
324
- suggestion: 'Add Jest, Vitest, or another testing framework as a devDependency.',
383
+ suggestion:
384
+ 'Add Jest, Vitest, or another testing framework as a devDependency.',
325
385
  });
326
386
  }
327
387
 
328
388
  if (actualRatio < minCoverage) {
329
- const needed = Math.ceil(sourceFiles.length * minCoverage) - testFiles.length;
389
+ const needed =
390
+ Math.ceil(sourceFiles.length * minCoverage) - testFiles.length;
330
391
  issues.push({
331
392
  type: 'low-testability',
332
393
  dimension: 'test-coverage',
@@ -344,7 +405,8 @@ export async function analyzeTestability(
344
405
  severity: 'major',
345
406
  message: `Only ${indexResult.dimensions.purityScore}% of functions are pure — side-effectful functions require complex test setup.`,
346
407
  location: { file: options.rootDir, line: 0 },
347
- suggestion: 'Extract pure transformation logic from I/O and mutation code.',
408
+ suggestion:
409
+ 'Extract pure transformation logic from I/O and mutation code.',
348
410
  });
349
411
  }
350
412