@aiready/testability 0.1.5 → 0.1.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.
- package/.turbo/turbo-build.log +10 -10
- package/.turbo/turbo-lint.log +5 -0
- package/.turbo/turbo-test.log +5 -6
- package/dist/chunk-D7AV63F3.mjs +332 -0
- package/dist/chunk-DDNB7FI4.mjs +333 -0
- package/dist/chunk-PAP7ZRNB.mjs +366 -0
- package/dist/chunk-YLYLRZRS.mjs +363 -0
- package/dist/cli.js +93 -69
- package/dist/cli.mjs +46 -13
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +53 -58
- package/dist/index.mjs +1 -1
- package/package.json +3 -3
- package/src/__tests__/analyzer.test.ts +18 -6
- package/src/analyzer.ts +116 -92
- package/src/cli.ts +80 -29
- package/src/scoring.ts +14 -9
- package/src/types.ts +10 -2
package/dist/cli.mjs
CHANGED
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
import {
|
|
3
3
|
analyzeTestability,
|
|
4
4
|
calculateTestabilityScore
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-DDNB7FI4.mjs";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import chalk from "chalk";
|
|
10
10
|
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
11
11
|
import { dirname } from "path";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
loadConfig,
|
|
14
|
+
mergeConfigWithDefaults,
|
|
15
|
+
resolveOutputPath
|
|
16
|
+
} from "@aiready/core";
|
|
13
17
|
var program = new Command();
|
|
14
|
-
program.name("aiready-testability").description(
|
|
18
|
+
program.name("aiready-testability").description(
|
|
19
|
+
"Measure how safely AI-generated changes can be verified in your codebase"
|
|
20
|
+
).version("0.1.0").addHelpText(
|
|
21
|
+
"after",
|
|
22
|
+
`
|
|
15
23
|
DIMENSIONS MEASURED:
|
|
16
24
|
Test Coverage Ratio of test files to source files
|
|
17
25
|
Function Purity Pure functions are trivially AI-testable
|
|
@@ -29,7 +37,15 @@ EXAMPLES:
|
|
|
29
37
|
aiready-testability . # Full analysis
|
|
30
38
|
aiready-testability src/ --output json # JSON report
|
|
31
39
|
aiready-testability . --min-coverage 0.5 # Stricter 50% threshold
|
|
32
|
-
`
|
|
40
|
+
`
|
|
41
|
+
).argument("<directory>", "Directory to analyze").option(
|
|
42
|
+
"--min-coverage <ratio>",
|
|
43
|
+
"Minimum acceptable test/source ratio (default: 0.3)",
|
|
44
|
+
"0.3"
|
|
45
|
+
).option(
|
|
46
|
+
"--test-patterns <patterns>",
|
|
47
|
+
"Additional test file patterns (comma-separated)"
|
|
48
|
+
).option("--include <patterns>", "File patterns to include (comma-separated)").option("--exclude <patterns>", "File patterns to exclude (comma-separated)").option("-o, --output <format>", "Output format: console|json", "console").option("--output-file <path>", "Output file path (for json)").action(async (directory, options) => {
|
|
33
49
|
console.log(chalk.blue("\u{1F9EA} Analyzing testability...\n"));
|
|
34
50
|
const startTime = Date.now();
|
|
35
51
|
const config = await loadConfig(directory);
|
|
@@ -98,18 +114,32 @@ function displayConsoleReport(report, scoring, elapsed) {
|
|
|
98
114
|
const safetyRating = summary.aiChangeSafetyRating;
|
|
99
115
|
console.log(chalk.bold("\n\u{1F9EA} Testability Analysis\n"));
|
|
100
116
|
if (safetyRating === "blind-risk") {
|
|
101
|
-
console.log(
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
console.log(
|
|
118
|
+
chalk.bgRed.white.bold(
|
|
119
|
+
" \u{1F480} BLIND RISK \u2014 NO TESTS DETECTED. AI-GENERATED CHANGES CANNOT BE VERIFIED. "
|
|
120
|
+
)
|
|
121
|
+
);
|
|
104
122
|
console.log();
|
|
105
123
|
} else if (safetyRating === "high-risk") {
|
|
106
|
-
console.log(
|
|
124
|
+
console.log(
|
|
125
|
+
chalk.red.bold(
|
|
126
|
+
` \u{1F534} HIGH RISK \u2014 Insufficient test coverage. AI changes may introduce silent bugs.`
|
|
127
|
+
)
|
|
128
|
+
);
|
|
107
129
|
console.log();
|
|
108
130
|
}
|
|
109
|
-
console.log(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
console.log(
|
|
131
|
+
console.log(
|
|
132
|
+
`AI Change Safety: ${safetyColor(safetyRating)(`${safetyIcon(safetyRating)} ${safetyRating.toUpperCase()}`)}`
|
|
133
|
+
);
|
|
134
|
+
console.log(
|
|
135
|
+
`Score: ${chalk.bold(summary.score + "/100")} (${summary.rating})`
|
|
136
|
+
);
|
|
137
|
+
console.log(
|
|
138
|
+
`Source Files: ${chalk.cyan(rawData.sourceFiles)} Test Files: ${chalk.cyan(rawData.testFiles)}`
|
|
139
|
+
);
|
|
140
|
+
console.log(
|
|
141
|
+
`Coverage Ratio: ${chalk.bold(Math.round(summary.coverageRatio * 100) + "%")}`
|
|
142
|
+
);
|
|
113
143
|
console.log(`Analysis Time: ${chalk.gray(elapsed + "s")}
|
|
114
144
|
`);
|
|
115
145
|
console.log(chalk.bold("\u{1F4D0} Dimension Scores\n"));
|
|
@@ -129,7 +159,10 @@ function displayConsoleReport(report, scoring, elapsed) {
|
|
|
129
159
|
for (const issue of issues) {
|
|
130
160
|
const sev = issue.severity === "critical" ? chalk.red : issue.severity === "major" ? chalk.yellow : chalk.blue;
|
|
131
161
|
console.log(`${sev(issue.severity.toUpperCase())} ${issue.message}`);
|
|
132
|
-
if (issue.suggestion)
|
|
162
|
+
if (issue.suggestion)
|
|
163
|
+
console.log(
|
|
164
|
+
` ${chalk.dim("\u2192")} ${chalk.italic(issue.suggestion)}`
|
|
165
|
+
);
|
|
133
166
|
console.log();
|
|
134
167
|
}
|
|
135
168
|
}
|
package/dist/index.d.mts
CHANGED
|
@@ -13,6 +13,8 @@ interface TestabilityOptions {
|
|
|
13
13
|
include?: string[];
|
|
14
14
|
/** File glob patterns to exclude */
|
|
15
15
|
exclude?: string[];
|
|
16
|
+
/** Progress callback */
|
|
17
|
+
onProgress?: (processed: number, total: number, message: string) => void;
|
|
16
18
|
}
|
|
17
19
|
interface TestabilityIssue extends Issue {
|
|
18
20
|
type: 'low-testability';
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ interface TestabilityOptions {
|
|
|
13
13
|
include?: string[];
|
|
14
14
|
/** File glob patterns to exclude */
|
|
15
15
|
exclude?: string[];
|
|
16
|
+
/** Progress callback */
|
|
17
|
+
onProgress?: (processed: number, total: number, message: string) => void;
|
|
16
18
|
}
|
|
17
19
|
interface TestabilityIssue extends Issue {
|
|
18
20
|
type: 'low-testability';
|
package/dist/index.js
CHANGED
|
@@ -26,56 +26,10 @@ __export(index_exports, {
|
|
|
26
26
|
module.exports = __toCommonJS(index_exports);
|
|
27
27
|
|
|
28
28
|
// src/analyzer.ts
|
|
29
|
+
var import_core = require("@aiready/core");
|
|
29
30
|
var import_fs = require("fs");
|
|
30
31
|
var import_path = require("path");
|
|
31
32
|
var import_typescript_estree = require("@typescript-eslint/typescript-estree");
|
|
32
|
-
var import_core = require("@aiready/core");
|
|
33
|
-
var SRC_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
34
|
-
var DEFAULT_EXCLUDES = ["node_modules", "dist", ".git", "coverage", ".turbo", "build"];
|
|
35
|
-
var TEST_PATTERNS = [
|
|
36
|
-
/\.(test|spec)\.(ts|tsx|js|jsx)$/,
|
|
37
|
-
/__tests__\//,
|
|
38
|
-
/\/tests?\//,
|
|
39
|
-
/\/e2e\//,
|
|
40
|
-
/\/fixtures\//
|
|
41
|
-
];
|
|
42
|
-
function isTestFile(filePath, extra) {
|
|
43
|
-
if (TEST_PATTERNS.some((p) => p.test(filePath))) return true;
|
|
44
|
-
if (extra) return extra.some((p) => filePath.includes(p));
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
function isSourceFile(filePath) {
|
|
48
|
-
return SRC_EXTENSIONS.has((0, import_path.extname)(filePath));
|
|
49
|
-
}
|
|
50
|
-
function collectFiles(dir, options, depth = 0) {
|
|
51
|
-
if (depth > (options.maxDepth ?? 20)) return [];
|
|
52
|
-
const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
|
|
53
|
-
const files = [];
|
|
54
|
-
let entries;
|
|
55
|
-
try {
|
|
56
|
-
entries = (0, import_fs.readdirSync)(dir);
|
|
57
|
-
} catch {
|
|
58
|
-
return files;
|
|
59
|
-
}
|
|
60
|
-
for (const entry of entries) {
|
|
61
|
-
if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
|
|
62
|
-
const full = (0, import_path.join)(dir, entry);
|
|
63
|
-
let stat;
|
|
64
|
-
try {
|
|
65
|
-
stat = (0, import_fs.statSync)(full);
|
|
66
|
-
} catch {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (stat.isDirectory()) {
|
|
70
|
-
files.push(...collectFiles(full, options, depth + 1));
|
|
71
|
-
} else if (stat.isFile() && isSourceFile(full)) {
|
|
72
|
-
if (!options.include || options.include.some((p) => full.includes(p))) {
|
|
73
|
-
files.push(full);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return files;
|
|
78
|
-
}
|
|
79
33
|
function countMethodsInInterface(node) {
|
|
80
34
|
if (node.type === "TSInterfaceDeclaration") {
|
|
81
35
|
return node.body.body.filter(
|
|
@@ -107,8 +61,12 @@ function isPureFunction(fn) {
|
|
|
107
61
|
let hasSideEffect = false;
|
|
108
62
|
function walk(node) {
|
|
109
63
|
if (node.type === "ReturnStatement" && node.argument) hasReturn = true;
|
|
110
|
-
if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
|
|
111
|
-
|
|
64
|
+
if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
|
|
65
|
+
hasSideEffect = true;
|
|
66
|
+
if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && ["console", "process", "window", "document", "fs"].includes(
|
|
67
|
+
node.callee.object.name
|
|
68
|
+
))
|
|
69
|
+
hasSideEffect = true;
|
|
112
70
|
for (const key of Object.keys(node)) {
|
|
113
71
|
if (key === "parent") continue;
|
|
114
72
|
const child = node[key];
|
|
@@ -132,7 +90,8 @@ function hasExternalStateMutation(fn) {
|
|
|
132
90
|
let found = false;
|
|
133
91
|
function walk(node) {
|
|
134
92
|
if (found) return;
|
|
135
|
-
if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
|
|
93
|
+
if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
|
|
94
|
+
found = true;
|
|
136
95
|
for (const key of Object.keys(node)) {
|
|
137
96
|
if (key === "parent") continue;
|
|
138
97
|
const child = node[key];
|
|
@@ -207,15 +166,42 @@ function detectTestFramework(rootDir) {
|
|
|
207
166
|
...pkg.dependencies ?? {},
|
|
208
167
|
...pkg.devDependencies ?? {}
|
|
209
168
|
};
|
|
210
|
-
const testFrameworks = [
|
|
169
|
+
const testFrameworks = [
|
|
170
|
+
"jest",
|
|
171
|
+
"vitest",
|
|
172
|
+
"mocha",
|
|
173
|
+
"jasmine",
|
|
174
|
+
"ava",
|
|
175
|
+
"tap",
|
|
176
|
+
"pytest",
|
|
177
|
+
"unittest"
|
|
178
|
+
];
|
|
211
179
|
return testFrameworks.some((fw) => allDeps[fw]);
|
|
212
180
|
} catch {
|
|
213
181
|
return false;
|
|
214
182
|
}
|
|
215
183
|
}
|
|
184
|
+
var TEST_PATTERNS = [
|
|
185
|
+
/\.(test|spec)\.(ts|tsx|js|jsx)$/,
|
|
186
|
+
/__tests__\//,
|
|
187
|
+
/\/tests?\//,
|
|
188
|
+
/\/e2e\//,
|
|
189
|
+
/\/fixtures\//
|
|
190
|
+
];
|
|
191
|
+
function isTestFile(filePath, extra) {
|
|
192
|
+
if (TEST_PATTERNS.some((p) => p.test(filePath))) return true;
|
|
193
|
+
if (extra) return extra.some((p) => filePath.includes(p));
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
216
196
|
async function analyzeTestability(options) {
|
|
217
|
-
const allFiles =
|
|
218
|
-
|
|
197
|
+
const allFiles = await (0, import_core.scanFiles)({
|
|
198
|
+
...options,
|
|
199
|
+
include: options.include || ["**/*.{ts,tsx,js,jsx}"],
|
|
200
|
+
includeTests: true
|
|
201
|
+
});
|
|
202
|
+
const sourceFiles = allFiles.filter(
|
|
203
|
+
(f) => !isTestFile(f, options.testPatterns)
|
|
204
|
+
);
|
|
219
205
|
const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
|
|
220
206
|
const aggregated = {
|
|
221
207
|
pureFunctions: 0,
|
|
@@ -226,7 +212,14 @@ async function analyzeTestability(options) {
|
|
|
226
212
|
totalInterfaces: 0,
|
|
227
213
|
externalStateMutations: 0
|
|
228
214
|
};
|
|
215
|
+
let processed = 0;
|
|
229
216
|
for (const f of sourceFiles) {
|
|
217
|
+
processed++;
|
|
218
|
+
options.onProgress?.(
|
|
219
|
+
processed,
|
|
220
|
+
sourceFiles.length,
|
|
221
|
+
`testability: analyzing files`
|
|
222
|
+
);
|
|
230
223
|
const a = analyzeFileTestability(f);
|
|
231
224
|
for (const key of Object.keys(aggregated)) {
|
|
232
225
|
aggregated[key] += a[key];
|
|
@@ -340,11 +333,13 @@ function calculateTestabilityScore(report) {
|
|
|
340
333
|
description: `${rawData.externalStateMutations} functions mutate external state`
|
|
341
334
|
}
|
|
342
335
|
];
|
|
343
|
-
const recs = recommendations.map(
|
|
344
|
-
action
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
336
|
+
const recs = recommendations.map(
|
|
337
|
+
(action) => ({
|
|
338
|
+
action,
|
|
339
|
+
estimatedImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
|
|
340
|
+
priority: summary.aiChangeSafetyRating === "blind-risk" || summary.aiChangeSafetyRating === "high-risk" ? "high" : "medium"
|
|
341
|
+
})
|
|
342
|
+
);
|
|
348
343
|
return {
|
|
349
344
|
toolName: "testability",
|
|
350
345
|
score: summary.score,
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/testability",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.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",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"@typescript-eslint/typescript-estree": "^8.53.0",
|
|
40
40
|
"chalk": "^5.3.0",
|
|
41
41
|
"commander": "^14.0.0",
|
|
42
|
-
"glob": "^
|
|
43
|
-
"@aiready/core": "0.9.
|
|
42
|
+
"glob": "^13.0.0",
|
|
43
|
+
"@aiready/core": "0.9.35"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^24.0.0",
|
|
@@ -26,8 +26,14 @@ describe('Testability Analyzer', () => {
|
|
|
26
26
|
describe('Test Coverage Ratio', () => {
|
|
27
27
|
it('should calculate ratio of test files to source files', async () => {
|
|
28
28
|
createTestFile('src/math.ts', 'export const add = (a, b) => a + b;');
|
|
29
|
-
createTestFile(
|
|
30
|
-
|
|
29
|
+
createTestFile(
|
|
30
|
+
'src/math.test.ts',
|
|
31
|
+
'import { add } from "./math"; test("add", () => {});'
|
|
32
|
+
);
|
|
33
|
+
createTestFile(
|
|
34
|
+
'src/string.ts',
|
|
35
|
+
'export const upper = (s) => s.toUpperCase();'
|
|
36
|
+
);
|
|
31
37
|
|
|
32
38
|
const report = await analyzeTestability({ rootDir: tmpDir });
|
|
33
39
|
|
|
@@ -38,7 +44,9 @@ describe('Testability Analyzer', () => {
|
|
|
38
44
|
|
|
39
45
|
describe('Pure Functions and State Mutations', () => {
|
|
40
46
|
it('should detect state mutations inside functions', async () => {
|
|
41
|
-
createTestFile(
|
|
47
|
+
createTestFile(
|
|
48
|
+
'src/mutations.ts',
|
|
49
|
+
`
|
|
42
50
|
const globalState = { value: 0 };
|
|
43
51
|
|
|
44
52
|
export function impureAdd(a: number) {
|
|
@@ -49,7 +57,8 @@ describe('Testability Analyzer', () => {
|
|
|
49
57
|
export function pureAdd(a: number, b: number) {
|
|
50
58
|
return a + b;
|
|
51
59
|
}
|
|
52
|
-
`
|
|
60
|
+
`
|
|
61
|
+
);
|
|
53
62
|
|
|
54
63
|
const report = await analyzeTestability({ rootDir: tmpDir });
|
|
55
64
|
|
|
@@ -60,7 +69,9 @@ describe('Testability Analyzer', () => {
|
|
|
60
69
|
|
|
61
70
|
describe('Bloated Interfaces', () => {
|
|
62
71
|
it('should detect interfaces with too many methods', async () => {
|
|
63
|
-
createTestFile(
|
|
72
|
+
createTestFile(
|
|
73
|
+
'src/interfaces.ts',
|
|
74
|
+
`
|
|
64
75
|
export interface BloatedService {
|
|
65
76
|
m1(): void;
|
|
66
77
|
m2(): void;
|
|
@@ -74,7 +85,8 @@ describe('Testability Analyzer', () => {
|
|
|
74
85
|
m10(): void;
|
|
75
86
|
m11(): void;
|
|
76
87
|
}
|
|
77
|
-
`
|
|
88
|
+
`
|
|
89
|
+
);
|
|
78
90
|
|
|
79
91
|
const report = await analyzeTestability({ rootDir: tmpDir });
|
|
80
92
|
|