@aiready/testability 0.6.18 โ†’ 0.6.20

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
@@ -44,7 +44,7 @@ async function analyzeFileTestability(filePath) {
44
44
  totalInterfaces: 0,
45
45
  externalStateMutations: 0
46
46
  };
47
- const parser = (0, import_core.getParser)(filePath);
47
+ const parser = await (0, import_core.getParser)(filePath);
48
48
  if (!parser) return result;
49
49
  let code;
50
50
  try {
@@ -240,52 +240,22 @@ async function analyzeTestability(options) {
240
240
  var import_core2 = require("@aiready/core");
241
241
  function calculateTestabilityScore(report) {
242
242
  const { summary, rawData, recommendations } = report;
243
- const factors = [
244
- {
245
- name: "Test Coverage",
246
- impact: Math.round(summary.dimensions.testCoverageRatio - 50),
247
- description: `${rawData.testFiles} test files / ${rawData.sourceFiles} source files (${Math.round(summary.coverageRatio * 100)}%)`
248
- },
249
- {
250
- name: "Function Purity",
251
- impact: Math.round(summary.dimensions.purityScore - 50),
252
- description: `${rawData.pureFunctions}/${rawData.totalFunctions} functions are pure`
253
- },
254
- {
255
- name: "Dependency Injection",
256
- impact: Math.round(summary.dimensions.dependencyInjectionScore - 50),
257
- description: `${rawData.injectionPatterns}/${rawData.totalClasses} classes use DI`
258
- },
259
- {
260
- name: "Interface Focus",
261
- impact: Math.round(summary.dimensions.interfaceFocusScore - 50),
262
- description: `${rawData.bloatedInterfaces} interfaces have >10 methods`
263
- },
264
- {
265
- name: "Observability",
266
- impact: Math.round(summary.dimensions.observabilityScore - 50),
267
- description: `${rawData.externalStateMutations} functions mutate external state`
268
- }
269
- ];
270
- const recs = recommendations.map(
271
- (action) => ({
272
- action,
273
- estimatedImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
274
- priority: summary.aiChangeSafetyRating === "blind-risk" || summary.aiChangeSafetyRating === "high-risk" ? "high" : "medium"
275
- })
276
- );
277
- return {
243
+ return (0, import_core2.buildStandardToolScore)({
278
244
  toolName: import_core2.ToolName.TestabilityIndex,
279
245
  score: summary.score,
280
- rawMetrics: {
281
- ...rawData,
282
- rating: summary.rating,
283
- aiChangeSafetyRating: summary.aiChangeSafetyRating,
284
- coverageRatio: summary.coverageRatio
246
+ rawData,
247
+ dimensions: summary.dimensions,
248
+ dimensionNames: {
249
+ testCoverageRatio: "Test Coverage",
250
+ purityScore: "Function Purity",
251
+ dependencyInjectionScore: "Dependency Injection",
252
+ interfaceFocusScore: "Interface Focus",
253
+ observabilityScore: "Observability"
285
254
  },
286
- factors,
287
- recommendations: recs
288
- };
255
+ recommendations,
256
+ recommendationImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
257
+ rating: summary.aiChangeSafetyRating || summary.rating
258
+ });
289
259
  }
290
260
 
291
261
  // src/provider.ts
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  analyzeTestability,
3
3
  calculateTestabilityScore
4
- } from "./chunk-JL4S6RHJ.mjs";
4
+ } from "./chunk-QMDUZA7H.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.18",
3
+ "version": "0.6.20",
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.19"
43
+ "@aiready/core": "0.23.21"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^24.0.0",
@@ -1,14 +1,14 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
1
2
  import { analyzeTestability } from '../analyzer';
2
3
  import { join } from 'path';
3
- import { writeFileSync, mkdirSync, rmSync } from 'fs';
4
+ import { mkdirSync, writeFileSync, rmSync } from 'fs';
4
5
  import { tmpdir } from 'os';
5
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
6
6
 
7
7
  describe('Testability Analyzer', () => {
8
8
  let tmpDir: string;
9
9
 
10
10
  beforeAll(() => {
11
- tmpDir = join(tmpdir(), `aiready-testability-complex-${Date.now()}`);
11
+ tmpDir = join(tmpdir(), `aiready-testability-${Date.now()}`);
12
12
  mkdirSync(tmpDir, { recursive: true });
13
13
  });
14
14
 
@@ -52,55 +52,44 @@ describe('Testability Analyzer', () => {
52
52
  const javaReport = await analyzeTestability({ rootDir: javaDir });
53
53
  expect(javaReport.rawData.hasTestFramework).toBe(true);
54
54
 
55
- // Test Go detection (built-in)
55
+ // Test Go detection
56
56
  const goReport = await analyzeTestability({ rootDir: goDir });
57
57
  expect(goReport.rawData.hasTestFramework).toBe(true);
58
- });
58
+ }, 15000);
59
59
 
60
60
  it('flags missing test framework as critical', async () => {
61
61
  const emptyDir = join(tmpDir, 'no-tests');
62
62
  mkdirSync(emptyDir);
63
- writeFileSync(join(emptyDir, 'app.ts'), 'export const x = 1;');
63
+ writeFileSync(join(emptyDir, 'App.ts'), 'export const a = 1;');
64
64
 
65
65
  const report = await analyzeTestability({ rootDir: emptyDir });
66
66
  expect(report.rawData.hasTestFramework).toBe(false);
67
- expect(
68
- report.issues.some(
69
- (i) => i.dimension === 'framework' && i.severity === 'critical'
70
- )
71
- ).toBe(true);
67
+ // Rating is 'unverifiable' when no framework is found
68
+ expect(report.summary.rating).toBe('unverifiable');
72
69
  });
73
70
 
74
71
  it('detects injection patterns in classes', async () => {
75
72
  createTestFile(
76
73
  'src/di.ts',
77
- `
78
- export class Service {
79
- constructor(public db: any) {}
80
- }
81
- `
74
+ 'export class UserService { constructor(db: any) {} }'
82
75
  );
83
76
  const report = await analyzeTestability({ rootDir: tmpDir });
84
- expect(report.rawData.injectionPatterns).toBeGreaterThanOrEqual(1);
77
+ expect(report.rawData.injectionPatterns).toBeGreaterThan(0);
85
78
  });
86
79
 
87
80
  it('detects bloated interfaces', async () => {
88
81
  createTestFile(
89
82
  'src/bloated.ts',
90
- `
91
- export interface Massive {
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;
94
- }
95
- `
83
+ 'export interface Big { m1(); m2(); m3(); m4(); m5(); m6(); m7(); m8(); m9(); m10(); m11(); m12(); }'
96
84
  );
97
85
  const report = await analyzeTestability({ rootDir: tmpDir });
98
- expect(report.rawData.bloatedInterfaces).toBeGreaterThanOrEqual(1);
86
+ expect(report.rawData.bloatedInterfaces).toBeGreaterThan(0);
99
87
  });
100
88
 
101
89
  it('gracefully handles missing parser or read errors', async () => {
102
- createTestFile('src/config.json', '{"test": true}');
90
+ createTestFile('src/unknown.xyz', 'some content');
103
91
  const report = await analyzeTestability({ rootDir: tmpDir });
104
- expect(report.summary.sourceFiles).toBeDefined();
92
+ expect(report).toBeDefined();
93
+ expect(report.summary.sourceFiles).toBeGreaterThan(0);
105
94
  });
106
95
  });
