@aiready/testability 0.1.4 → 0.1.6
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/.turbo/turbo-build.log +8 -8
- package/.turbo/turbo-test.log +4 -4
- package/dist/chunk-YLYLRZRS.mjs +363 -0
- package/dist/cli.js +76 -22
- package/dist/cli.mjs +46 -13
- package/dist/index.js +36 -11
- package/dist/index.mjs +1 -1
- package/package.json +3 -3
- package/src/__tests__/analyzer.test.ts +18 -6
- package/src/analyzer.ts +103 -41
- package/src/cli.ts +80 -29
- package/src/scoring.ts +14 -8
- package/src/types.ts +7 -1
package/src/cli.ts
CHANGED
|
@@ -7,15 +7,23 @@ import type { TestabilityOptions } from './types';
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
9
9
|
import { dirname } from 'path';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
loadConfig,
|
|
12
|
+
mergeConfigWithDefaults,
|
|
13
|
+
resolveOutputPath,
|
|
14
|
+
} from '@aiready/core';
|
|
11
15
|
|
|
12
16
|
const program = new Command();
|
|
13
17
|
|
|
14
18
|
program
|
|
15
19
|
.name('aiready-testability')
|
|
16
|
-
.description(
|
|
20
|
+
.description(
|
|
21
|
+
'Measure how safely AI-generated changes can be verified in your codebase'
|
|
22
|
+
)
|
|
17
23
|
.version('0.1.0')
|
|
18
|
-
.addHelpText(
|
|
24
|
+
.addHelpText(
|
|
25
|
+
'after',
|
|
26
|
+
`
|
|
19
27
|
DIMENSIONS MEASURED:
|
|
20
28
|
Test Coverage Ratio of test files to source files
|
|
21
29
|
Function Purity Pure functions are trivially AI-testable
|
|
@@ -33,10 +41,18 @@ EXAMPLES:
|
|
|
33
41
|
aiready-testability . # Full analysis
|
|
34
42
|
aiready-testability src/ --output json # JSON report
|
|
35
43
|
aiready-testability . --min-coverage 0.5 # Stricter 50% threshold
|
|
36
|
-
`
|
|
44
|
+
`
|
|
45
|
+
)
|
|
37
46
|
.argument('<directory>', 'Directory to analyze')
|
|
38
|
-
.option(
|
|
39
|
-
|
|
47
|
+
.option(
|
|
48
|
+
'--min-coverage <ratio>',
|
|
49
|
+
'Minimum acceptable test/source ratio (default: 0.3)',
|
|
50
|
+
'0.3'
|
|
51
|
+
)
|
|
52
|
+
.option(
|
|
53
|
+
'--test-patterns <patterns>',
|
|
54
|
+
'Additional test file patterns (comma-separated)'
|
|
55
|
+
)
|
|
40
56
|
.option('--include <patterns>', 'File patterns to include (comma-separated)')
|
|
41
57
|
.option('--exclude <patterns>', 'File patterns to exclude (comma-separated)')
|
|
42
58
|
.option('-o, --output <format>', 'Output format: console|json', 'console')
|
|
@@ -52,7 +68,9 @@ EXAMPLES:
|
|
|
52
68
|
|
|
53
69
|
const finalOptions: TestabilityOptions = {
|
|
54
70
|
rootDir: directory,
|
|
55
|
-
minCoverageRatio:
|
|
71
|
+
minCoverageRatio:
|
|
72
|
+
parseFloat(options.minCoverage ?? '0.3') ||
|
|
73
|
+
mergedConfig.minCoverageRatio,
|
|
56
74
|
testPatterns: options.testPatterns?.split(','),
|
|
57
75
|
include: options.include?.split(','),
|
|
58
76
|
exclude: options.exclude?.split(','),
|
|
@@ -67,7 +85,7 @@ EXAMPLES:
|
|
|
67
85
|
const outputPath = resolveOutputPath(
|
|
68
86
|
options.outputFile,
|
|
69
87
|
`testability-report-${new Date().toISOString().split('T')[0]}.json`,
|
|
70
|
-
directory
|
|
88
|
+
directory
|
|
71
89
|
);
|
|
72
90
|
const dir = dirname(outputPath);
|
|
73
91
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
@@ -82,21 +100,31 @@ program.parse();
|
|
|
82
100
|
|
|
83
101
|
function safetyColor(rating: string) {
|
|
84
102
|
switch (rating) {
|
|
85
|
-
case 'safe':
|
|
86
|
-
|
|
87
|
-
case '
|
|
88
|
-
|
|
89
|
-
|
|
103
|
+
case 'safe':
|
|
104
|
+
return chalk.green;
|
|
105
|
+
case 'moderate-risk':
|
|
106
|
+
return chalk.yellow;
|
|
107
|
+
case 'high-risk':
|
|
108
|
+
return chalk.red;
|
|
109
|
+
case 'blind-risk':
|
|
110
|
+
return chalk.bgRed.white;
|
|
111
|
+
default:
|
|
112
|
+
return chalk.white;
|
|
90
113
|
}
|
|
91
114
|
}
|
|
92
115
|
|
|
93
116
|
function safetyIcon(rating: string) {
|
|
94
117
|
switch (rating) {
|
|
95
|
-
case 'safe':
|
|
96
|
-
|
|
97
|
-
case '
|
|
98
|
-
|
|
99
|
-
|
|
118
|
+
case 'safe':
|
|
119
|
+
return '✅';
|
|
120
|
+
case 'moderate-risk':
|
|
121
|
+
return '⚠️ ';
|
|
122
|
+
case 'high-risk':
|
|
123
|
+
return '🔴';
|
|
124
|
+
case 'blind-risk':
|
|
125
|
+
return '💀';
|
|
126
|
+
default:
|
|
127
|
+
return '❓';
|
|
100
128
|
}
|
|
101
129
|
}
|
|
102
130
|
|
|
@@ -112,19 +140,33 @@ function displayConsoleReport(report: any, scoring: any, elapsed: string) {
|
|
|
112
140
|
console.log(chalk.bold('\n🧪 Testability Analysis\n'));
|
|
113
141
|
|
|
114
142
|
if (safetyRating === 'blind-risk') {
|
|
115
|
-
console.log(
|
|
116
|
-
|
|
117
|
-
|
|
143
|
+
console.log(
|
|
144
|
+
chalk.bgRed.white.bold(
|
|
145
|
+
' 💀 BLIND RISK — NO TESTS DETECTED. AI-GENERATED CHANGES CANNOT BE VERIFIED. '
|
|
146
|
+
)
|
|
147
|
+
);
|
|
118
148
|
console.log();
|
|
119
149
|
} else if (safetyRating === 'high-risk') {
|
|
120
|
-
console.log(
|
|
150
|
+
console.log(
|
|
151
|
+
chalk.red.bold(
|
|
152
|
+
` 🔴 HIGH RISK — Insufficient test coverage. AI changes may introduce silent bugs.`
|
|
153
|
+
)
|
|
154
|
+
);
|
|
121
155
|
console.log();
|
|
122
156
|
}
|
|
123
157
|
|
|
124
|
-
console.log(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
console.log(
|
|
158
|
+
console.log(
|
|
159
|
+
`AI Change Safety: ${safetyColor(safetyRating)(`${safetyIcon(safetyRating)} ${safetyRating.toUpperCase()}`)}`
|
|
160
|
+
);
|
|
161
|
+
console.log(
|
|
162
|
+
`Score: ${chalk.bold(summary.score + '/100')} (${summary.rating})`
|
|
163
|
+
);
|
|
164
|
+
console.log(
|
|
165
|
+
`Source Files: ${chalk.cyan(rawData.sourceFiles)} Test Files: ${chalk.cyan(rawData.testFiles)}`
|
|
166
|
+
);
|
|
167
|
+
console.log(
|
|
168
|
+
`Coverage Ratio: ${chalk.bold(Math.round(summary.coverageRatio * 100) + '%')}`
|
|
169
|
+
);
|
|
128
170
|
console.log(`Analysis Time: ${chalk.gray(elapsed + 's')}\n`);
|
|
129
171
|
|
|
130
172
|
console.log(chalk.bold('📐 Dimension Scores\n'));
|
|
@@ -136,16 +178,25 @@ function displayConsoleReport(report: any, scoring: any, elapsed: string) {
|
|
|
136
178
|
['Observability', summary.dimensions.observabilityScore],
|
|
137
179
|
];
|
|
138
180
|
for (const [name, val] of dims) {
|
|
139
|
-
const color =
|
|
181
|
+
const color =
|
|
182
|
+
val >= 70 ? chalk.green : val >= 50 ? chalk.yellow : chalk.red;
|
|
140
183
|
console.log(` ${name.padEnd(22)} ${color(scoreBar(val))} ${val}/100`);
|
|
141
184
|
}
|
|
142
185
|
|
|
143
186
|
if (issues.length > 0) {
|
|
144
187
|
console.log(chalk.bold('\n⚠️ Issues\n'));
|
|
145
188
|
for (const issue of issues) {
|
|
146
|
-
const sev =
|
|
189
|
+
const sev =
|
|
190
|
+
issue.severity === 'critical'
|
|
191
|
+
? chalk.red
|
|
192
|
+
: issue.severity === 'major'
|
|
193
|
+
? chalk.yellow
|
|
194
|
+
: chalk.blue;
|
|
147
195
|
console.log(`${sev(issue.severity.toUpperCase())} ${issue.message}`);
|
|
148
|
-
if (issue.suggestion)
|
|
196
|
+
if (issue.suggestion)
|
|
197
|
+
console.log(
|
|
198
|
+
` ${chalk.dim('→')} ${chalk.italic(issue.suggestion)}`
|
|
199
|
+
);
|
|
149
200
|
console.log();
|
|
150
201
|
}
|
|
151
202
|
}
|
package/src/scoring.ts
CHANGED
|
@@ -5,7 +5,9 @@ import type { TestabilityReport } from './types';
|
|
|
5
5
|
/**
|
|
6
6
|
* Convert testability report into a ToolScoringOutput for the unified score.
|
|
7
7
|
*/
|
|
8
|
-
export function calculateTestabilityScore(
|
|
8
|
+
export function calculateTestabilityScore(
|
|
9
|
+
report: TestabilityReport
|
|
10
|
+
): ToolScoringOutput {
|
|
9
11
|
const { summary, rawData, recommendations } = report;
|
|
10
12
|
|
|
11
13
|
const factors: ToolScoringOutput['factors'] = [
|
|
@@ -36,13 +38,17 @@ export function calculateTestabilityScore(report: TestabilityReport): ToolScorin
|
|
|
36
38
|
},
|
|
37
39
|
];
|
|
38
40
|
|
|
39
|
-
const recs: ToolScoringOutput['recommendations'] = recommendations.map(
|
|
40
|
-
action
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
const recs: ToolScoringOutput['recommendations'] = recommendations.map(
|
|
42
|
+
(action) => ({
|
|
43
|
+
action,
|
|
44
|
+
estimatedImpact: summary.aiChangeSafetyRating === 'blind-risk' ? 15 : 8,
|
|
45
|
+
priority:
|
|
46
|
+
summary.aiChangeSafetyRating === 'blind-risk' ||
|
|
47
|
+
summary.aiChangeSafetyRating === 'high-risk'
|
|
48
|
+
? 'high'
|
|
49
|
+
: 'medium',
|
|
50
|
+
})
|
|
51
|
+
);
|
|
46
52
|
|
|
47
53
|
return {
|
|
48
54
|
toolName: 'testability',
|
package/src/types.ts
CHANGED
|
@@ -18,7 +18,13 @@ export interface TestabilityOptions {
|
|
|
18
18
|
export interface TestabilityIssue extends Issue {
|
|
19
19
|
type: 'low-testability';
|
|
20
20
|
/** Category of testability barrier */
|
|
21
|
-
dimension:
|
|
21
|
+
dimension:
|
|
22
|
+
| 'test-coverage'
|
|
23
|
+
| 'purity'
|
|
24
|
+
| 'dependency-injection'
|
|
25
|
+
| 'interface-focus'
|
|
26
|
+
| 'observability'
|
|
27
|
+
| 'framework';
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
export interface TestabilityReport {
|