@aiready/testability 0.6.7 → 0.6.8

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,23 +1,24 @@
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
+
2
+ 
3
+ > @aiready/testability@0.6.8 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/index.js 10.78 KB
13
+ CJS dist/cli.js 15.78 KB
14
+ CJS ⚡️ Build success in 62ms
15
+ ESM dist/index.mjs 1.17 KB
16
+ ESM dist/cli.mjs 5.75 KB
17
+ ESM dist/chunk-JL4S6RHJ.mjs 8.47 KB
18
+ ESM ⚡️ Build success in 63ms
19
+ DTS Build start
20
+ DTS ⚡️ Build success in 4353ms
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,18 +1,20 @@
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
-
1
+
2
+ 
3
+ > @aiready/testability@0.6.7 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) 4ms
11
+ ✓ src/__tests__/provider.test.ts (2 tests) 3ms
12
+ ✓ src/__tests__/analyzer.test.ts (5 tests) 943ms
13
+ ✓ detects test frameworks in multiple languages  916ms
14
+
15
+  Test Files  4 passed (4)
16
+  Tests  14 passed (14)
17
+  Start at  23:50:20
18
+  Duration  1.67s (transform 715ms, setup 0ms, import 1.94s, tests 951ms, environment 0ms)
19
+
20
+ [?25h
@@ -0,0 +1,269 @@
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
+ const fileDetails = [];
125
+ let processed = 0;
126
+ for (const f of sourceFiles) {
127
+ processed++;
128
+ emitProgress(
129
+ processed,
130
+ sourceFiles.length,
131
+ "testability",
132
+ "analyzing files",
133
+ options.onProgress
134
+ );
135
+ const a = await analyzeFileTestability(f);
136
+ for (const key of Object.keys(aggregated)) {
137
+ aggregated[key] += a[key];
138
+ }
139
+ fileDetails.push({
140
+ filePath: f,
141
+ pureFunctions: a.pureFunctions,
142
+ totalFunctions: a.totalFunctions
143
+ });
144
+ }
145
+ const hasTestFramework = detectTestFramework(options.rootDir);
146
+ const indexResult = calculateTestabilityIndex({
147
+ testFiles: testFiles.length,
148
+ sourceFiles: sourceFiles.length,
149
+ pureFunctions: aggregated.pureFunctions,
150
+ totalFunctions: Math.max(1, aggregated.totalFunctions),
151
+ injectionPatterns: aggregated.injectionPatterns,
152
+ totalClasses: Math.max(1, aggregated.totalClasses),
153
+ bloatedInterfaces: aggregated.bloatedInterfaces,
154
+ totalInterfaces: Math.max(1, aggregated.totalInterfaces),
155
+ externalStateMutations: aggregated.externalStateMutations,
156
+ hasTestFramework,
157
+ fileDetails
158
+ });
159
+ const issues = [];
160
+ const minCoverage = options.minCoverageRatio ?? 0.3;
161
+ const actualRatio = sourceFiles.length > 0 ? testFiles.length / sourceFiles.length : 0;
162
+ if (!hasTestFramework) {
163
+ issues.push({
164
+ type: IssueType.LowTestability,
165
+ dimension: "framework",
166
+ severity: Severity.Critical,
167
+ message: "No major testing framework detected \u2014 AI changes cannot be safely verified.",
168
+ location: { file: options.rootDir, line: 0 },
169
+ suggestion: "Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification."
170
+ });
171
+ }
172
+ if (actualRatio < minCoverage) {
173
+ const needed = Math.ceil(sourceFiles.length * minCoverage) - testFiles.length;
174
+ issues.push({
175
+ type: IssueType.LowTestability,
176
+ dimension: "test-coverage",
177
+ severity: actualRatio === 0 ? Severity.Critical : Severity.Major,
178
+ message: `Test ratio is ${Math.round(actualRatio * 100)}% (${testFiles.length} test files for ${sourceFiles.length} source files). Need at least ${Math.round(minCoverage * 100)}%.`,
179
+ location: { file: options.rootDir, line: 0 },
180
+ suggestion: `Add ~${needed} test file(s) to reach the ${Math.round(minCoverage * 100)}% minimum for safe AI assistance.`
181
+ });
182
+ }
183
+ if (indexResult.dimensions.purityScore < 50) {
184
+ issues.push({
185
+ type: IssueType.LowTestability,
186
+ dimension: "purity",
187
+ severity: Severity.Major,
188
+ message: `Only ${indexResult.dimensions.purityScore}% of functions appear pure \u2014 side-effectful code is harder for AI to verify safely.`,
189
+ location: { file: options.rootDir, line: 0 },
190
+ suggestion: "Refactor complex side-effectful logic into pure functions where possible."
191
+ });
192
+ }
193
+ return {
194
+ summary: {
195
+ sourceFiles: sourceFiles.length,
196
+ testFiles: testFiles.length,
197
+ coverageRatio: Math.round(actualRatio * 100) / 100,
198
+ score: indexResult.score,
199
+ rating: indexResult.rating,
200
+ aiChangeSafetyRating: indexResult.aiChangeSafetyRating,
201
+ dimensions: indexResult.dimensions
202
+ },
203
+ issues,
204
+ rawData: {
205
+ sourceFiles: sourceFiles.length,
206
+ testFiles: testFiles.length,
207
+ ...aggregated,
208
+ hasTestFramework
209
+ },
210
+ recommendations: indexResult.recommendations
211
+ };
212
+ }
213
+
214
+ // src/scoring.ts
215
+ import { ToolName } from "@aiready/core";
216
+ function calculateTestabilityScore(report) {
217
+ const { summary, rawData, recommendations } = report;
218
+ const factors = [
219
+ {
220
+ name: "Test Coverage",
221
+ impact: Math.round(summary.dimensions.testCoverageRatio - 50),
222
+ description: `${rawData.testFiles} test files / ${rawData.sourceFiles} source files (${Math.round(summary.coverageRatio * 100)}%)`
223
+ },
224
+ {
225
+ name: "Function Purity",
226
+ impact: Math.round(summary.dimensions.purityScore - 50),
227
+ description: `${rawData.pureFunctions}/${rawData.totalFunctions} functions are pure`
228
+ },
229
+ {
230
+ name: "Dependency Injection",
231
+ impact: Math.round(summary.dimensions.dependencyInjectionScore - 50),
232
+ description: `${rawData.injectionPatterns}/${rawData.totalClasses} classes use DI`
233
+ },
234
+ {
235
+ name: "Interface Focus",
236
+ impact: Math.round(summary.dimensions.interfaceFocusScore - 50),
237
+ description: `${rawData.bloatedInterfaces} interfaces have >10 methods`
238
+ },
239
+ {
240
+ name: "Observability",
241
+ impact: Math.round(summary.dimensions.observabilityScore - 50),
242
+ description: `${rawData.externalStateMutations} functions mutate external state`
243
+ }
244
+ ];
245
+ const recs = recommendations.map(
246
+ (action) => ({
247
+ action,
248
+ estimatedImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
249
+ priority: summary.aiChangeSafetyRating === "blind-risk" || summary.aiChangeSafetyRating === "high-risk" ? "high" : "medium"
250
+ })
251
+ );
252
+ return {
253
+ toolName: ToolName.TestabilityIndex,
254
+ score: summary.score,
255
+ rawMetrics: {
256
+ ...rawData,
257
+ rating: summary.rating,
258
+ aiChangeSafetyRating: summary.aiChangeSafetyRating,
259
+ coverageRatio: summary.coverageRatio
260
+ },
261
+ factors,
262
+ recommendations: recs
263
+ };
264
+ }
265
+
266
+ export {
267
+ analyzeTestability,
268
+ calculateTestabilityScore
269
+ };
package/dist/cli.js CHANGED
@@ -142,6 +142,7 @@ async function analyzeTestability(options) {
142
142
  totalInterfaces: 0,
143
143
  externalStateMutations: 0
144
144
  };
