@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/.turbo/turbo-build.log +24 -23
- package/.turbo/turbo-lint.log +5 -10
- package/.turbo/turbo-test.log +20 -18
- package/dist/chunk-QMDUZA7H.mjs +239 -0
- package/dist/chunk-RBPS3OGD.mjs +238 -0
- package/dist/chunk-YT5DTEQ4.mjs +238 -0
- package/dist/cli.js +57 -118
- package/dist/cli.mjs +46 -80
- package/dist/index.d.mts +2 -5
- package/dist/index.d.ts +2 -5
- package/dist/index.js +14 -44
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/__tests__/analyzer.test.ts +15 -26
- package/src/__tests__/scoring.test.ts +1 -1
- package/src/analyzer.ts +1 -1
- package/src/cli.ts +44 -89
- package/src/scoring.ts +16 -56
|
@@ -0,0 +1,238 @@
|
|
|
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 = 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
|
+
let processed = 0;
|
|
126
|
+
for (const f of sourceFiles) {
|
|
127
|
+
processed++;
|
|
128
|
+
emitProgress(
|
|
129
|
+
processed,
|
|
130
|
+
sourceFiles.length,
|
|
131
|
+
"testability",
|
|
132
|
+
"analyzing files",
|
|
133
|
+
options.onProgress
|
|
134
|
+
);
|
|
135
|
+
const a = await analyzeFileTestability(f);
|
|
136
|
+
for (const key of Object.keys(aggregated)) {
|
|
137
|
+
aggregated[key] += a[key];
|
|
138
|
+
}
|
|
139
|
+
fileDetails.push({
|
|
140
|
+
filePath: f,
|
|
141
|
+
pureFunctions: a.pureFunctions,
|
|
142
|
+
totalFunctions: a.totalFunctions
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const hasTestFramework = detectTestFramework(options.rootDir);
|
|
146
|
+
const indexResult = calculateTestabilityIndex({
|
|
147
|
+
testFiles: testFiles.length,
|
|
148
|
+
sourceFiles: sourceFiles.length,
|
|
149
|
+
pureFunctions: aggregated.pureFunctions,
|
|
150
|
+
totalFunctions: Math.max(1, aggregated.totalFunctions),
|
|
151
|
+
injectionPatterns: aggregated.injectionPatterns,
|
|
152
|
+
totalClasses: Math.max(1, aggregated.totalClasses),
|
|
153
|
+
bloatedInterfaces: aggregated.bloatedInterfaces,
|
|
154
|
+
totalInterfaces: Math.max(1, aggregated.totalInterfaces),
|
|
155
|
+
externalStateMutations: aggregated.externalStateMutations,
|
|
156
|
+
hasTestFramework,
|
|
157
|
+
fileDetails
|
|
158
|
+
});
|
|
159
|
+
const issues = [];
|
|
160
|
+
const minCoverage = options.minCoverageRatio ?? 0.3;
|
|
161
|
+
const actualRatio = sourceFiles.length > 0 ? testFiles.length / sourceFiles.length : 0;
|
|
162
|
+
if (!hasTestFramework) {
|
|
163
|
+
issues.push({
|
|
164
|
+
type: IssueType.LowTestability,
|
|
165
|
+
dimension: "framework",
|
|
166
|
+
severity: Severity.Critical,
|
|
167
|
+
message: "No major testing framework detected \u2014 AI changes cannot be safely verified.",
|
|
168
|
+
location: { file: options.rootDir, line: 0 },
|
|
169
|
+
suggestion: "Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification."
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (actualRatio < minCoverage) {
|
|
173
|
+
const needed = Math.ceil(sourceFiles.length * minCoverage) - testFiles.length;
|
|
174
|
+
issues.push({
|
|
175
|
+
type: IssueType.LowTestability,
|
|
176
|
+
dimension: "test-coverage",
|
|
177
|
+
severity: actualRatio === 0 ? Severity.Critical : Severity.Major,
|
|
178
|
+
message: `Test ratio is ${Math.round(actualRatio * 100)}% (${testFiles.length} test files for ${sourceFiles.length} source files). Need at least ${Math.round(minCoverage * 100)}%.`,
|
|
179
|
+
location: { file: options.rootDir, line: 0 },
|
|
180
|
+
suggestion: `Add ~${needed} test file(s) to reach the ${Math.round(minCoverage * 100)}% minimum for safe AI assistance.`
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (indexResult.dimensions.purityScore < 50) {
|
|
184
|
+
issues.push({
|
|
185
|
+
type: IssueType.LowTestability,
|
|
186
|
+
dimension: "purity",
|
|
187
|
+
severity: Severity.Major,
|
|
188
|
+
message: `Only ${indexResult.dimensions.purityScore}% of functions appear pure \u2014 side-effectful code is harder for AI to verify safely.`,
|
|
189
|
+
location: { file: options.rootDir, line: 0 },
|
|
190
|
+
suggestion: "Refactor complex side-effectful logic into pure functions where possible."
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
summary: {
|
|
195
|
+
sourceFiles: sourceFiles.length,
|
|
196
|
+
testFiles: testFiles.length,
|
|
197
|
+
coverageRatio: Math.round(actualRatio * 100) / 100,
|
|
198
|
+
score: indexResult.score,
|
|
199
|
+
rating: indexResult.rating,
|
|
200
|
+
aiChangeSafetyRating: indexResult.aiChangeSafetyRating,
|
|
201
|
+
dimensions: indexResult.dimensions
|
|
202
|
+
},
|
|
203
|
+
issues,
|
|
204
|
+
rawData: {
|
|
205
|
+
sourceFiles: sourceFiles.length,
|
|
206
|
+
testFiles: testFiles.length,
|
|
207
|
+
...aggregated,
|
|
208
|
+
hasTestFramework
|
|
209
|
+
},
|
|
210
|
+
recommendations: indexResult.recommendations
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/scoring.ts
|
|
215
|
+
import { ToolName, buildStandardToolScore } from "@aiready/core";
|
|
216
|
+
function calculateTestabilityScore(report) {
|
|
217
|
+
const { summary, rawData, recommendations } = report;
|
|
218
|
+
return buildStandardToolScore({
|
|
219
|
+
toolName: ToolName.TestabilityIndex,
|
|
220
|
+
score: summary.score,
|
|
221
|
+
rawData,
|
|
222
|
+
dimensions: summary.dimensions,
|
|
223
|
+
dimensionNames: {
|
|
224
|
+
testCoverageRatio: "Test Coverage",
|
|
225
|
+
purityScore: "Function Purity",
|
|
226
|
+
dependencyInjectionScore: "Dependency Injection",
|
|
227
|
+
interfaceFocusScore: "Interface Focus",
|
|
228
|
+
observabilityScore: "Observability"
|
|
229
|
+
},
|
|
230
|
+
recommendations,
|
|
231
|
+
rating: summary.rating
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export {
|
|
236
|
+
analyzeTestability,
|
|
237
|
+
calculateTestabilityScore
|
|
238
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -40,7 +40,7 @@ async function analyzeFileTestability(filePath) {
|
|
|
40
40
|
totalInterfaces: 0,
|
|
41
41
|
externalStateMutations: 0
|
|
42
42
|
};
|
|
43
|
-
const parser = (0, import_core.getParser)(filePath);
|
|
43
|
+
const parser = await (0, import_core.getParser)(filePath);
|
|
44
44
|
if (!parser) return result;
|
|
45
45
|
let code;
|
|
46
46
|
try {
|
|
@@ -236,52 +236,22 @@ async function analyzeTestability(options) {
|
|
|
236
236
|
var import_core2 = require("@aiready/core");
|
|
237
237
|
function calculateTestabilityScore(report) {
|
|
238
238
|
const { summary, rawData, recommendations } = report;
|
|
239
|
-
|
|
240
|
-
{
|
|
241
|
-
name: "Test Coverage",
|
|
242
|
-
impact: Math.round(summary.dimensions.testCoverageRatio - 50),
|
|
243
|
-
description: `${rawData.testFiles} test files / ${rawData.sourceFiles} source files (${Math.round(summary.coverageRatio * 100)}%)`
|
|
244
|
-
},
|
|
245
|
-
{
|
|
246
|
-
name: "Function Purity",
|
|
247
|
-
impact: Math.round(summary.dimensions.purityScore - 50),
|
|
248
|
-
description: `${rawData.pureFunctions}/${rawData.totalFunctions} functions are pure`
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
name: "Dependency Injection",
|
|
252
|
-
impact: Math.round(summary.dimensions.dependencyInjectionScore - 50),
|
|
253
|
-
description: `${rawData.injectionPatterns}/${rawData.totalClasses} classes use DI`
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
name: "Interface Focus",
|
|
257
|
-
impact: Math.round(summary.dimensions.interfaceFocusScore - 50),
|
|
258
|
-
description: `${rawData.bloatedInterfaces} interfaces have >10 methods`
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
name: "Observability",
|
|
262
|
-
impact: Math.round(summary.dimensions.observabilityScore - 50),
|
|
263
|
-
description: `${rawData.externalStateMutations} functions mutate external state`
|
|
264
|
-
}
|
|
265
|
-
];
|
|
266
|
-
const recs = recommendations.map(
|
|
267
|
-
(action) => ({
|
|
268
|
-
action,
|
|
269
|
-
estimatedImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
|
|
270
|
-
priority: summary.aiChangeSafetyRating === "blind-risk" || summary.aiChangeSafetyRating === "high-risk" ? "high" : "medium"
|
|
271
|
-
})
|
|
272
|
-
);
|
|
273
|
-
return {
|
|
239
|
+
return (0, import_core2.buildStandardToolScore)({
|
|
274
240
|
toolName: import_core2.ToolName.TestabilityIndex,
|
|
275
241
|
score: summary.score,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
242
|
+
rawData,
|
|
243
|
+
dimensions: summary.dimensions,
|
|
244
|
+
dimensionNames: {
|
|
245
|
+
testCoverageRatio: "Test Coverage",
|
|
246
|
+
purityScore: "Function Purity",
|
|
247
|
+
dependencyInjectionScore: "Dependency Injection",
|
|
248
|
+
interfaceFocusScore: "Interface Focus",
|
|
249
|
+
observabilityScore: "Observability"
|
|
281
250
|
},
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
251
|
+
recommendations,
|
|
252
|
+
recommendationImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
|
|
253
|
+
rating: summary.aiChangeSafetyRating || summary.rating
|
|
254
|
+
});
|
|
285
255
|
}
|
|
286
256
|
|
|
287
257
|
// src/cli.ts
|
|
@@ -290,6 +260,7 @@ var import_fs2 = require("fs");
|
|
|
290
260
|
var import_path2 = require("path");
|
|
291
261
|
var import_core3 = require("@aiready/core");
|
|
292
262
|
var program = new import_commander.Command();
|
|
263
|
+
var startTime = Date.now();
|
|
293
264
|
program.name("aiready-testability").description(
|
|
294
265
|
"Measure how safely AI-generated changes can be verified in your codebase"
|
|
295
266
|
).version("0.1.0").addHelpText(
|
|
@@ -322,17 +293,13 @@ EXAMPLES:
|
|
|
322
293
|
"Additional test file patterns (comma-separated)"
|
|
323
294
|
).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) => {
|
|
324
295
|
console.log(import_chalk.default.blue("\u{1F9EA} Analyzing testability...\n"));
|
|
325
|
-
const startTime = Date.now();
|
|
326
|
-
const config = await (0, import_core3.loadConfig)(directory);
|
|
327
|
-
const mergedConfig = (0, import_core3.mergeConfigWithDefaults)(config, {
|
|
328
|
-
minCoverageRatio: 0.3
|
|
329
|
-
});
|
|
330
296
|
const finalOptions = {
|
|
331
297
|
rootDir: directory,
|
|
332
|
-
minCoverageRatio: parseFloat(options.minCoverage ?? "0.3")
|
|
298
|
+
minCoverageRatio: parseFloat(options.minCoverage ?? "0.3"),
|
|
333
299
|
testPatterns: options.testPatterns?.split(","),
|
|
334
300
|
include: options.include?.split(","),
|
|
335
|
-
exclude: options.exclude?.split(",")
|
|
301
|
+
exclude: options.exclude?.split(","),
|
|
302
|
+
onProgress: (0, import_core3.createStandardProgressCallback)("testability")
|
|
336
303
|
};
|
|
337
304
|
const report = await analyzeTestability(finalOptions);
|
|
338
305
|
const scoring = calculateTestabilityScore(report);
|
|
@@ -349,73 +316,45 @@ EXAMPLES:
|
|
|
349
316
|
(0, import_fs2.writeFileSync)(outputPath, JSON.stringify(payload, null, 2));
|
|
350
317
|
console.log(import_chalk.default.green(`\u2713 Report saved to ${outputPath}`));
|
|
351
318
|
} else {
|
|
352
|
-
|
|
319
|
+
(0, import_core3.displayStandardConsoleReport)({
|
|
320
|
+
title: "\u{1F9EA} Testability Analysis",
|
|
321
|
+
score: scoring.summary.score,
|
|
322
|
+
rating: scoring.summary.rating,
|
|
323
|
+
dimensions: [
|
|
324
|
+
{
|
|
325
|
+
name: "Test Coverage",
|
|
326
|
+
value: scoring.summary.dimensions.testCoverageRatio
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "Function Purity",
|
|
330
|
+
value: scoring.summary.dimensions.purityScore
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
name: "Dependency Injection",
|
|
334
|
+
value: scoring.summary.dimensions.dependencyInjectionScore
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: "Interface Focus",
|
|
338
|
+
value: scoring.summary.dimensions.interfaceFocusScore
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "Observability",
|
|
342
|
+
value: scoring.summary.dimensions.observabilityScore
|
|
343
|
+
}
|
|
344
|
+
],
|
|
345
|
+
stats: [
|
|
346
|
+
{ label: "Source Files", value: report.rawData.sourceFiles },
|
|
347
|
+
{ label: "Test Files", value: report.rawData.testFiles },
|
|
348
|
+
{
|
|
349
|
+
label: "Coverage Ratio",
|
|
350
|
+
value: Math.round(scoring.summary.coverageRatio * 100) + "%"
|
|
351
|
+
}
|
|
352
|
+
],
|
|
353
|
+
issues: report.issues,
|
|
354
|
+
recommendations: report.recommendations,
|
|
355
|
+
elapsedTime: elapsed,
|
|
356
|
+
safetyRating: report.summary.aiChangeSafetyRating
|
|
357
|
+
});
|
|
353
358
|
}
|
|
354
359
|
});
|
|
355
360
|
program.parse();
|
|
356
|
-
function displayConsoleReport(report, scoring, elapsed) {
|
|
357
|
-
const { summary, rawData, issues, recommendations } = report;
|
|
358
|
-
const safetyRating = summary.aiChangeSafetyRating;
|
|
359
|
-
console.log(import_chalk.default.bold("\n\u{1F9EA} Testability Analysis\n"));
|
|
360
|
-
if (safetyRating === "blind-risk") {
|
|
361
|
-
console.log(
|
|
362
|
-
import_chalk.default.bgRed.white.bold(
|
|
363
|
-
" \u{1F480} BLIND RISK \u2014 NO TESTS DETECTED. AI-GENERATED CHANGES CANNOT BE VERIFIED. "
|
|
364
|
-
)
|
|
365
|
-
);
|
|
366
|
-
console.log();
|
|
367
|
-
} else if (safetyRating === "high-risk") {
|
|
368
|
-
console.log(
|
|
369
|
-
import_chalk.default.red.bold(
|
|
370
|
-
` \u{1F534} HIGH RISK \u2014 Insufficient test coverage. AI changes may introduce silent bugs.`
|
|
371
|
-
)
|
|
372
|
-
);
|
|
373
|
-
console.log();
|
|
374
|
-
}
|
|
375
|
-
const safetyColor = (0, import_core3.getSeverityColor)(safetyRating, import_chalk.default);
|
|
376
|
-
console.log(
|
|
377
|
-
`AI Change Safety: ${safetyColor(`${(0, import_core3.getSafetyIcon)(safetyRating)} ${safetyRating.toUpperCase()}`)}`
|
|
378
|
-
);
|
|
379
|
-
console.log(
|
|
380
|
-
`Score: ${import_chalk.default.bold(summary.score + "/100")} (${summary.rating})`
|
|
381
|
-
);
|
|
382
|
-
console.log(
|
|
383
|
-
`Source Files: ${import_chalk.default.cyan(rawData.sourceFiles)} Test Files: ${import_chalk.default.cyan(rawData.testFiles)}`
|
|
384
|
-
);
|
|
385
|
-
console.log(
|
|
386
|
-
`Coverage Ratio: ${import_chalk.default.bold(Math.round(summary.coverageRatio * 100) + "%")}`
|
|
387
|
-
);
|
|
388
|
-
console.log(`Analysis Time: ${import_chalk.default.gray(elapsed + "s")}
|
|
389
|
-
`);
|
|
390
|
-
console.log(import_chalk.default.bold("\u{1F4D0} Dimension Scores\n"));
|
|
391
|
-
const dims = [
|
|
392
|
-
["Test Coverage", summary.dimensions.testCoverageRatio],
|
|
393
|
-
["Function Purity", summary.dimensions.purityScore],
|
|
394
|
-
["Dependency Injection", summary.dimensions.dependencyInjectionScore],
|
|
395
|
-
["Interface Focus", summary.dimensions.interfaceFocusScore],
|
|
396
|
-
["Observability", summary.dimensions.observabilityScore]
|
|
397
|
-
];
|
|
398
|
-
for (const [name, val] of dims) {
|
|
399
|
-
const color = val >= 70 ? import_chalk.default.green : val >= 50 ? import_chalk.default.yellow : import_chalk.default.red;
|
|
400
|
-
console.log(` ${name.padEnd(22)} ${color((0, import_core3.getScoreBar)(val))} ${val}/100`);
|
|
401
|
-
}
|
|
402
|
-
if (issues.length > 0) {
|
|
403
|
-
console.log(import_chalk.default.bold("\n\u26A0\uFE0F Issues\n"));
|
|
404
|
-
for (const issue of issues) {
|
|
405
|
-
const sev = (0, import_core3.getSeverityColor)(issue.severity, import_chalk.default);
|
|
406
|
-
console.log(`${sev(issue.severity.toUpperCase())} ${issue.message}`);
|
|
407
|
-
if (issue.suggestion)
|
|
408
|
-
console.log(
|
|
409
|
-
` ${import_chalk.default.dim("\u2192")} ${import_chalk.default.italic(issue.suggestion)}`
|
|
410
|
-
);
|
|
411
|
-
console.log();
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
if (recommendations.length > 0) {
|
|
415
|
-
console.log(import_chalk.default.bold("\u{1F4A1} Recommendations\n"));
|
|
416
|
-
recommendations.forEach((rec, i) => {
|
|
417
|
-
console.log(`${i + 1}. ${rec}`);
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
console.log();
|
|
421
|
-
}
|
package/dist/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
analyzeTestability,
|
|
4
4
|
calculateTestabilityScore
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-QMDUZA7H.mjs";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
8
|
import { Command } from "commander";
|
|
@@ -10,14 +10,12 @@ import chalk from "chalk";
|
|
|
10
10
|
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
11
11
|
import { dirname } from "path";
|
|
12
12
|
import {
|
|
13
|
-
loadConfig,
|
|
14
|
-
mergeConfigWithDefaults,
|
|
15
13
|
resolveOutputPath,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
getSeverityColor
|
|
14
|
+
displayStandardConsoleReport,
|
|
15
|
+
createStandardProgressCallback
|
|
19
16
|
} from "@aiready/core";
|
|
20
17
|
var program = new Command();
|
|
18
|
+
var startTime = Date.now();
|
|
21
19
|
program.name("aiready-testability").description(
|
|
22
20
|
"Measure how safely AI-generated changes can be verified in your codebase"
|
|
23
21
|
).version("0.1.0").addHelpText(
|
|
@@ -50,17 +48,13 @@ EXAMPLES:
|
|
|
50
48
|
"Additional test file patterns (comma-separated)"
|
|
51
49
|
).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) => {
|
|
52
50
|
console.log(chalk.blue("\u{1F9EA} Analyzing testability...\n"));
|
|
53
|
-
const startTime = Date.now();
|
|
54
|
-
const config = await loadConfig(directory);
|
|
55
|
-
const mergedConfig = mergeConfigWithDefaults(config, {
|
|
56
|
-
minCoverageRatio: 0.3
|
|
57
|
-
});
|
|
58
51
|
const finalOptions = {
|
|
59
52
|
rootDir: directory,
|
|
60
|
-
minCoverageRatio: parseFloat(options.minCoverage ?? "0.3")
|
|
53
|
+
minCoverageRatio: parseFloat(options.minCoverage ?? "0.3"),
|
|
61
54
|
testPatterns: options.testPatterns?.split(","),
|
|
62
55
|
include: options.include?.split(","),
|
|
63
|
-
exclude: options.exclude?.split(",")
|
|
56
|
+
exclude: options.exclude?.split(","),
|
|
57
|
+
onProgress: createStandardProgressCallback("testability")
|
|
64
58
|
};
|
|
65
59
|
const report = await analyzeTestability(finalOptions);
|
|
66
60
|
const scoring = calculateTestabilityScore(report);
|
|
@@ -77,73 +71,45 @@ EXAMPLES:
|
|
|
77
71
|
writeFileSync(outputPath, JSON.stringify(payload, null, 2));
|
|
78
72
|
console.log(chalk.green(`\u2713 Report saved to ${outputPath}`));
|
|
79
73
|
} else {
|
|
80
|
-
|
|
74
|
+
displayStandardConsoleReport({
|
|
75
|
+
title: "\u{1F9EA} Testability Analysis",
|
|
76
|
+
score: scoring.summary.score,
|
|
77
|
+
rating: scoring.summary.rating,
|
|
78
|
+
dimensions: [
|
|
79
|
+
{
|
|
80
|
+
name: "Test Coverage",
|
|
81
|
+
value: scoring.summary.dimensions.testCoverageRatio
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "Function Purity",
|
|
85
|
+
value: scoring.summary.dimensions.purityScore
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "Dependency Injection",
|
|
89
|
+
value: scoring.summary.dimensions.dependencyInjectionScore
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "Interface Focus",
|
|
93
|
+
value: scoring.summary.dimensions.interfaceFocusScore
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "Observability",
|
|
97
|
+
value: scoring.summary.dimensions.observabilityScore
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
stats: [
|
|
101
|
+
{ label: "Source Files", value: report.rawData.sourceFiles },
|
|
102
|
+
{ label: "Test Files", value: report.rawData.testFiles },
|
|
103
|
+
{
|
|
104
|
+
label: "Coverage Ratio",
|
|
105
|
+
value: Math.round(scoring.summary.coverageRatio * 100) + "%"
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
issues: report.issues,
|
|
109
|
+
recommendations: report.recommendations,
|
|
110
|
+
elapsedTime: elapsed,
|
|
111
|
+
safetyRating: report.summary.aiChangeSafetyRating
|
|
112
|
+
});
|
|
81
113
|
}
|
|
82
114
|
});
|
|
83
115
|
program.parse();
|
|
84
|
-
function displayConsoleReport(report, scoring, elapsed) {
|
|
85
|
-
const { summary, rawData, issues, recommendations } = report;
|
|
86
|
-
const safetyRating = summary.aiChangeSafetyRating;
|
|
87
|
-
console.log(chalk.bold("\n\u{1F9EA} Testability Analysis\n"));
|
|
88
|
-
if (safetyRating === "blind-risk") {
|
|
89
|
-
console.log(
|
|
90
|
-
chalk.bgRed.white.bold(
|
|
91
|
-
" \u{1F480} BLIND RISK \u2014 NO TESTS DETECTED. AI-GENERATED CHANGES CANNOT BE VERIFIED. "
|
|
92
|
-
)
|
|
93
|
-
);
|
|
94
|
-
console.log();
|
|
95
|
-
} else if (safetyRating === "high-risk") {
|
|
96
|
-
console.log(
|
|
97
|
-
chalk.red.bold(
|
|
98
|
-
` \u{1F534} HIGH RISK \u2014 Insufficient test coverage. AI changes may introduce silent bugs.`
|
|
99
|
-
)
|
|
100
|
-
);
|
|
101
|
-
console.log();
|
|
102
|
-
}
|
|
103
|
-
const safetyColor = getSeverityColor(safetyRating, chalk);
|
|
104
|
-
console.log(
|
|
105
|
-
`AI Change Safety: ${safetyColor(`${getSafetyIcon(safetyRating)} ${safetyRating.toUpperCase()}`)}`
|
|
106
|
-
);
|
|
107
|
-
console.log(
|
|
108
|
-
`Score: ${chalk.bold(summary.score + "/100")} (${summary.rating})`
|
|
109
|
-
);
|
|
110
|
-
console.log(
|
|
111
|
-
`Source Files: ${chalk.cyan(rawData.sourceFiles)} Test Files: ${chalk.cyan(rawData.testFiles)}`
|
|
112
|
-
);
|
|
113
|
-
console.log(
|
|
114
|
-
`Coverage Ratio: ${chalk.bold(Math.round(summary.coverageRatio * 100) + "%")}`
|
|
115
|
-
);
|
|
116
|
-
console.log(`Analysis Time: ${chalk.gray(elapsed + "s")}
|
|
117
|
-
`);
|
|
118
|
-
console.log(chalk.bold("\u{1F4D0} Dimension Scores\n"));
|
|
119
|
-
const dims = [
|
|
120
|
-
["Test Coverage", summary.dimensions.testCoverageRatio],
|
|
121
|
-
["Function Purity", summary.dimensions.purityScore],
|
|
122
|
-
["Dependency Injection", summary.dimensions.dependencyInjectionScore],
|
|
123
|
-
["Interface Focus", summary.dimensions.interfaceFocusScore],
|
|
124
|
-
["Observability", summary.dimensions.observabilityScore]
|
|
125
|
-
];
|
|
126
|
-
for (const [name, val] of dims) {
|
|
127
|
-
const color = val >= 70 ? chalk.green : val >= 50 ? chalk.yellow : chalk.red;
|
|
128
|
-
console.log(` ${name.padEnd(22)} ${color(getScoreBar(val))} ${val}/100`);
|
|
129
|
-
}
|
|
130
|
-
if (issues.length > 0) {
|
|
131
|
-
console.log(chalk.bold("\n\u26A0\uFE0F Issues\n"));
|
|
132
|
-
for (const issue of issues) {
|
|
133
|
-
const sev = getSeverityColor(issue.severity, chalk);
|
|
134
|
-
console.log(`${sev(issue.severity.toUpperCase())} ${issue.message}`);
|
|
135
|
-
if (issue.suggestion)
|
|
136
|
-
console.log(
|
|
137
|
-
` ${chalk.dim("\u2192")} ${chalk.italic(issue.suggestion)}`
|
|
138
|
-
);
|
|
139
|
-
console.log();
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (recommendations.length > 0) {
|
|
143
|
-
console.log(chalk.bold("\u{1F4A1} Recommendations\n"));
|
|
144
|
-
recommendations.forEach((rec, i) => {
|
|
145
|
-
console.log(`${i + 1}. ${rec}`);
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
console.log();
|
|
149
|
-
}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _aiready_core from '@aiready/core';
|
|
2
|
-
import { Issue, IssueType
|
|
2
|
+
import { Issue, IssueType } from '@aiready/core';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Testability Tool Provider
|
|
@@ -64,10 +64,7 @@ declare function analyzeTestability(options: TestabilityOptions): Promise<Testab
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Convert testability report into a ToolScoringOutput for the unified score.
|
|
67
|
-
*
|
|
68
|
-
* @param report - The comprehensive testability report containing raw metrics and summary.
|
|
69
|
-
* @returns Standardized scoring output with impact factors and recommendations.
|
|
70
67
|
*/
|
|
71
|
-
declare function calculateTestabilityScore(report: TestabilityReport):
|
|
68
|
+
declare function calculateTestabilityScore(report: TestabilityReport): any;
|
|
72
69
|
|
|
73
70
|
export { type TestabilityIssue, type TestabilityOptions, TestabilityProvider, type TestabilityReport, analyzeTestability, calculateTestabilityScore };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _aiready_core from '@aiready/core';
|
|
2
|
-
import { Issue, IssueType
|
|
2
|
+
import { Issue, IssueType } from '@aiready/core';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Testability Tool Provider
|
|
@@ -64,10 +64,7 @@ declare function analyzeTestability(options: TestabilityOptions): Promise<Testab
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Convert testability report into a ToolScoringOutput for the unified score.
|
|
67
|
-
*
|
|
68
|
-
* @param report - The comprehensive testability report containing raw metrics and summary.
|
|
69
|
-
* @returns Standardized scoring output with impact factors and recommendations.
|
|
70
67
|
*/
|
|
71
|
-
declare function calculateTestabilityScore(report: TestabilityReport):
|
|
68
|
+
declare function calculateTestabilityScore(report: TestabilityReport): any;
|
|
72
69
|
|
|
73
70
|
export { type TestabilityIssue, type TestabilityOptions, TestabilityProvider, type TestabilityReport, analyzeTestability, calculateTestabilityScore };
|