@aiready/testability 0.6.22 → 0.7.0

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.
@@ -0,0 +1,237 @@
1
+ // src/analyzer.ts
2
+ import {
3
+ scanFiles,
4
+ calculateTestabilityIndex,
5
+ Severity,
6
+ IssueType,
7
+ runBatchAnalysis,
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 = await 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
+ await runBatchAnalysis(
126
+ sourceFiles,
127
+ "analyzing files",
128
+ "testability",
129
+ options.onProgress,
130
+ async (f) => ({ filePath: f, analysis: await analyzeFileTestability(f) }),
131
+ (result) => {
132
+ const a = result.analysis;
133
+ for (const key of Object.keys(aggregated)) {
134
+ aggregated[key] += a[key];
135
+ }
136
+ fileDetails.push({
137
+ filePath: result.filePath,
138
+ pureFunctions: a.pureFunctions,
139
+ totalFunctions: a.totalFunctions
140
+ });
141
+ }
142
+ );
143
+ const hasTestFramework = detectTestFramework(options.rootDir);
144
+ const indexResult = calculateTestabilityIndex({
145
+ testFiles: testFiles.length,
146
+ sourceFiles: sourceFiles.length,
147
+ pureFunctions: aggregated.pureFunctions,
148
+ totalFunctions: Math.max(1, aggregated.totalFunctions),
149
+ injectionPatterns: aggregated.injectionPatterns,
150
+ totalClasses: Math.max(1, aggregated.totalClasses),
151
+ bloatedInterfaces: aggregated.bloatedInterfaces,
152
+ totalInterfaces: Math.max(1, aggregated.totalInterfaces),
153
+ externalStateMutations: aggregated.externalStateMutations,
154
+ hasTestFramework,
155
+ fileDetails
156
+ });
157
+ const issues = [];
158
+ const minCoverage = options.minCoverageRatio ?? 0.3;
159
+ const actualRatio = sourceFiles.length > 0 ? testFiles.length / sourceFiles.length : 0;
160
+ if (!hasTestFramework) {
161
+ issues.push({
162
+ type: IssueType.LowTestability,
163
+ dimension: "framework",
164
+ severity: Severity.Critical,
165
+ message: "No major testing framework detected \u2014 AI changes cannot be safely verified.",
166
+ location: { file: options.rootDir, line: 0 },
167
+ suggestion: "Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification."
168
+ });
169
+ }
170
+ if (actualRatio < minCoverage) {
171
+ const needed = Math.ceil(sourceFiles.length * minCoverage) - testFiles.length;
172
+ issues.push({
173
+ type: IssueType.LowTestability,
174
+ dimension: "test-coverage",
175
+ severity: actualRatio === 0 ? Severity.Critical : Severity.Major,
176
+ message: `Test ratio is ${Math.round(actualRatio * 100)}% (${testFiles.length} test files for ${sourceFiles.length} source files). Need at least ${Math.round(minCoverage * 100)}%.`,
177
+ location: { file: options.rootDir, line: 0 },
178
+ suggestion: `Add ~${needed} test file(s) to reach the ${Math.round(minCoverage * 100)}% minimum for safe AI assistance.`
179
+ });
180
+ }
181
+ if (indexResult.dimensions.purityScore < 50) {
182
+ issues.push({
183
+ type: IssueType.LowTestability,
184
+ dimension: "purity",
185
+ severity: Severity.Major,
186
+ message: `Only ${indexResult.dimensions.purityScore}% of functions appear pure \u2014 side-effectful code is harder for AI to verify safely.`,
187
+ location: { file: options.rootDir, line: 0 },
188
+ suggestion: "Refactor complex side-effectful logic into pure functions where possible."
189
+ });
190
+ }
191
+ return {
192
+ summary: {
193
+ sourceFiles: sourceFiles.length,
194
+ testFiles: testFiles.length,
195
+ coverageRatio: Math.round(actualRatio * 100) / 100,
196
+ score: indexResult.score,
197
+ rating: indexResult.rating,
198
+ aiChangeSafetyRating: indexResult.aiChangeSafetyRating,
199
+ dimensions: indexResult.dimensions
200
+ },
201
+ issues,
202
+ rawData: {
203
+ sourceFiles: sourceFiles.length,
204
+ testFiles: testFiles.length,
205
+ ...aggregated,
206
+ hasTestFramework
207
+ },
208
+ recommendations: indexResult.recommendations
209
+ };
210
+ }
211
+
212
+ // src/scoring.ts
213
+ import { ToolName, buildStandardToolScore } from "@aiready/core";
214
+ function calculateTestabilityScore(report) {
215
+ const { summary, rawData, recommendations } = report;
216
+ return buildStandardToolScore({
217
+ toolName: ToolName.TestabilityIndex,
218
+ score: summary.score,
219
+ rawData,
220
+ dimensions: summary.dimensions,
221
+ dimensionNames: {
222
+ testCoverageRatio: "Test Coverage",
223
+ purityScore: "Function Purity",
224
+ dependencyInjectionScore: "Dependency Injection",
225
+ interfaceFocusScore: "Interface Focus",
226
+ observabilityScore: "Observability"
227
+ },
228
+ recommendations,
229
+ recommendationImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
230
+ rating: summary.aiChangeSafetyRating || summary.rating
231
+ });
232
+ }
233
+
234
+ export {
235
+ analyzeTestability,
236
+ calculateTestabilityScore
237
+ };
package/dist/cli.js CHANGED
@@ -29,7 +29,6 @@ var import_commander = require("commander");
29
29
  // src/analyzer.ts