145
+ const fileDetails = [];
145
146
  let processed = 0;
146
147
  for (const f of sourceFiles) {
147
148
  processed++;
@@ -156,6 +157,11 @@ async function analyzeTestability(options) {
156
157
  for (const key of Object.keys(aggregated)) {
157
158
  aggregated[key] += a[key];
158
159
  }
160
+ fileDetails.push({
161
+ filePath: f,
162
+ pureFunctions: a.pureFunctions,
163
+ totalFunctions: a.totalFunctions
164
+ });
159
165
  }
160
166
  const hasTestFramework = detectTestFramework(options.rootDir);
161
167
  const indexResult = (0, import_core.calculateTestabilityIndex)({
@@ -168,7 +174,8 @@ async function analyzeTestability(options) {
168
174
  bloatedInterfaces: aggregated.bloatedInterfaces,
169
175
  totalInterfaces: Math.max(1, aggregated.totalInterfaces),
170
176
  externalStateMutations: aggregated.externalStateMutations,
171
- hasTestFramework
177
+ hasTestFramework,
178
+ fileDetails
172
179
  });
173
180
  const issues = [];
174
181
  const minCoverage = options.minCoverageRatio ?? 0.3;
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  analyzeTestability,
4
4
  calculateTestabilityScore
5
- } from "./chunk-HIE74PK3.mjs";
5
+ } from "./chunk-JL4S6RHJ.mjs";
6
6
 
