@aiready/testability 0.4.15 → 0.4.17

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.js CHANGED
@@ -34,82 +34,7 @@ var import_core3 = require("@aiready/core");
34
34
  var import_core = require("@aiready/core");
35
35
  var import_fs = require("fs");
36
36
  var import_path = require("path");
37
- var import_typescript_estree = require("@typescript-eslint/typescript-estree");
38
- function countMethodsInInterface(node) {
39
- if (node.type === "TSInterfaceDeclaration") {
40
- return node.body.body.filter(
41
- (m) => m.type === "TSMethodSignature" || m.type === "TSPropertySignature"
42
- ).length;
43
- }
44
- if (node.type === "TSTypeAliasDeclaration" && node.typeAnnotation.type === "TSTypeLiteral") {
45
- return node.typeAnnotation.members.length;
46
- }
47
- return 0;
48
- }
49
- function hasDependencyInjection(node) {
50
- for (const member of node.body.body) {
51
- if (member.type === "MethodDefinition" && member.key.type === "Identifier" && member.key.name === "constructor") {
52
- const fn = member.value;
53
- if (fn.params && fn.params.length > 0) {
54
- const typedParams = fn.params.filter((p) => {
55
- const param = p;
56
- return param.typeAnnotation != null || param.parameter?.typeAnnotation != null;
57
- });
58
- if (typedParams.length > 0) return true;
59
- }
60
- }
61
- }
62
- return false;
63
- }
64
- function isPureFunction(fn) {
65
- let hasReturn = false;
66
- let hasSideEffect = false;
67
- function walk(node) {
68
- if (node.type === "ReturnStatement" && node.argument) hasReturn = true;
69
- if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
70
- hasSideEffect = true;
71
- if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && ["console", "process", "window", "document", "fs"].includes(
72
- node.callee.object.name
73
- ))
74
- hasSideEffect = true;
75
- for (const key of Object.keys(node)) {
76
- if (key === "parent") continue;
77
- const child = node[key];
78
- if (child && typeof child === "object") {
79
- if (Array.isArray(child)) {
80
- child.forEach((c) => c?.type && walk(c));
81
- } else if (child.type) {
82
- walk(child);
83
- }
84
- }
85
- }
86
- }
87
- if (fn.body?.type === "BlockStatement") {
88
- fn.body.body.forEach((s) => walk(s));
89
- } else if (fn.body) {
90
- hasReturn = true;
91
- }
92
- return hasReturn && !hasSideEffect;
93
- }
94
- function hasExternalStateMutation(fn) {
95
- let found = false;
96
- function walk(node) {
97
- if (found) return;
98
- if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
99
- found = true;
100
- for (const key of Object.keys(node)) {
101
- if (key === "parent") continue;
102
- const child = node[key];
103
- if (child && typeof child === "object") {
104
- if (Array.isArray(child)) child.forEach((c) => c?.type && walk(c));
105
- else if (child.type) walk(child);
106
- }
107
- }
108
- }
109
- if (fn.body?.type === "BlockStatement") fn.body.body.forEach((s) => walk(s));
110
- return found;
111
- }
112
- function analyzeFileTestability(filePath) {
37
+ async function analyzeFileTestability(filePath) {
113
38
  const result = {
114
39
  pureFunctions: 0,
115
40
  totalFunctions: 0,
@@ -119,75 +44,79 @@ function analyzeFileTestability(filePath) {
119
44
  totalInterfaces: 0,
120
45
  externalStateMutations: 0
121
46
  };
47
+ const parser = (0, import_core.getParser)(filePath);
48
+ if (!parser) return result;
122
49
  let code;
123
50
  try {
124
51
  code = (0, import_fs.readFileSync)(filePath, "utf-8");
125
52
  } catch {
126
53
  return result;
127
54
  }
128
- let ast;
129
55
  try {
130
- ast = (0, import_typescript_estree.parse)(code, {
131
- jsx: filePath.endsWith(".tsx") || filePath.endsWith(".jsx"),
132
- range: false,
133
- loc: false
134
- });
135
- } catch {
136
- return result;
137
- }
138
- function visit(node) {
139
- if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
140
- result.totalFunctions++;
141
- if (isPureFunction(node)) result.pureFunctions++;
142
- if (hasExternalStateMutation(node)) result.externalStateMutations++;
143
- }
144
- if (node.type === "ClassDeclaration" || node.type === "ClassExpression") {
145
- result.totalClasses++;
146
- if (hasDependencyInjection(node)) result.injectionPatterns++;
147
- }
148
- if (node.type === "TSInterfaceDeclaration" || node.type === "TSTypeAliasDeclaration") {
149
- result.totalInterfaces++;
150
- const methodCount = countMethodsInInterface(node);
151
- if (methodCount > 10) result.bloatedInterfaces++;
152
- }
153
- for (const key of Object.keys(node)) {
154
- if (key === "parent") continue;
155
- const child = node[key];
156
- if (child && typeof child === "object") {
157
- if (Array.isArray(child)) child.forEach((c) => c?.type && visit(c));
158
- else if (child.type) visit(child);
56
+ await parser.initialize();
57
+ const parseResult = parser.parse(code, filePath);
58
+ for (const exp of parseResult.exports) {
59
+ if (exp.type === "function") {
60
+ result.totalFunctions++;
61
+ if (exp.isPure) result.pureFunctions++;
62
+ if (exp.hasSideEffects) result.externalStateMutations++;
63
+ }
64
+ if (exp.type === "class") {
65
+ result.totalClasses++;
66
+ if (exp.parameters && exp.parameters.length > 0) {
67
+ result.injectionPatterns++;
68
+ }
69
+ const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
70
+ if (total > 5) {
71
+ result.bloatedInterfaces++;
72
+ }
73
+ }
74
+ if (exp.type === "interface") {
75
+ result.totalInterfaces++;
76
+ const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
77
+ if (total > 5) {
78
+ result.bloatedInterfaces++;
79
+ }
159
80
  }
160
81
  }
82
+ } catch (error) {
83
+ console.warn(`Testability: Failed to parse ${filePath}: ${error}`);
161
84
  }
162
- ast.body.forEach(visit);
163
85
  return result;
164
86
  }
165
87
  function detectTestFramework(rootDir) {
166
- const pkgPath = (0, import_path.join)(rootDir, "package.json");
167
- if (!(0, import_fs.existsSync)(pkgPath)) return false;
168
- try {
169
- const pkg = JSON.parse((0, import_fs.readFileSync)(pkgPath, "utf-8"));
170
- const allDeps = {
171
- ...pkg.dependencies ?? {},
172
- ...pkg.devDependencies ?? {}
173
- };
174
- const testFrameworks = [
175
- "jest",
176
- "vitest",
177
- "mocha",
178
- "jasmine",
179
- "ava",
180
- "tap",
181
- "pytest",
182
- "unittest"
183
- ];
184
- return testFrameworks.some((fw) => allDeps[fw]);
185
- } catch {
186
- return false;
88
+ const manifests = [
89
+ {
90
+ file: "package.json",
91
+ deps: ["jest", "vitest", "mocha", "mocha", "jasmine", "ava", "tap"]
92
+ },
93
+ { file: "requirements.txt", deps: ["pytest", "unittest", "nose"] },
94
+ { file: "pyproject.toml", deps: ["pytest"] },
95
+ { file: "pom.xml", deps: ["junit", "testng"] },
96
+ { file: "build.gradle", deps: ["junit", "testng"] },
97
+ { file: "go.mod", deps: ["testing"] }
98
+ // go testing is built-in
99
+ ];
100
+ for (const m of manifests) {
101
+ const p = (0, import_path.join)(rootDir, m.file);
102
+ if ((0, import_fs.existsSync)(p)) {
103
+ if (m.file === "go.mod") return true;
104
+ try {
105
+ const content = (0, import_fs.readFileSync)(p, "utf-8");
106
+ if (m.deps.some((d) => content.includes(d))) return true;
107
+ } catch {
108
+ }
109
+ }
187
110
  }
111
+ return false;
188
112
  }
189
113
  var TEST_PATTERNS = [
190
114
  /\.(test|spec)\.(ts|tsx|js|jsx)$/,
115
+ /_test\.go$/,
116
+ /test_.*\.py$/,
117
+ /.*_test\.py$/,
118
+ /.*Test\.java$/,
119
+ /.*Tests\.cs$/,
191
120
  /__tests__\//,
192
121
  /\/tests?\//,
193
122
  /\/e2e\//,
@@ -201,7 +130,7 @@ function isTestFile(filePath, extra) {
201
130
  async function analyzeTestability(options) {
202
131
  const allFiles = await (0, import_core.scanFiles)({
203
132
  ...options,
204
- include: options.include || ["**/*.{ts,tsx,js,jsx}"],
133
+ include: options.include || ["**/*.{ts,tsx,js,jsx,py,java,cs,go}"],
205
134
  includeTests: true
206
135
  });
207
136
  const sourceFiles = allFiles.filter(
@@ -227,7 +156,7 @@ async function analyzeTestability(options) {
227
156
  "analyzing files",
228
157
  options.onProgress
229
158
  );
230
- const a = analyzeFileTestability(f);
159
+ const a = await analyzeFileTestability(f);
231
160
  for (const key of Object.keys(aggregated)) {
232
161
  aggregated[key] += a[key];
233
162
  }
@@ -253,9 +182,9 @@ async function analyzeTestability(options) {
253
182
  type: import_core.IssueType.LowTestability,
254
183
  dimension: "framework",
255
184
  severity: import_core.Severity.Critical,
256
- message: "No testing framework detected in package.json \u2014 AI changes cannot be verified at all.",
185
+ message: "No major testing framework detected \u2014 AI changes cannot be safely verified.",
257
186
  location: { file: options.rootDir, line: 0 },
258
- suggestion: "Add Jest, Vitest, or another testing framework as a devDependency."
187
+ suggestion: "Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification."
259
188
  });
260
189
  }
261
190
  if (actualRatio < minCoverage) {
@@ -274,19 +203,9 @@ async function analyzeTestability(options) {
274
203
  type: import_core.IssueType.LowTestability,
275
204
  dimension: "purity",
276
205
  severity: import_core.Severity.Major,
277
- message: `Only ${indexResult.dimensions.purityScore}% of functions are pure \u2014 side-effectful functions require complex test setup.`,
278
- location: { file: options.rootDir, line: 0 },
279
- suggestion: "Extract pure transformation logic from I/O and mutation code."
280
- });
281
- }
282
- if (indexResult.dimensions.observabilityScore < 50) {
283
- issues.push({
284
- type: import_core.IssueType.LowTestability,
285
- dimension: "observability",
286
- severity: import_core.Severity.Major,
287
- message: `Many functions mutate external state directly \u2014 outputs are invisible to unit tests.`,
206
+ message: `Only ${indexResult.dimensions.purityScore}% of functions appear pure \u2014 side-effectful code is harder for AI to verify safely.`,
288
207
  location: { file: options.rootDir, line: 0 },
289
- suggestion: "Prefer returning values over mutating shared state."
208
+ suggestion: "Refactor complex side-effectful logic into pure functions where possible."
290
209
  });
291
210
  }
292
211
  return {
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  analyzeTestability,
3
3
  calculateTestabilityScore
4
- } from "./chunk-LJLAJRZR.mjs";
4
+ } from "./chunk-QOIBI5E7.mjs";
5
5
 
6
6
  // src/index.ts
7
7
  import { ToolRegistry } from "@aiready/core";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/testability",
3
- "version": "0.4.15",
3
+ "version": "0.4.17",
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",
@@ -40,7 +40,7 @@
40
40
  "chalk": "^5.3.0",
41
41
  "commander": "^14.0.0",
42
42
  "glob": "^13.0.0",
43
- "@aiready/core": "0.21.15"
43
+ "@aiready/core": "0.21.17"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^24.0.0",
package/src/analyzer.ts CHANGED
@@ -1,26 +1,14 @@
1
- /**
2
- * Testability analyzer.
3
- *
4
- * Walks the codebase and measures 5 structural dimensions that determine
5
- * whether AI-generated changes can be safely verified:
6
- * 1. Test file coverage ratio
7
- * 2. Pure function prevalence
8
- * 3. Dependency injection patterns
9
- * 4. Interface focus (bloated interface detection)
10
- * 5. Observability (return values vs. external state mutations)
11
- */
12
-
13
1
  import {
14
2
  scanFiles,
15
3
  calculateTestabilityIndex,
16
4
  Severity,
17
5
  IssueType,
18
6
  emitProgress,
7
+ getParser,
8
+ Language,
19
9
  } from '@aiready/core';
20
10
  import { readFileSync, existsSync } from 'fs';
21
11
  import { join } from 'path';
22
- import { parse } from '@typescript-eslint/typescript-estree';
23
- import type { TSESTree } from '@typescript-eslint/types';
24
12
  import type {
25
13
  TestabilityOptions,
26
14
  TestabilityIssue,
@@ -41,129 +29,7 @@ interface FileAnalysis {
41
29
  externalStateMutations: number;
42
30
  }
43
31
 
44
- function countMethodsInInterface(
45
- node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration
46
- ): number {
47
- // Count method signatures
48
- if (node.type === 'TSInterfaceDeclaration') {
49
- return node.body.body.filter(
50
- (m) => m.type === 'TSMethodSignature' || m.type === 'TSPropertySignature'
51
- ).length;
52
- }
53
- if (
54
- node.type === 'TSTypeAliasDeclaration' &&
55
- node.typeAnnotation.type === 'TSTypeLiteral'
56
- ) {
57
- return node.typeAnnotation.members.length;
58
- }
59
- return 0;
60
- }
61
-
62
- function hasDependencyInjection(
63
- node: TSESTree.ClassDeclaration | TSESTree.ClassExpression
64
- ): boolean {
65
- // Look for a constructor with typed parameters (the most common DI pattern)
66
- for (const member of node.body.body) {
67
- if (
68
- member.type === 'MethodDefinition' &&
69
- member.key.type === 'Identifier' &&
70
- member.key.name === 'constructor'
71
- ) {
72
- const fn = member.value;
73
- if (fn.params && fn.params.length > 0) {
74
- // If constructor takes parameters that are typed class/interface references, that's DI
75
- const typedParams = fn.params.filter((p) => {
76
- const param = p as any;
77
- return (
78
- param.typeAnnotation != null ||
79
- param.parameter?.typeAnnotation != null
80
- );
81
- });
82
- if (typedParams.length > 0) return true;
83
- }
84
- }
85
- }
86
- return false;
87
- }
88
-
89
- function isPureFunction(
90
- fn:
91
- | TSESTree.FunctionDeclaration
92
- | TSESTree.FunctionExpression
93
- | TSESTree.ArrowFunctionExpression
94
- ): boolean {
95
- let hasReturn = false;
96
- let hasSideEffect = false;
97
-
98
- function walk(node: TSESTree.Node) {
99
- if (node.type === 'ReturnStatement' && node.argument) hasReturn = true;
100
- if (
101
- node.type === 'AssignmentExpression' &&
102
- node.left.type === 'MemberExpression'
103
- )
104
- hasSideEffect = true;
105
- // Calls to console, process, global objects
106
- if (
107
- node.type === 'CallExpression' &&
108
- node.callee.type === 'MemberExpression' &&
109
- node.callee.object.type === 'Identifier' &&
110
- ['console', 'process', 'window', 'document', 'fs'].includes(
111
- node.callee.object.name
112
- )
113
- )
114
- hasSideEffect = true;
115
-
116
- // Recurse
117
- for (const key of Object.keys(node)) {
118
- if (key === 'parent') continue;
119
- const child = (node as any)[key];
120
- if (child && typeof child === 'object') {
121
- if (Array.isArray(child)) {
122
- child.forEach((c) => c?.type && walk(c));
123
- } else if (child.type) {
124
- walk(child);
125
- }
126
- }
127
- }
128
- }
129
-
130
- if (fn.body?.type === 'BlockStatement') {
131
- fn.body.body.forEach((s) => walk(s));
132
- } else if (fn.body) {
133
- hasReturn = true; // arrow expression body
134
- }
135
-
136
- return hasReturn && !hasSideEffect;
137
- }
138
-
139
- function hasExternalStateMutation(
140
- fn:
141
- | TSESTree.FunctionDeclaration
142
- | TSESTree.FunctionExpression
143
- | TSESTree.ArrowFunctionExpression
144
- ): boolean {
145
- let found = false;
146
- function walk(node: TSESTree.Node) {
147
- if (found) return;
148
- if (
149
- node.type === 'AssignmentExpression' &&
150
- node.left.type === 'MemberExpression'
151
- )
152
- found = true;
153
- for (const key of Object.keys(node)) {
154
- if (key === 'parent') continue;
155
- const child = (node as any)[key];
156
- if (child && typeof child === 'object') {
157
- if (Array.isArray(child)) child.forEach((c) => c?.type && walk(c));
158
- else if (child.type) walk(child);
159
- }
160
- }
161
- }
162
- if (fn.body?.type === 'BlockStatement') fn.body.body.forEach((s) => walk(s));
163
- return found;
164
- }
165
-
166
- function analyzeFileTestability(filePath: string): FileAnalysis {
32
+ async function analyzeFileTestability(filePath: string): Promise<FileAnalysis> {
167
33
  const result: FileAnalysis = {
168
34
  pureFunctions: 0,
169
35
  totalFunctions: 0,
@@ -174,6 +40,9 @@ function analyzeFileTestability(filePath: string): FileAnalysis {
174
40
  externalStateMutations: 0,
175
41
  };
176
42
 
43
+ const parser = getParser(filePath);
44
+ if (!parser) return result;
45
+
177
46
  let code: string;
178
47
  try {
179
48
  code = readFileSync(filePath, 'utf-8');
@@ -181,54 +50,43 @@ function analyzeFileTestability(filePath: string): FileAnalysis {
181
50
  return result;
182
51
  }
183
52
 
184
- let ast: TSESTree.Program;
185
53
  try {
186
- ast = parse(code, {
187
- jsx: filePath.endsWith('.tsx') || filePath.endsWith('.jsx'),
188
- range: false,
189
- loc: false,
190
- });
191
- } catch {
192
- return result;
193
- }
194
-
195
- function visit(node: TSESTree.Node) {
196
- if (
197
- node.type === 'FunctionDeclaration' ||
198
- node.type === 'FunctionExpression' ||
199
- node.type === 'ArrowFunctionExpression'
200
- ) {
201
- result.totalFunctions++;
202
- if (isPureFunction(node)) result.pureFunctions++;
203
- if (hasExternalStateMutation(node)) result.externalStateMutations++;
204
- }
205
-
206
- if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
207
- result.totalClasses++;
208
- if (hasDependencyInjection(node)) result.injectionPatterns++;
209
- }
54
+ await parser.initialize();
55
+ const parseResult = parser.parse(code, filePath);
56
+
57
+ for (const exp of parseResult.exports) {
58
+ if (exp.type === 'function') {
59
+ result.totalFunctions++;
60
+ if (exp.isPure) result.pureFunctions++;
61
+ if (exp.hasSideEffects) result.externalStateMutations++;
62
+ }
210
63
 
211
- if (
212
- node.type === 'TSInterfaceDeclaration' ||
213
- node.type === 'TSTypeAliasDeclaration'
214
- ) {
215
- result.totalInterfaces++;
216
- const methodCount = countMethodsInInterface(node as any);
217
- if (methodCount > 10) result.bloatedInterfaces++;
218
- }
64
+ if (exp.type === 'class') {
65
+ result.totalClasses++;
66
+ // Generalized DI heuristic: constructor/initializer with parameters
67
+ if (exp.parameters && exp.parameters.length > 0) {
68
+ result.injectionPatterns++;
69
+ }
70
+ // Heuristic: bloated classes
71
+ const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
72
+ if (total > 5) {
73
+ result.bloatedInterfaces++;
74
+ }
75
+ }
219
76
 
220
- // Recurse
221
- for (const key of Object.keys(node)) {
222
- if (key === 'parent') continue;
223
- const child = (node as any)[key];
224
- if (child && typeof child === 'object') {
225
- if (Array.isArray(child)) child.forEach((c) => c?.type && visit(c));
226
- else if (child.type) visit(child);
77
+ if (exp.type === 'interface') {
78
+ result.totalInterfaces++;
79
+ // Heuristic: interfaces with many methods/props are considered bloated
80
+ const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
81
+ if (total > 5) {
82
+ result.bloatedInterfaces++;
83
+ }
227
84
  }
228
85
  }
86
+ } catch (error) {
87
+ console.warn(`Testability: Failed to parse ${filePath}: ${error}`);
229
88
  }
230
89
 
231
- ast.body.forEach(visit);
232
90
  return result;
233
91
  }
234
92
 
@@ -237,28 +95,30 @@ function analyzeFileTestability(filePath: string): FileAnalysis {
237
95
  // ---------------------------------------------------------------------------
238
96
 
239
97
  function detectTestFramework(rootDir: string): boolean {
240
- const pkgPath = join(rootDir, 'package.json');
241
- if (!existsSync(pkgPath)) return false;
242
- try {
243
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
244
- const allDeps = {
245
- ...(pkg.dependencies ?? {}),
246
- ...(pkg.devDependencies ?? {}),
247
- };
248
- const testFrameworks = [
249
- 'jest',
250
- 'vitest',
251
- 'mocha',
252
- 'jasmine',
253
- 'ava',
254
- 'tap',
255
- 'pytest',
256
- 'unittest',
257
- ];
258
- return testFrameworks.some((fw) => allDeps[fw]);
259
- } catch {
260
- return false;
98
+ // Check common manifest files
99
+ const manifests = [
100
+ {
101
+ file: 'package.json',
102
+ deps: ['jest', 'vitest', 'mocha', 'mocha', 'jasmine', 'ava', 'tap'],
103
+ },
104
+ { file: 'requirements.txt', deps: ['pytest', 'unittest', 'nose'] },
105
+ { file: 'pyproject.toml', deps: ['pytest'] },
106
+ { file: 'pom.xml', deps: ['junit', 'testng'] },
107
+ { file: 'build.gradle', deps: ['junit', 'testng'] },
108
+ { file: 'go.mod', deps: ['testing'] }, // go testing is built-in
109
+ ];
110
+
111
+ for (const m of manifests) {
112
+ const p = join(rootDir, m.file);
113
+ if (existsSync(p)) {
114
+ if (m.file === 'go.mod') return true; // built-in
115
+ try {
116
+ const content = readFileSync(p, 'utf-8');
117
+ if (m.deps.some((d) => content.includes(d))) return true;
118
+ } catch {}
119
+ }
261
120
  }
121
+ return false;
262
122
  }
263
123
 
264
124
  // ---------------------------------------------------------------------------
@@ -267,6 +127,11 @@ function detectTestFramework(rootDir: string): boolean {
267
127
 
268
128
  const TEST_PATTERNS = [
269
129
  /\.(test|spec)\.(ts|tsx|js|jsx)$/,
130
+ /_test\.go$/,
131
+ /test_.*\.py$/,
132
+ /.*_test\.py$/,
133
+ /.*Test\.java$/,
134
+ /.*Tests\.cs$/,
270
135
  /__tests__\//,
271
136
  /\/tests?\//,
272
137
  /\/e2e\//,
@@ -285,7 +150,7 @@ export async function analyzeTestability(
285
150
  // Use core scanFiles which respects .gitignore recursively
286
151
  const allFiles = await scanFiles({
287
152
  ...options,
288
- include: options.include || ['**/*.{ts,tsx,js,jsx}'],
153
+ include: options.include || ['**/*.{ts,tsx,js,jsx,py,java,cs,go}'],
289
154
  includeTests: true,
290
155
  });
291
156
 
@@ -315,7 +180,7 @@ export async function analyzeTestability(
315
180
  options.onProgress
316
181
  );
317
182
 
318
- const a = analyzeFileTestability(f);
183
+ const a = await analyzeFileTestability(f);
319
184
  for (const key of Object.keys(aggregated) as Array<keyof FileAnalysis>) {
320
185
  aggregated[key] += a[key];
321
186
  }
@@ -348,10 +213,10 @@ export async function analyzeTestability(
348
213
  dimension: 'framework',
349
214
  severity: Severity.Critical,
350
215
  message:
351
- 'No testing framework detected in package.json — AI changes cannot be verified at all.',
216
+ 'No major testing framework detected — AI changes cannot be safely verified.',
352
217
  location: { file: options.rootDir, line: 0 },
353
218
  suggestion:
354
- 'Add Jest, Vitest, or another testing framework as a devDependency.',
219
+ 'Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification.',
355
220
  });
356
221
  }
357
222
 
@@ -373,21 +238,10 @@ export async function analyzeTestability(
373
238
  type: IssueType.LowTestability,
374
239
  dimension: 'purity',
375
240
  severity: Severity.Major,
376
- message: `Only ${indexResult.dimensions.purityScore}% of functions are pure — side-effectful functions require complex test setup.`,
241
+ message: `Only ${indexResult.dimensions.purityScore}% of functions appear pure — side-effectful code is harder for AI to verify safely.`,
377
242
  location: { file: options.rootDir, line: 0 },
378
243
  suggestion:
379
- 'Extract pure transformation logic from I/O and mutation code.',
380
- });
381
- }
382
-
383
- if (indexResult.dimensions.observabilityScore < 50) {
384
- issues.push({
385
- type: IssueType.LowTestability,
386
- dimension: 'observability',
387
- severity: Severity.Major,
388
- message: `Many functions mutate external state directly — outputs are invisible to unit tests.`,
389
- location: { file: options.rootDir, line: 0 },
390
- suggestion: 'Prefer returning values over mutating shared state.',
244
+ 'Refactor complex side-effectful logic into pure functions where possible.',
391
245
  });
392
246
  }
393
247