30
30
  var import_core = require("@aiready/core");
31
31
  var import_fs = require("fs");
32
- var import_path = require("path");
33
32
  async function analyzeFileTestability(filePath) {
34
33
  const result = {
35
34
  pureFunctions: 0,
@@ -80,49 +79,6 @@ async function analyzeFileTestability(filePath) {
80
79
  }
81
80
  return result;
82
81
  }
83
- function detectTestFramework(rootDir) {
84
- const manifests = [
85
- {
86
- file: "package.json",
87
- deps: ["jest", "vitest", "mocha", "mocha", "jasmine", "ava", "tap"]
88
- },
89
- { file: "requirements.txt", deps: ["pytest", "unittest", "nose"] },
90
- { file: "pyproject.toml", deps: ["pytest"] },
91
- { file: "pom.xml", deps: ["junit", "testng"] },
92
- { file: "build.gradle", deps: ["junit", "testng"] },
93
- { file: "go.mod", deps: ["testing"] }
94
- // go testing is built-in
95
- ];
96
- for (const m of manifests) {
97
- const p = (0, import_path.join)(rootDir, m.file);
98
- if ((0, import_fs.existsSync)(p)) {
99
- if (m.file === "go.mod") return true;
100
- try {
101
- const content = (0, import_fs.readFileSync)(p, "utf-8");
102
- if (m.deps.some((d) => content.includes(d))) return true;
103
- } catch {
104
- }
105
- }
106
- }
107
- return false;
108
- }
109
- var TEST_PATTERNS = [
110
- /\.(test|spec)\.(ts|tsx|js|jsx)$/,
111
- /_test\.go$/,
112
- /test_.*\.py$/,
113
- /.*_test\.py$/,
114
- /.*Test\.java$/,
115
- /.*Tests\.cs$/,
116
- /__tests__\//,
117
- /\/tests?\//,
118
- /\/e2e\//,
119
- /\/fixtures\//
120
- ];
121
- function isTestFile(filePath, extra) {
122
- if (TEST_PATTERNS.some((p) => p.test(filePath))) return true;
123
- if (extra) return extra.some((p) => filePath.includes(p));
124
- return false;
125
- }
126
82
  async function analyzeTestability(options) {
127
83
  const allFiles = await (0, import_core.scanFiles)({
128
84
  ...options,
@@ -130,9 +86,9 @@ async function analyzeTestability(options) {
130
86
  includeTests: true
131
87
  });
132
88
  const sourceFiles = allFiles.filter(
133
- (f) => !isTestFile(f, options.testPatterns)
89
+ (f) => !(0, import_core.isTestFile)(f, options.testPatterns)
134
90
  );
135
- const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
91
+ const testFiles = allFiles.filter((f) => (0, import_core.isTestFile)(f, options.testPatterns));
136
92
  const aggregated = {
137
93
  pureFunctions: 0,
138
94
  totalFunctions: 0,
@@ -143,27 +99,28 @@ async function analyzeTestability(options) {
143
99
  externalStateMutations: 0
144
100
  };
145
101
  const fileDetails = [];
146
- let processed = 0;
147
- for (const f of sourceFiles) {
148
- processed++;
149
- (0, import_core.emitProgress)(
150
- processed,
151
- sourceFiles.length,
152
- "testability",
153
- "analyzing files",
154
- options.onProgress
155
- );
156
- const a = await analyzeFileTestability(f);
157
- for (const key of Object.keys(aggregated)) {
158
- aggregated[key] += a[key];
159
- }
160
- fileDetails.push({
102
+ await (0, import_core.runBatchAnalysis)(
103
+ sourceFiles,
104
+ "analyzing files",
105
+ "testability",
106
+ options.onProgress,
107
+ async (f) => ({
161
108
  filePath: f,
162
- pureFunctions: a.pureFunctions,
163
- totalFunctions: a.totalFunctions
164
- });
165
- }
166
- const hasTestFramework = detectTestFramework(options.rootDir);
109
+ analysis: await analyzeFileTestability(f)
110
+ }),
111
+ (result) => {
112
+ const a = result.analysis;
113
+ for (const key of Object.keys(aggregated)) {
114
+ aggregated[key] += a[key];
115
+ }
116
+ fileDetails.push({
117
+ filePath: result.filePath,
118
+ pureFunctions: a.pureFunctions,
119
+ totalFunctions: a.totalFunctions
120
+ });
121
+ }
122
+ );
123
+ const hasTestFramework = (0, import_core.detectTestFramework)(options.rootDir);
167
124
  const indexResult = (0, import_core.calculateTestabilityIndex)({
168
125
  testFiles: testFiles.length,
169
126
  sourceFiles: sourceFiles.length,
@@ -257,7 +214,7 @@ function calculateTestabilityScore(report) {
257
214
  // src/cli.ts
258
215
  var import_chalk = __toESM(require("chalk"));
259
216
  var import_fs2 = require("fs");
260
- var import_path2 = require("path");
217
+ var import_path = require("path");
261
218
  var import_core3 = require("@aiready/core");
262
219
  var program = new import_commander.Command();
263
220
  var startTime = Date.now();
@@ -311,35 +268,35 @@ EXAMPLES:
311
268
  `testability-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
312
269
  directory
313
270
  );
314
- const dir = (0, import_path2.dirname)(outputPath);
271
+ const dir = (0, import_path.dirname)(outputPath);
315
272
  if (!(0, import_fs2.existsSync)(dir)) (0, import_fs2.mkdirSync)(dir, { recursive: true });
316
273
  (0, import_fs2.writeFileSync)(outputPath, JSON.stringify(payload, null, 2));
317
274
  console.log(import_chalk.default.green(`\u2713 Report saved to ${outputPath}`));
318
275
  } else {
319
276
  (0, import_core3.displayStandardConsoleReport)({
320
277
  title: "\u{1F9EA} Testability Analysis",
321
- score: scoring.summary.score,
322
- rating: scoring.summary.rating,
278
+ score: scoring.score,
279
+ rating: scoring.rating || report.summary.rating,
323
280
  dimensions: [
324
281
  {
325
282
  name: "Test Coverage",
326
- value: scoring.summary.dimensions.testCoverageRatio
283
+ value: report.summary.dimensions.testCoverageRatio
327
284
  },
328
285
  {
329
286
  name: "Function Purity",
330
- value: scoring.summary.dimensions.purityScore
287
+ value: report.summary.dimensions.purityScore
331
288
  },
332
289
  {
333
290
  name: "Dependency Injection",
334
- value: scoring.summary.dimensions.dependencyInjectionScore
291
+ value: report.summary.dimensions.dependencyInjectionScore
335
292
  },
336
293
  {
337
294
  name: "Interface Focus",
338
- value: scoring.summary.dimensions.interfaceFocusScore
295
+ value: report.summary.dimensions.interfaceFocusScore
339
296
  },
340
297
  {
341
298
  name: "Observability",
342
- value: scoring.summary.dimensions.observabilityScore
299
+ value: report.summary.dimensions.observabilityScore
343
300
  }
344
301
  ],
345
302
  stats: [
@@ -347,7 +304,7 @@ EXAMPLES:
347
304
  { label: "Test Files", value: report.rawData.testFiles },
348
305
  {
349
306
  label: "Coverage Ratio",
350
- value: Math.round(scoring.summary.coverageRatio * 100) + "%"
307
+ value: Math.round(report.summary.coverageRatio * 100) + "%"
351
308
  }
352
309
  ],
353
310
  issues: report.issues,
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  analyzeTestability,
4
4
  calculateTestabilityScore
5
- } from "./chunk-QMDUZA7H.mjs";
5
+ } from "./chunk-CISO2RDG.mjs";
6
6
 
7
7
  // src/cli.ts
8
8
  import { Command } from "commander";
@@ -73,28 +73,28 @@ EXAMPLES:
73
73
  } else {
74
74
  displayStandardConsoleReport({
75
75
  title: "\u{1F9EA} Testability Analysis",
76
- score: scoring.summary.score,
77
- rating: scoring.summary.rating,
76
+ score: scoring.score,
77
+ rating: scoring.rating || report.summary.rating,
78
78
  dimensions: [
79
79
  {
80
80
  name: "Test Coverage",
81
- value: scoring.summary.dimensions.testCoverageRatio
81
+ value: report.summary.dimensions.testCoverageRatio
82
82
  },
83
83
  {
84
84
  name: "Function Purity",
85
- value: scoring.summary.dimensions.purityScore
85
+ value: report.summary.dimensions.purityScore
86
86
  },
87
87
  {
88
88
  name: "Dependency Injection",
89
- value: scoring.summary.dimensions.dependencyInjectionScore
89
+ value: report.summary.dimensions.dependencyInjectionScore
90
90
  },
91
91
  {
92
92
  name: "Interface Focus",
93
- value: scoring.summary.dimensions.interfaceFocusScore
93
+ value: report.summary.dimensions.interfaceFocusScore
94
94
  },
95
95
  {
96
96
  name: "Observability",
97
- value: scoring.summary.dimensions.observabilityScore
97
+ value: report.summary.dimensions.observabilityScore
98
98
  }
99
99
  ],
100
100
  stats: [
@@ -102,7 +102,7 @@ EXAMPLES:
102
102
  { label: "Test Files", value: report.rawData.testFiles },
103
103
  {
104
104
  label: "Coverage Ratio",
105
- value: Math.round(scoring.summary.coverageRatio * 100) + "%"
105
+ value: Math.round(report.summary.coverageRatio * 100) + "%"
106
106
  }
107
107
  ],
108
108
  issues: report.issues,
package/dist/index.js CHANGED
@@ -33,7 +33,6 @@ var import_core3 = require("@aiready/core");
33
33
  // src/analyzer.ts
34
34
  var import_core = require("@aiready/core");
35
35
  var import_fs = require("fs");
36
- var import_path = require("path");
37
36
  async function analyzeFileTestability(filePath) {
38
37
  const result = {
39
38
  pureFunctions: 0,
@@ -84,49 +83,6 @@ async function analyzeFileTestability(filePath) {
84
83
  }
85
84
  return result;
86
85
  }
87
- function detectTestFramework(rootDir) {
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
- }
110
- }
111
- return false;
112
- }
113
- var TEST_PATTERNS = [
114
- /\.(test|spec)\.(ts|tsx|js|jsx)$/,
115
- /_test\.go$/,
116
- /test_.*\.py$/,
117
- /.*_test\.py$/,
118
- /.*Test\.java$/,
119
- /.*Tests\.cs$/,
120
- /__tests__\//,
121
- /\/tests?\//,
122
- /\/e2e\//,
123
- /\/fixtures\//
124
- ];
125
- function isTestFile(filePath, extra) {
126
- if (TEST_PATTERNS.some((p) => p.test(filePath))) return true;
127
- if (extra) return extra.some((p) => filePath.includes(p));
128
- return false;
129
- }
130
86
  async function analyzeTestability(options) {
131
87
  const allFiles = await (0, import_core.scanFiles)({
132
88
  ...options,
@@ -134,9 +90,9 @@ async function analyzeTestability(options) {
134
90
  includeTests: true
135
91
  });
136
92
  const sourceFiles = allFiles.filter(
137
- (f) => !isTestFile(f, options.testPatterns)
93
+ (f) => !(0, import_core.isTestFile)(f, options.testPatterns)
138
94
  );
139
- const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
95
+ const testFiles = allFiles.filter((f) => (0, import_core.isTestFile)(f, options.testPatterns));
140
96
  const aggregated = {
141
97
  pureFunctions: 0,
142
98
  totalFunctions: 0,
@@ -147,27 +103,28 @@ async function analyzeTestability(options) {
147
103
  externalStateMutations: 0
148
104
  };
149
105
  const fileDetails = [];
150
- let processed = 0;
151
- for (const f of sourceFiles) {
152
- processed++;
153
- (0, import_core.emitProgress)(
154
- processed,
155
- sourceFiles.length,
156
- "testability",
157
- "analyzing files",
158
- options.onProgress
159
- );
160
- const a = await analyzeFileTestability(f);
161
- for (const key of Object.keys(aggregated)) {
162
- aggregated[key] += a[key];
163
- }
164
- fileDetails.push({
106
+ await (0, import_core.runBatchAnalysis)(
107
+ sourceFiles,
108
+ "analyzing files",
109
+ "testability",
110
+ options.onProgress,
111
+ async (f) => ({
165
112
  filePath: f,
166
- pureFunctions: a.pureFunctions,
167
- totalFunctions: a.totalFunctions
168
- });
169
- }
170
- const hasTestFramework = detectTestFramework(options.rootDir);
113
+ analysis: await analyzeFileTestability(f)
114
+ }),
115
+ (result) => {
116
+ const a = result.analysis;
117
+ for (const key of Object.keys(aggregated)) {
118
+ aggregated[key] += a[key];
119
+ }
120
+ fileDetails.push({
121
+ filePath: result.filePath,
122
+ pureFunctions: a.pureFunctions,
123
+ totalFunctions: a.totalFunctions
124
+ });
125
+ }
126
+ );
127
+ const hasTestFramework = (0, import_core.detectTestFramework)(options.rootDir);
171
128
  const indexResult = (0, import_core.calculateTestabilityIndex)({
172
129
  testFiles: testFiles.length,
173
130
  sourceFiles: sourceFiles.length,
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  analyzeTestability,
3
3
  calculateTestabilityScore
4
- } from "./chunk-QMDUZA7H.mjs";
4
+ } from "./chunk-CISO2RDG.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.22",
3
+ "version": "0.7.0",
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.23"
43
+ "@aiready/core": "0.24.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^24.0.0",
@@ -78,4 +78,35 @@ describe('Testability Scoring', () => {
78
78
  expect(scoring.recommendations[0].estimatedImpact).toBe(15);
79
79
  expect(scoring.recommendations[0].priority).toBe('high');
80
80
  });
81
+
82
+ it('should return ToolScoringOutput with score at top level (not summary.score)', () => {
83
+ const scoring = calculateTestabilityScore(mockReport);
84
+
85
+ // The bug was that CLI tried to access scoring.summary.score
86
+ // but ToolScoringOutput has score at the top level
87
+ expect(scoring.score).toBeDefined();
88
+ expect(typeof scoring.score).toBe('number');
89
+ expect(scoring.score).toBe(75);
90
+
91
+ // Verify there is no .summary property (which would cause the CLI crash)
92
+ expect((scoring as any).summary).toBeUndefined();
93
+ });
94
+
95
+ it('should return rating at top level (not summary.rating)', () => {
96
+ const scoring = calculateTestabilityScore(mockReport);
97
+
98
+ // The CLI accesses scoring.rating || report.summary.rating
99
+ // ToolScoringOutput may not have rating, but it should not have summary
100
+ expect((scoring as any).summary).toBeUndefined();
101
+ });
102
+
103
+ it('should have rawMetrics with testability data', () => {
104
+ const scoring = calculateTestabilityScore(mockReport);
105
+
106
+ expect(scoring.rawMetrics).toBeDefined();
107
+ expect(scoring.rawMetrics).toHaveProperty('sourceFiles');
108
+ expect(scoring.rawMetrics).toHaveProperty('testFiles');
109
+ expect(scoring.rawMetrics).toHaveProperty('pureFunctions');
110
+ expect(scoring.rawMetrics).toHaveProperty('totalFunctions');
111
+ });
81
112
  });