7
7
  // src/cli.ts
8
8
  import { Command } from "commander";
package/dist/index.js CHANGED
@@ -146,6 +146,7 @@ async function analyzeTestability(options) {
146
146
  totalInterfaces: 0,
147
147
  externalStateMutations: 0
148
148
  };
149
+ const fileDetails = [];
149
150
  let processed = 0;
150
151
  for (const f of sourceFiles) {
151
152
  processed++;
@@ -160,6 +161,11 @@ async function analyzeTestability(options) {
160
161
  for (const key of Object.keys(aggregated)) {
161
162
  aggregated[key] += a[key];
162
163
  }
164
+ fileDetails.push({
165
+ filePath: f,
166
+ pureFunctions: a.pureFunctions,
167
+ totalFunctions: a.totalFunctions
168
+ });
163
169
  }
164
170
  const hasTestFramework = detectTestFramework(options.rootDir);
165
171
  const indexResult = (0, import_core.calculateTestabilityIndex)({
@@ -172,7 +178,8 @@ async function analyzeTestability(options) {
172
178
  bloatedInterfaces: aggregated.bloatedInterfaces,
173
179
  totalInterfaces: Math.max(1, aggregated.totalInterfaces),
174
180
  externalStateMutations: aggregated.externalStateMutations,
175
- hasTestFramework
181
+ hasTestFramework,
182
+ fileDetails
176
183
  });
177
184
  const issues = [];
178
185
  const minCoverage = options.minCoverageRatio ?? 0.3;
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  analyzeTestability,
3
3
  calculateTestabilityScore
4
- } from "./chunk-HIE74PK3.mjs";
4
+ } from "./chunk-JL4S6RHJ.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.7",
3
+ "version": "0.6.8",
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.8"
43
+ "@aiready/core": "0.23.9"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^24.0.0",
package/src/analyzer.ts CHANGED
@@ -170,6 +170,13 @@ export async function analyzeTestability(
170
170
  externalStateMutations: 0,
171
171
  };
172
172
 
173
+ // Collect file-level details for smarter scoring
174
+ const fileDetails: Array<{
175
+ filePath: string;
176
+ pureFunctions: number;
177
+ totalFunctions: number;
178
+ }> = [];
179
+
173
180
  let processed = 0;
174
181
  for (const f of sourceFiles) {
175
182
  processed++;
@@ -185,6 +192,13 @@ export async function analyzeTestability(
185
192
  for (const key of Object.keys(aggregated) as Array<keyof FileAnalysis>) {
186
193
  aggregated[key] += a[key];
187
194
  }
195
+
196
+ // Collect file-level data
197
+ fileDetails.push({
198
+ filePath: f,
199
+ pureFunctions: a.pureFunctions,
200
+ totalFunctions: a.totalFunctions,
201
+ });
188
202
  }
189
203
 
190
204
  const hasTestFramework = detectTestFramework(options.rootDir);
@@ -200,6 +214,7 @@ export async function analyzeTestability(
200
214
  totalInterfaces: Math.max(1, aggregated.totalInterfaces),
201
215
  externalStateMutations: aggregated.externalStateMutations,
202
216
  hasTestFramework,
217
+ fileDetails,
203
218
  });
204
219
 
205
220
  // Build issues