@aiready/testability 0.6.6 → 0.6.7

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,24 +1,23 @@
1
-
2
- 
3
- > @aiready/testability@0.6.6 build /Users/pengcao/projects/aiready/packages/testability
4
- > tsup src/index.ts src/cli.ts --format cjs,esm --dts
5
-
6
- CLI Building entry: src/cli.ts, src/index.ts
7
- CLI Using tsconfig: tsconfig.json
8
- CLI tsup v8.5.1
9
- CLI Target: es2020
10
- CJS Build start
11
- ESM Build start
12
- CJS dist/cli.js 15.61 KB
13
- CJS dist/index.js 10.61 KB
14
- CJS ⚡️ Build success in 483ms
15
- ESM dist/index.mjs 1.17 KB
16
- ESM dist/chunk-QOIBI5E7.mjs 8.31 KB
17
- ESM dist/cli.mjs 5.75 KB
18
- ESM ⚡️ Build success in 483ms
19
- DTS Build start
20
- DTS ⚡️ Build success in 4452ms
21
- DTS dist/cli.d.ts 20.00 B
22
- DTS dist/index.d.ts 2.62 KB
23
- DTS dist/cli.d.mts 20.00 B
24
- DTS dist/index.d.mts 2.62 KB
1
+
2
+ > @aiready/testability@0.6.7 build /Users/pengcao/projects/aiready/packages/testability
3
+ > tsup src/index.ts src/cli.ts --format cjs,esm --dts
4
+
5
+ CLI Building entry: src/cli.ts, src/index.ts
6
+ CLI Using tsconfig: tsconfig.json
7
+ CLI tsup v8.5.1
8
+ CLI Target: es2020
9
+ CJS Build start
10
+ ESM Build start
11
+ ESM dist/chunk-HIE74PK3.mjs 8.31 KB
12
+ ESM dist/cli.mjs 5.75 KB
13
+ ESM dist/index.mjs 1.17 KB
14
+ ESM ⚡️ Build success in 128ms
15
+ CJS dist/index.js 10.61 KB
16
+ CJS dist/cli.js 15.61 KB
17
+ CJS ⚡️ Build success in 128ms
18
+ DTS Build start
19
+ DTS ⚡️ Build success in 5667ms
20
+ DTS dist/cli.d.ts 20.00 B
21
+ DTS dist/index.d.ts 2.62 KB
22
+ DTS dist/cli.d.mts 20.00 B
23
+ DTS dist/index.d.mts 2.62 KB
@@ -1,20 +1,18 @@
1
-
2
- 
3
- > @aiready/testability@0.6.5 test /Users/pengcao/projects/aiready/packages/testability
4
- > vitest run
5
-
6
- [?25l
7
-  RUN  v4.0.18 /Users/pengcao/projects/aiready/packages/testability
8
-
9
- ✓ src/__tests__/types.test.ts (4 tests) 2ms
10
- ✓ src/__tests__/scoring.test.ts (3 tests) 10ms
11
- ✓ src/__tests__/provider.test.ts (2 tests) 16ms
12
- ✓ src/__tests__/analyzer.test.ts (5 tests) 4638ms
13
- ✓ detects test frameworks in multiple languages  4611ms
14
-
15
-  Test Files  4 passed (4)
16
-  Tests  14 passed (14)
17
-  Start at  10:12:34
18
-  Duration  9.12s (transform 1.51s, setup 0ms, import 9.44s, tests 4.66s, environment 0ms)
19
-
20
- [?25h
1
+
2
+ > @aiready/testability@0.6.6 test /Users/pengcao/projects/aiready/packages/testability
3
+ > vitest run
4
+
5
+
6
+  RUN  v4.0.18 /Users/pengcao/projects/aiready/packages/testability
7
+
8
+ ✓ src/__tests__/types.test.ts (4 tests) 2ms
9
+ ✓ src/__tests__/scoring.test.ts (3 tests) 2ms
10
+ ✓ src/__tests__/provider.test.ts (2 tests) 4ms
11
+ ✓ src/__tests__/analyzer.test.ts (5 tests) 2336ms
12
+ ✓ detects test frameworks in multiple languages  2304ms
13
+
14
+  Test Files  4 passed (4)
15
+  Tests  14 passed (14)
16
+  Start at  15:20:15
17
+  Duration  5.21s (transform 2.14s, setup 0ms, import 6.67s, tests 2.34s, environment 0ms)
18
+
@@ -0,0 +1,262 @@
1
+ // src/analyzer.ts
2
+ import {
3
+ scanFiles,
4
+ calculateTestabilityIndex,
5
+ Severity,
6
+ IssueType,
7
+ emitProgress,
8
+ getParser
9
+ } from "@aiready/core";
10
+ import { readFileSync, existsSync } from "fs";
11
+ import { join } from "path";
12
+ async function analyzeFileTestability(filePath) {
13
+ const result = {
14
+ pureFunctions: 0,
15
+ totalFunctions: 0,
16
+ injectionPatterns: 0,
17
+ totalClasses: 0,
18
+ bloatedInterfaces: 0,
19
+ totalInterfaces: 0,
20
+ externalStateMutations: 0
21
+ };
22
+ const parser = getParser(filePath);
23
+ if (!parser) return result;
24
+ let code;
25
+ try {
26
+ code = readFileSync(filePath, "utf-8");
27
+ } catch {
28
+ return result;
29
+ }
30
+ try {
31
+ await parser.initialize();
32
+ const parseResult = parser.parse(code, filePath);
33
+ for (const exp of parseResult.exports) {
34
+ if (exp.type === "function") {
35
+ result.totalFunctions++;
36
+ if (exp.isPure) result.pureFunctions++;
37
+ if (exp.hasSideEffects) result.externalStateMutations++;
38
+ }
39
+ if (exp.type === "class") {
40
+ result.totalClasses++;
41
+ if (exp.parameters && exp.parameters.length > 0) {
42
+ result.injectionPatterns++;
43
+ }
44
+ const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
45
+ if (total > 10) {
46
+ result.bloatedInterfaces++;
47
+ }
48
+ }
49
+ if (exp.type === "interface") {
50
+ result.totalInterfaces++;
51
+ const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
52
+ if (total > 10) {
53
+ result.bloatedInterfaces++;
54
+ }
55
+ }
56
+ }
57
+ } catch (error) {
58
+ console.warn(`Testability: Failed to parse ${filePath}: ${error}`);
59
+ }
60
+ return result;
61
+ }
62
+ function detectTestFramework(rootDir) {
63
+ const manifests = [
64
+ {
65
+ file: "package.json",
66
+ deps: ["jest", "vitest", "mocha", "mocha", "jasmine", "ava", "tap"]
67
+ },
68
+ { file: "requirements.txt", deps: ["pytest", "unittest", "nose"] },
69
+ { file: "pyproject.toml", deps: ["pytest"] },
70
+ { file: "pom.xml", deps: ["junit", "testng"] },
71
+ { file: "build.gradle", deps: ["junit", "testng"] },
72
+ { file: "go.mod", deps: ["testing"] }
73
+ // go testing is built-in
74
+ ];
75
+ for (const m of manifests) {
76
+ const p = join(rootDir, m.file);
77
+ if (existsSync(p)) {
78
+ if (m.file === "go.mod") return true;
79
+ try {
80
+ const content = readFileSync(p, "utf-8");
81
+ if (m.deps.some((d) => content.includes(d))) return true;
82
+ } catch {
83
+ }
84
+ }
85
+ }
86
+ return false;
87
+ }
88
+ var TEST_PATTERNS = [
89
+ /\.(test|spec)\.(ts|tsx|js|jsx)$/,
90
+ /_test\.go$/,
91
+ /test_.*\.py$/,
92
+ /.*_test\.py$/,
93
+ /.*Test\.java$/,
94
+ /.*Tests\.cs$/,
95
+ /__tests__\//,
96
+ /\/tests?\//,
97
+ /\/e2e\//,
98
+ /\/fixtures\//
99
+ ];
100
+ function isTestFile(filePath, extra) {
101
+ if (TEST_PATTERNS.some((p) => p.test(filePath))) return true;
102
+ if (extra) return extra.some((p) => filePath.includes(p));
103
+ return false;
104
+ }
105
+ async function analyzeTestability(options) {
106
+ const allFiles = await scanFiles({
107
+ ...options,
108
+ include: options.include || ["**/*.{ts,tsx,js,jsx,py,java,cs,go}"],
109
+ includeTests: true
110
+ });
111
+ const sourceFiles = allFiles.filter(
112
+ (f) => !isTestFile(f, options.testPatterns)
113
+ );
114
+ const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
115
+ const aggregated = {
116
+ pureFunctions: 0,
117
+ totalFunctions: 0,
118
+ injectionPatterns: 0,
119
+ totalClasses: 0,
120
+ bloatedInterfaces: 0,
121
+ totalInterfaces: 0,
122
+ externalStateMutations: 0
123
+ };
124
+ let processed = 0;
125
+ for (const f of sourceFiles) {
126
+ processed++;
127
+ emitProgress(
128
+ processed,
129
+ sourceFiles.length,
130
+ "testability",
131
+ "analyzing files",
132
+ options.onProgress
133
+ );
134
+ const a = await analyzeFileTestability(f);
135
+ for (const key of Object.keys(aggregated)) {
136
+ aggregated[key] += a[key];
137
+ }
138
+ }
139
+ const hasTestFramework = detectTestFramework(options.rootDir);
140
+ const indexResult = calculateTestabilityIndex({
141
+ testFiles: testFiles.length,
142
+ sourceFiles: sourceFiles.length,
143
+ pureFunctions: aggregated.pureFunctions,
144
+ totalFunctions: Math.max(1, aggregated.totalFunctions),
145
+ injectionPatterns: aggregated.injectionPatterns,
146
+ totalClasses: Math.max(1, aggregated.totalClasses),
147
+ bloatedInterfaces: aggregated.bloatedInterfaces,
148
+ totalInterfaces: Math.max(1, aggregated.totalInterfaces),
149
+ externalStateMutations: aggregated.externalStateMutations,
150
+ hasTestFramework
151
+ });
152
+ const issues = [];
153
+ const minCoverage = options.minCoverageRatio ?? 0.3;
154
+ const actualRatio = sourceFiles.length > 0 ? testFiles.length / sourceFiles.length : 0;
155
+ if (!hasTestFramework) {
156
+ issues.push({
157
+ type: IssueType.LowTestability,
158
+ dimension: "framework",
159
+ severity: Severity.Critical,
160
+ message: "No major testing framework detected \u2014 AI changes cannot be safely verified.",
161
+ location: { file: options.rootDir, line: 0 },
162
+ suggestion: "Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification."
163
+ });
164
+ }
165
+ if (actualRatio < minCoverage) {
166
+ const needed = Math.ceil(sourceFiles.length * minCoverage) - testFiles.length;
167
+ issues.push({
168
+ type: IssueType.LowTestability,
169
+ dimension: "test-coverage",
170
+ severity: actualRatio === 0 ? Severity.Critical : Severity.Major,
171
+ message: `Test ratio is ${Math.round(actualRatio * 100)}% (${testFiles.length} test files for ${sourceFiles.length} source files). Need at least ${Math.round(minCoverage * 100)}%.`,
172
+ location: { file: options.rootDir, line: 0 },
173
+ suggestion: `Add ~${needed} test file(s) to reach the ${Math.round(minCoverage * 100)}% minimum for safe AI assistance.`
174
+ });
175
+ }
176
+ if (indexResult.dimensions.purityScore < 50) {
177
+ issues.push({
178
+ type: IssueType.LowTestability,
179
+ dimension: "purity",
180
+ severity: Severity.Major,
181
+ message: `Only ${indexResult.dimensions.purityScore}% of functions appear pure \u2014 side-effectful code is harder for AI to verify safely.`,
182
+ location: { file: options.rootDir, line: 0 },
183
+ suggestion: "Refactor complex side-effectful logic into pure functions where possible."
184
+ });
185
+ }
186
+ return {
187
+ summary: {
188
+ sourceFiles: sourceFiles.length,
189
+ testFiles: testFiles.length,
190
+ coverageRatio: Math.round(actualRatio * 100) / 100,
191
+ score: indexResult.score,
192
+ rating: indexResult.rating,
193
+ aiChangeSafetyRating: indexResult.aiChangeSafetyRating,
194
+ dimensions: indexResult.dimensions
195
+ },
196
+ issues,
197
+ rawData: {
198
+ sourceFiles: sourceFiles.length,
199
+ testFiles: testFiles.length,
200
+ ...aggregated,
201
+ hasTestFramework
202
+ },
203
+ recommendations: indexResult.recommendations
204
+ };
205
+ }
206
+
207
+ // src/scoring.ts
208
+ import { ToolName } from "@aiready/core";
209
+ function calculateTestabilityScore(report) {
210
+ const { summary, rawData, recommendations } = report;
211
+ const factors = [
212
+ {
213
+ name: "Test Coverage",
214
+ impact: Math.round(summary.dimensions.testCoverageRatio - 50),
215
+ description: `${rawData.testFiles} test files / ${rawData.sourceFiles} source files (${Math.round(summary.coverageRatio * 100)}%)`
216
+ },
217
+ {
218
+ name: "Function Purity",
219
+ impact: Math.round(summary.dimensions.purityScore - 50),
220
+ description: `${rawData.pureFunctions}/${rawData.totalFunctions} functions are pure`
221
+ },
222
+ {
223
+ name: "Dependency Injection",
224
+ impact: Math.round(summary.dimensions.dependencyInjectionScore - 50),
225
+ description: `${rawData.injectionPatterns}/${rawData.totalClasses} classes use DI`
226
+ },
227
+ {
228
+ name: "Interface Focus",
229
+ impact: Math.round(summary.dimensions.interfaceFocusScore - 50),
230
+ description: `${rawData.bloatedInterfaces} interfaces have >10 methods`
231
+ },
232
+ {
233
+ name: "Observability",
234
+ impact: Math.round(summary.dimensions.observabilityScore - 50),
235
+ description: `${rawData.externalStateMutations} functions mutate external state`
236
+ }
237
+ ];
238
+ const recs = recommendations.map(
239
+ (action) => ({
240
+ action,
241
+ estimatedImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
242
+ priority: summary.aiChangeSafetyRating === "blind-risk" || summary.aiChangeSafetyRating === "high-risk" ? "high" : "medium"
243
+ })
244
+ );
245
+ return {
246
+ toolName: ToolName.TestabilityIndex,
247
+ score: summary.score,
248
+ rawMetrics: {
249
+ ...rawData,
250
+ rating: summary.rating,
251
+ aiChangeSafetyRating: summary.aiChangeSafetyRating,
252
+ coverageRatio: summary.coverageRatio
253
+ },
254
+ factors,
255
+ recommendations: recs
256
+ };
257
+ }
258
+
259
+ export {
260
+ analyzeTestability,
261
+ calculateTestabilityScore
262
+ };
package/dist/cli.js CHANGED
@@ -63,14 +63,14 @@ async function analyzeFileTestability(filePath) {
63
63
  result.injectionPatterns++;
64
64
  }
65
65
  const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
66
- if (total > 5) {
66
+ if (total > 10) {
67
67
  result.bloatedInterfaces++;
68
68
  }
69
69
  }
70
70
  if (exp.type === "interface") {
71
71
  result.totalInterfaces++;
72
72
  const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
73
- if (total > 5) {
73
+ if (total > 10) {
74
74
  result.bloatedInterfaces++;
75
75
  }
76
76
  }
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  analyzeTestability,
4
4
  calculateTestabilityScore
5
- } from "./chunk-QOIBI5E7.mjs";
5
+ } from "./chunk-HIE74PK3.mjs";
6
6
 
7
7
  // src/cli.ts
8
8
  import { Command } from "commander";
package/dist/index.js CHANGED
@@ -67,14 +67,14 @@ async function analyzeFileTestability(filePath) {
67
67
  result.injectionPatterns++;
68
68
  }
69
69
  const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
70
- if (total > 5) {
70
+ if (total > 10) {
71
71
  result.bloatedInterfaces++;
72
72
  }
73
73
  }
74
74
  if (exp.type === "interface") {
75
75
  result.totalInterfaces++;
76
76
  const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
77
- if (total > 5) {
77
+ if (total > 10) {
78
78
  result.bloatedInterfaces++;
79
79
  }
80
80
  }
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  analyzeTestability,
3
3
  calculateTestabilityScore
4
- } from "./chunk-QOIBI5E7.mjs";
4
+ } from "./chunk-HIE74PK3.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.6.6",
3
+ "version": "0.6.7",
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.23.7"
43
+ "@aiready/core": "0.23.8"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^24.0.0",
@@ -90,6 +90,7 @@ describe('Testability Analyzer', () => {
90
90
  `
91
91
  export interface Massive {
92
92
  a(): void; b(): void; c(): void; d(): void; e(): void; f(): void; g(): void;
93
+ h(): void; i(): void; j(): void; k(): void; l(): void;
93
94
  }
94
95
  `
95
96
  );
package/src/analyzer.ts CHANGED
@@ -68,7 +68,7 @@ async function analyzeFileTestability(filePath: string): Promise<FileAnalysis> {
68
68
  }
69
69
  // Heuristic: bloated classes
70
70
  const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
71
- if (total > 5) {
71
+ if (total > 10) {
72
72
  result.bloatedInterfaces++;
73
73
  }
74
74
  }
@@ -77,7 +77,7 @@ async function analyzeFileTestability(filePath: string): Promise<FileAnalysis> {
77
77
  result.totalInterfaces++;
78
78
  // Heuristic: interfaces with many methods/props are considered bloated
79
79
  const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
80
- if (total > 5) {
80
+ if (total > 10) {
81
81
  result.bloatedInterfaces++;
82
82
  }
83
83
  }