@aiready/testability 0.4.16 → 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.
@@ -1,6 +1,6 @@
1
1
 
2
2
  
3
- > @aiready/testability@0.4.15 build /Users/pengcao/projects/aiready/packages/testability
3
+ > @aiready/testability@0.4.16 build /Users/pengcao/projects/aiready/packages/testability
4
4
  > tsup src/index.ts src/cli.ts --format cjs,esm --dts
5
5
 
6
6
  CLI Building entry: src/cli.ts, src/index.ts
@@ -9,16 +9,16 @@
9
9
  CLI Target: es2020
10
10
  CJS Build start
11
11
  ESM Build start
12
+ CJS dist/cli.js 15.61 KB
13
+ CJS dist/index.js 10.73 KB
14
+ CJS ⚡️ Build success in 42ms
12
15
  ESM dist/cli.mjs 5.75 KB
13
- ESM dist/chunk-LJLAJRZR.mjs 11.12 KB
14
16
  ESM dist/index.mjs 1.28 KB
15
- ESM ⚡️ Build success in 138ms
16
- CJS dist/index.js 13.60 KB
17
- CJS dist/cli.js 18.49 KB
18
- CJS ⚡️ Build success in 142ms
17
+ ESM dist/chunk-QOIBI5E7.mjs 8.31 KB
18
+ ESM ⚡️ Build success in 42ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 2714ms
20
+ DTS ⚡️ Build success in 1209ms
21
21
  DTS dist/cli.d.ts 20.00 B
22
- DTS dist/index.d.ts 2.78 KB
22
+ DTS dist/index.d.ts 2.41 KB
23
23
  DTS dist/cli.d.mts 20.00 B
24
- DTS dist/index.d.mts 2.78 KB
24
+ DTS dist/index.d.mts 2.41 KB
@@ -1,16 +1,16 @@
1
1
 
2
2
  
3
- > @aiready/testability@0.4.15 test /Users/pengcao/projects/aiready/packages/testability
3
+ > @aiready/testability@0.4.16 test /Users/pengcao/projects/aiready/packages/testability
4
4
  > vitest run
5
5
 