@@ -44,7 +44,7 @@ describe('Testability Scoring', () => {
44
44
  expect(scoring.factors.length).toBe(5);
45
45
 
46
46
  const coverageFactor = scoring.factors.find(
47
- (f) => f.name === 'Test Coverage'
47
+ (f: any) => f.name === 'Test Coverage'
48
48
  );
49
49
  expect(coverageFactor?.impact).toBe(30); // 80 - 50
50
50
  expect(coverageFactor?.description).toContain(
package/src/analyzer.ts CHANGED
@@ -39,7 +39,7 @@ async function analyzeFileTestability(filePath: string): Promise<FileAnalysis> {
39
39
  externalStateMutations: 0,
40
40
  };
41
41
 
42
- const parser = getParser(filePath);
42
+ const parser = await getParser(filePath);
43
43
  if (!parser) return result;
44
44
 
45
45
  let code: string;
package/src/cli.ts CHANGED
@@ -8,15 +8,13 @@ import chalk from 'chalk';
8
8
  import { writeFileSync, mkdirSync, existsSync } from 'fs';
9
9
  import { dirname } from 'path';
10
10
  import {
11
- loadConfig,
12
- mergeConfigWithDefaults,
13
11
  resolveOutputPath,
14
- getScoreBar,
15
- getSafetyIcon,
16
- getSeverityColor,
12
+ displayStandardConsoleReport,
13
+ createStandardProgressCallback,
17
14
  } from '@aiready/core';
18
15
 
19
16
  const program = new Command();
17
+ const startTime = Date.now();
20
18
 
21
19
  program
22
20
  .name('aiready-testability')
@@ -62,21 +60,14 @@ EXAMPLES:
62
60
  .option('--output-file <path>', 'Output file path (for json)')
63
61
  .action(async (directory, options) => {
64
62
  console.log(chalk.blue('๐Ÿงช Analyzing testability...\n'));
65
- const startTime = Date.now();
66
-
67
- const config = await loadConfig(directory);
68
- const mergedConfig = mergeConfigWithDefaults(config, {
69
- minCoverageRatio: 0.3,
70
- });
71
63
 
72
64
  const finalOptions: TestabilityOptions = {
73
65
  rootDir: directory,
74
- minCoverageRatio:
75
- parseFloat(options.minCoverage ?? '0.3') ||
76
- mergedConfig.minCoverageRatio,
66
+ minCoverageRatio: parseFloat(options.minCoverage ?? '0.3'),
77
67
  testPatterns: options.testPatterns?.split(','),
78
68
  include: options.include?.split(','),
79
69
  exclude: options.exclude?.split(','),
70
+ onProgress: createStandardProgressCallback('testability'),
80
71
  };
81
72
 
82
73
  const report = await analyzeTestability(finalOptions);
@@ -95,82 +86,46 @@ EXAMPLES:
95
86
  writeFileSync(outputPath, JSON.stringify(payload, null, 2));
96
87
  console.log(chalk.green(`โœ“ Report saved to ${outputPath}`));
97
88
  } else {
98
- displayConsoleReport(report, scoring, elapsed);
89
+ displayStandardConsoleReport({
90
+ title: '๐Ÿงช Testability Analysis',
91
+ score: scoring.summary.score,
92
+ rating: scoring.summary.rating,
93
+ dimensions: [
94
+ {
95
+ name: 'Test Coverage',
96
+ value: scoring.summary.dimensions.testCoverageRatio,
97
+ },
98
+ {
99
+ name: 'Function Purity',
100
+ value: scoring.summary.dimensions.purityScore,
101
+ },
102
+ {
103
+ name: 'Dependency Injection',
104
+ value: scoring.summary.dimensions.dependencyInjectionScore,
105
+ },
106
+ {
107
+ name: 'Interface Focus',
108
+ value: scoring.summary.dimensions.interfaceFocusScore,
109
+ },
110
+ {
111
+ name: 'Observability',
112
+ value: scoring.summary.dimensions.observabilityScore,
113
+ },
114
+ ],
115
+ stats: [
116
+ { label: 'Source Files', value: report.rawData.sourceFiles },
117
+ { label: 'Test Files', value: report.rawData.testFiles },
118
+ {
119
+ label: 'Coverage Ratio',
120
+ value: Math.round(scoring.summary.coverageRatio * 100) + '%',
121
+ },
122
+ ],
123
+ issues: report.issues,
124
+ recommendations: report.recommendations,
125
+ elapsedTime: elapsed,
126
+ safetyRating: report.summary.aiChangeSafetyRating,
127
+ });
99
128
  }
100
129
  });
101
130
 
102
131
  program.parse();
103
-
104
- function displayConsoleReport(report: any, scoring: any, elapsed: string) {
105
- const { summary, rawData, issues, recommendations } = report;
106
-
107
- // The most important banner
108
- const safetyRating = summary.aiChangeSafetyRating;
109
- console.log(chalk.bold('\n๐Ÿงช Testability Analysis\n'));
110
-
111
- if (safetyRating === 'blind-risk') {
112
- console.log(
113
- chalk.bgRed.white.bold(
114
- ' ๐Ÿ’€ BLIND RISK โ€” NO TESTS DETECTED. AI-GENERATED CHANGES CANNOT BE VERIFIED. '
115
- )
116
- );
117
- console.log();
118
- } else if (safetyRating === 'high-risk') {
119
- console.log(
120
- chalk.red.bold(
121
- ` ๐Ÿ”ด HIGH RISK โ€” Insufficient test coverage. AI changes may introduce silent bugs.`
122
- )
123
- );
124
- console.log();
125
- }
126
-
127
- const safetyColor = getSeverityColor(safetyRating, chalk);
128
- console.log(
129
- `AI Change Safety: ${safetyColor(`${getSafetyIcon(safetyRating)} ${safetyRating.toUpperCase()}`)}`
130
- );
131
- console.log(
132
- `Score: ${chalk.bold(summary.score + '/100')} (${summary.rating})`
133
- );
134
- console.log(
135
- `Source Files: ${chalk.cyan(rawData.sourceFiles)} Test Files: ${chalk.cyan(rawData.testFiles)}`
136
- );
137
- console.log(
138
- `Coverage Ratio: ${chalk.bold(Math.round(summary.coverageRatio * 100) + '%')}`
139
- );
140
- console.log(`Analysis Time: ${chalk.gray(elapsed + 's')}\n`);
141
-
142
- console.log(chalk.bold('๐Ÿ“ Dimension Scores\n'));
143
- const dims: [string, number][] = [
144
- ['Test Coverage', summary.dimensions.testCoverageRatio],
145
- ['Function Purity', summary.dimensions.purityScore],
146
- ['Dependency Injection', summary.dimensions.dependencyInjectionScore],
147
- ['Interface Focus', summary.dimensions.interfaceFocusScore],
148
- ['Observability', summary.dimensions.observabilityScore],
149
- ];
150
- for (const [name, val] of dims) {
151
- const color =
152
- val >= 70 ? chalk.green : val >= 50 ? chalk.yellow : chalk.red;
153
- console.log(` ${name.padEnd(22)} ${color(getScoreBar(val))} ${val}/100`);
154
- }
155
-
156
- if (issues.length > 0) {
157
- console.log(chalk.bold('\nโš ๏ธ Issues\n'));
158
- for (const issue of issues) {
159
- const sev = getSeverityColor(issue.severity, chalk);
160
- console.log(`${sev(issue.severity.toUpperCase())} ${issue.message}`);
161
- if (issue.suggestion)
162
- console.log(
163
- ` ${chalk.dim('โ†’')} ${chalk.italic(issue.suggestion)}`
164
- );
165
- console.log();
166
- }
167
- }
168
-
169
- if (recommendations.length > 0) {
170
- console.log(chalk.bold('๐Ÿ’ก Recommendations\n'));
171
- recommendations.forEach((rec: string, i: number) => {
172
- console.log(`${i + 1}. ${rec}`);
173
- });
174
- }
175
- console.log();
176
- }
package/src/scoring.ts CHANGED
@@ -1,67 +1,27 @@
1
- import { type ToolScoringOutput, ToolName } from '@aiready/core';
1
+ import { ToolName, buildStandardToolScore } from '@aiready/core';
2
2
  import type { TestabilityReport } from './types';
3
3
 
4
4
  /**
5
5
  * Convert testability report into a ToolScoringOutput for the unified score.
6
- *
7
- * @param report - The comprehensive testability report containing raw metrics and summary.
8
- * @returns Standardized scoring output with impact factors and recommendations.
9
6
  */
10
- export function calculateTestabilityScore(
11
- report: TestabilityReport
12
- ): ToolScoringOutput {
7
+ export function calculateTestabilityScore(report: TestabilityReport): any {
13
8
  const { summary, rawData, recommendations } = report;
14
9
 
15
- const factors: ToolScoringOutput['factors'] = [
16
- {
17
- name: 'Test Coverage',
18
- impact: Math.round(summary.dimensions.testCoverageRatio - 50),
19
- description: `${rawData.testFiles} test files / ${rawData.sourceFiles} source files (${Math.round(summary.coverageRatio * 100)}%)`,
20
- },
21
- {
22
- name: 'Function Purity',
23
- impact: Math.round(summary.dimensions.purityScore - 50),
24
- description: `${rawData.pureFunctions}/${rawData.totalFunctions} functions are pure`,
25
- },
26
- {
27
- name: 'Dependency Injection',
28
- impact: Math.round(summary.dimensions.dependencyInjectionScore - 50),
29
- description: `${rawData.injectionPatterns}/${rawData.totalClasses} classes use DI`,
30
- },
31
- {
32
- name: 'Interface Focus',
33
- impact: Math.round(summary.dimensions.interfaceFocusScore - 50),
34
- description: `${rawData.bloatedInterfaces} interfaces have >10 methods`,
35
- },
36
- {
37
- name: 'Observability',
38
- impact: Math.round(summary.dimensions.observabilityScore - 50),
39
- description: `${rawData.externalStateMutations} functions mutate external state`,
40
- },
41
- ];
42
-
43
- const recs: ToolScoringOutput['recommendations'] = recommendations.map(
44
- (action) => ({
45
- action,
46
- estimatedImpact: summary.aiChangeSafetyRating === 'blind-risk' ? 15 : 8,
47
- priority:
48
- summary.aiChangeSafetyRating === 'blind-risk' ||
49
- summary.aiChangeSafetyRating === 'high-risk'
50
- ? 'high'
51
- : 'medium',
52
- })
53
- );
54
-
55
- return {
10
+ return buildStandardToolScore({
56
11
  toolName: ToolName.TestabilityIndex,
57
12
  score: summary.score,
58
- rawMetrics: {
59
- ...rawData,
60
- rating: summary.rating,
61
- aiChangeSafetyRating: summary.aiChangeSafetyRating,
62
- coverageRatio: summary.coverageRatio,
13
+ rawData,
14
+ dimensions: summary.dimensions,
15
+ dimensionNames: {
16
+ testCoverageRatio: 'Test Coverage',
17
+ purityScore: 'Function Purity',
18
+ dependencyInjectionScore: 'Dependency Injection',
19
+ interfaceFocusScore: 'Interface Focus',
20
+ observabilityScore: 'Observability',
63
21
  },
64
- factors,
65
- recommendations: recs,
66
- };
22
+ recommendations,
23
+ recommendationImpact:
24
+ summary.aiChangeSafetyRating === 'blind-risk' ? 15 : 8,
25
+ rating: summary.aiChangeSafetyRating || summary.rating,
26
+ });
67
27
  }