6
6
  [?25l
7
7
   RUN  v4.0.18 /Users/pengcao/projects/aiready/packages/testability
8
8
 
9
- ✓ src/__tests__/analyzer.test.ts (3 tests) 145ms
9
+ ✓ src/__tests__/analyzer.test.ts (3 tests) 87ms
10
10
 
11
11
   Test Files  1 passed (1)
12
12
   Tests  3 passed (3)
13
-  Start at  02:11:32
14
-  Duration  1.40s (transform 184ms, setup 0ms, import 926ms, tests 145ms, environment 0ms)
13
+  Start at  12:45:53
14
+  Duration  1.37s (transform 221ms, setup 0ms, import 663ms, tests 87ms, environment 0ms)
15
15
 
16
16
  [?25h
package/README.md CHANGED
@@ -9,6 +9,12 @@
9
9
 
10
10
  The "Verify" loop is the most expensive part of the AI agent workflow. Codebases with high global state, missing dependency injection, or poor test coverage force agents into long, expensive retry loops. The **Testability Index** quantifies these frictions.
11
11
 
12
+ ### Language Support
13
+
14
+ - **Full Support:** TypeScript, JavaScript, Python, Java, Go, C#
15
+ - **Capabilities:** Purity analysis, global state detection, DI pattern recognition.
16
+ toxicology
17
+
12
18
  ## 🏛️ Architecture
13
19
 
14
20
  ```
@@ -0,0 +1,258 @@
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
+ }
45
+ if (exp.type === "interface" || exp.type === "class") {
46
+ if (exp.type === "interface") result.totalInterfaces++;
47
+ const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
48
+ if (total > 5) {
49
+ result.bloatedInterfaces++;
50
+ }
51
+ }
52
+ }
53
+ } catch (error) {
54
+ console.warn(`Testability: Failed to parse ${filePath}: ${error}`);
55
+ }
56
+ return result;
57
+ }
58
+ function detectTestFramework(rootDir) {
59
+ const manifests = [
60
+ {
61
+ file: "package.json",
62
+ deps: ["jest", "vitest", "mocha", "mocha", "jasmine", "ava", "tap"]
63
+ },
64
+ { file: "requirements.txt", deps: ["pytest", "unittest", "nose"] },
65
+ { file: "pyproject.toml", deps: ["pytest"] },
66
+ { file: "pom.xml", deps: ["junit", "testng"] },
67
+ { file: "build.gradle", deps: ["junit", "testng"] },
68
+ { file: "go.mod", deps: ["testing"] }
69
+ // go testing is built-in
70
+ ];
71
+ for (const m of manifests) {
72
+ const p = join(rootDir, m.file);
73
+ if (existsSync(p)) {
74
+ if (m.file === "go.mod") return true;
75
+ try {
76
+ const content = readFileSync(p, "utf-8");
77
+ if (m.deps.some((d) => content.includes(d))) return true;
78
+ } catch {
79
+ }
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+ var TEST_PATTERNS = [
85
+ /\.(test|spec)\.(ts|tsx|js|jsx)$/,
86
+ /_test\.go$/,
87
+ /test_.*\.py$/,
88
+ /.*_test\.py$/,
89
+ /.*Test\.java$/,
90
+ /.*Tests\.cs$/,
91
+ /__tests__\//,
92
+ /\/tests?\//,
93
+ /\/e2e\//,
94
+ /\/fixtures\//
95
+ ];
96
+ function isTestFile(filePath, extra) {
97
+ if (TEST_PATTERNS.some((p) => p.test(filePath))) return true;
98
+ if (extra) return extra.some((p) => filePath.includes(p));
99
+ return false;
100
+ }
101
+ async function analyzeTestability(options) {
102
+ const allFiles = await scanFiles({
103
+ ...options,
104
+ include: options.include || ["**/*.{ts,tsx,js,jsx,py,java,cs,go}"],
105
+ includeTests: true
106
+ });
107
+ const sourceFiles = allFiles.filter(
108
+ (f) => !isTestFile(f, options.testPatterns)
109
+ );
110
+ const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
111
+ const aggregated = {
112
+ pureFunctions: 0,
113
+ totalFunctions: 0,
114
+ injectionPatterns: 0,
115
+ totalClasses: 0,
116
+ bloatedInterfaces: 0,
117
+ totalInterfaces: 0,
118
+ externalStateMutations: 0
119
+ };
120
+ let processed = 0;
121
+ for (const f of sourceFiles) {
122
+ processed++;
123
+ emitProgress(
124
+ processed,
125
+ sourceFiles.length,
126
+ "testability",
127
+ "analyzing files",
128
+ options.onProgress
129
+ );
130
+ const a = await analyzeFileTestability(f);
131
+ for (const key of Object.keys(aggregated)) {
132
+ aggregated[key] += a[key];
133
+ }
134
+ }
135
+ const hasTestFramework = detectTestFramework(options.rootDir);
136
+ const indexResult = calculateTestabilityIndex({
137
+ testFiles: testFiles.length,
138
+ sourceFiles: sourceFiles.length,
139
+ pureFunctions: aggregated.pureFunctions,
140
+ totalFunctions: Math.max(1, aggregated.totalFunctions),
141
+ injectionPatterns: aggregated.injectionPatterns,
142
+ totalClasses: Math.max(1, aggregated.totalClasses),
143
+ bloatedInterfaces: aggregated.bloatedInterfaces,
144
+ totalInterfaces: Math.max(1, aggregated.totalInterfaces),
145
+ externalStateMutations: aggregated.externalStateMutations,
146
+ hasTestFramework
147
+ });
148
+ const issues = [];
149
+ const minCoverage = options.minCoverageRatio ?? 0.3;
150
+ const actualRatio = sourceFiles.length > 0 ? testFiles.length / sourceFiles.length : 0;
151
+ if (!hasTestFramework) {
152
+ issues.push({
153
+ type: IssueType.LowTestability,
154
+ dimension: "framework",
155
+ severity: Severity.Critical,
156
+ message: "No major testing framework detected \u2014 AI changes cannot be safely verified.",
157
+ location: { file: options.rootDir, line: 0 },
158
+ suggestion: "Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification."
159
+ });
160
+ }
161
+ if (actualRatio < minCoverage) {
162
+ const needed = Math.ceil(sourceFiles.length * minCoverage) - testFiles.length;
163
+ issues.push({
164
+ type: IssueType.LowTestability,
165
+ dimension: "test-coverage",
166
+ severity: actualRatio === 0 ? Severity.Critical : Severity.Major,
167
+ message: `Test ratio is ${Math.round(actualRatio * 100)}% (${testFiles.length} test files for ${sourceFiles.length} source files). Need at least ${Math.round(minCoverage * 100)}%.`,
168
+ location: { file: options.rootDir, line: 0 },
169
+ suggestion: `Add ~${needed} test file(s) to reach the ${Math.round(minCoverage * 100)}% minimum for safe AI assistance.`
170
+ });
171
+ }
172
+ if (indexResult.dimensions.purityScore < 50) {
173
+ issues.push({
174
+ type: IssueType.LowTestability,
175
+ dimension: "purity",
176
+ severity: Severity.Major,
177
+ message: `Only ${indexResult.dimensions.purityScore}% of functions appear pure \u2014 side-effectful code is harder for AI to verify safely.`,
178
+ location: { file: options.rootDir, line: 0 },
179
+ suggestion: "Refactor complex side-effectful logic into pure functions where possible."
180
+ });
181
+ }
182
+ return {
183
+ summary: {
184
+ sourceFiles: sourceFiles.length,
185
+ testFiles: testFiles.length,
186
+ coverageRatio: Math.round(actualRatio * 100) / 100,
187
+ score: indexResult.score,
188
+ rating: indexResult.rating,
189
+ aiChangeSafetyRating: indexResult.aiChangeSafetyRating,
190
+ dimensions: indexResult.dimensions
191
+ },
192
+ issues,
193
+ rawData: {
194
+ sourceFiles: sourceFiles.length,
195
+ testFiles: testFiles.length,
196
+ ...aggregated,
197
+ hasTestFramework
198
+ },
199
+ recommendations: indexResult.recommendations
200
+ };
201
+ }
202
+
203
+ // src/scoring.ts
204
+ import { ToolName } from "@aiready/core";
205
+ function calculateTestabilityScore(report) {
206
+ const { summary, rawData, recommendations } = report;
207
+ const factors = [
208
+ {
209
+ name: "Test Coverage",
210
+ impact: Math.round(summary.dimensions.testCoverageRatio - 50),
211
+ description: `${rawData.testFiles} test files / ${rawData.sourceFiles} source files (${Math.round(summary.coverageRatio * 100)}%)`
212
+ },
213
+ {
214
+ name: "Function Purity",
215
+ impact: Math.round(summary.dimensions.purityScore - 50),
216
+ description: `${rawData.pureFunctions}/${rawData.totalFunctions} functions are pure`
217
+ },
218
+ {
219
+ name: "Dependency Injection",
220
+ impact: Math.round(summary.dimensions.dependencyInjectionScore - 50),
221
+ description: `${rawData.injectionPatterns}/${rawData.totalClasses} classes use DI`
222
+ },
223
+ {
224
+ name: "Interface Focus",
225
+ impact: Math.round(summary.dimensions.interfaceFocusScore - 50),
226
+ description: `${rawData.bloatedInterfaces} interfaces have >10 methods`
227
+ },
228
+ {
229
+ name: "Observability",
230
+ impact: Math.round(summary.dimensions.observabilityScore - 50),
231
+ description: `${rawData.externalStateMutations} functions mutate external state`
232
+ }
233
+ ];
234
+ const recs = recommendations.map(
235
+ (action) => ({
236
+ action,
237
+ estimatedImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
238
+ priority: summary.aiChangeSafetyRating === "blind-risk" || summary.aiChangeSafetyRating === "high-risk" ? "high" : "medium"
239
+ })
240
+ );
241
+ return {
242
+ toolName: ToolName.TestabilityIndex,
243
+ score: summary.score,
244
+ rawMetrics: {
245
+ ...rawData,
246
+ rating: summary.rating,
247
+ aiChangeSafetyRating: summary.aiChangeSafetyRating,
248
+ coverageRatio: summary.coverageRatio
249
+ },
250
+ factors,
251
+ recommendations: recs
252
+ };
253
+ }
254
+
255
+ export {
256
+ analyzeTestability,
257
+ calculateTestabilityScore
258
+ };
@@ -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 > 5) {
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 > 5) {
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
+ };