@aiready/testability 0.6.7 → 0.6.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 +24 -23
- package/.turbo/turbo-test.log +20 -18
- package/dist/chunk-JL4S6RHJ.mjs +269 -0
- package/dist/cli.js +8 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.js +8 -1
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/analyzer.ts +15 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
DTS
|
|
20
|
-
DTS
|
|
21
|
-
DTS dist/
|
|
22
|
-
DTS dist/
|
|
23
|
-
DTS dist/
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @aiready/testability@0.6.8 build /Users/pengcao/projects/aiready/packages/testability
|
|
4
|
+
> tsup src/index.ts src/cli.ts --format cjs,esm --dts
|
|
5
|
+
|
|
6
|
+
[34mCLI[39m Building entry: src/cli.ts, src/index.ts
|
|
7
|
+
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
8
|
+
[34mCLI[39m tsup v8.5.1
|
|
9
|
+
[34mCLI[39m Target: es2020
|
|
10
|
+
[34mCJS[39m Build start
|
|
11
|
+
[34mESM[39m Build start
|
|
12
|
+
[32mCJS[39m [1mdist/index.js [22m[32m10.78 KB[39m
|
|
13
|
+
[32mCJS[39m [1mdist/cli.js [22m[32m15.78 KB[39m
|
|
14
|
+
[32mCJS[39m ⚡️ Build success in 62ms
|
|
15
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m1.17 KB[39m
|
|
16
|
+
[32mESM[39m [1mdist/cli.mjs [22m[32m5.75 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/chunk-JL4S6RHJ.mjs [22m[32m8.47 KB[39m
|
|
18
|
+
[32mESM[39m ⚡️ Build success in 63ms
|
|
19
|
+
DTS Build start
|
|
20
|
+
DTS ⚡️ Build success in 4353ms
|
|
21
|
+
DTS dist/cli.d.ts 20.00 B
|
|
22
|
+
DTS dist/index.d.ts 2.62 KB
|
|
23
|
+
DTS dist/cli.d.mts 20.00 B
|
|
24
|
+
DTS dist/index.d.mts 2.62 KB
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[32m✓[39m src/__tests__/
|
|
10
|
-
[32m✓[39m src/__tests__/
|
|
11
|
-
[32m✓[39m src/__tests__/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
[2m
|
|
16
|
-
[2m
|
|
17
|
-
[2m
|
|
18
|
-
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @aiready/testability@0.6.7 test /Users/pengcao/projects/aiready/packages/testability
|
|
4
|
+
> vitest run
|
|
5
|
+
|
|
6
|
+
[?25l
|
|
7
|
+
[1m[46m RUN [49m[22m [36mv4.0.18 [39m[90m/Users/pengcao/projects/aiready/packages/testability[39m
|
|
8
|
+
|
|
9
|
+
[32m✓[39m src/__tests__/types.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 2[2mms[22m[39m
|
|
10
|
+
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
11
|
+
[32m✓[39m src/__tests__/provider.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 3[2mms[22m[39m
|
|
12
|
+
[32m✓[39m src/__tests__/analyzer.test.ts [2m([22m[2m5 tests[22m[2m)[22m[33m 943[2mms[22m[39m
|
|
13
|
+
[33m[2m✓[22m[39m detects test frameworks in multiple languages [33m 916[2mms[22m[39m
|
|
14
|
+
|
|
15
|
+
[2m Test Files [22m [1m[32m4 passed[39m[22m[90m (4)[39m
|
|
16
|
+
[2m Tests [22m [1m[32m14 passed[39m[22m[90m (14)[39m
|
|
17
|
+
[2m Start at [22m 23:50:20
|
|
18
|
+
[2m Duration [22m 1.67s[2m (transform 715ms, setup 0ms, import 1.94s, tests 951ms, environment 0ms)[22m
|
|
19
|
+
|
|
20
|
+
[?25h
|
|
@@ -0,0 +1,269 @@
|
|
|
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 = 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 } from "@aiready/core";
|
|
216
|
+
function calculateTestabilityScore(report) {
|
|
217
|
+
const { summary, rawData, recommendations } = report;
|
|
218
|
+
const factors = [
|
|
219
|
+
{
|
|
220
|
+
name: "Test Coverage",
|
|
221
|
+
impact: Math.round(summary.dimensions.testCoverageRatio - 50),
|
|
222
|
+
description: `${rawData.testFiles} test files / ${rawData.sourceFiles} source files (${Math.round(summary.coverageRatio * 100)}%)`
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: "Function Purity",
|
|
226
|
+
impact: Math.round(summary.dimensions.purityScore - 50),
|
|
227
|
+
description: `${rawData.pureFunctions}/${rawData.totalFunctions} functions are pure`
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: "Dependency Injection",
|
|
231
|
+
impact: Math.round(summary.dimensions.dependencyInjectionScore - 50),
|
|
232
|
+
description: `${rawData.injectionPatterns}/${rawData.totalClasses} classes use DI`
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "Interface Focus",
|
|
236
|
+
impact: Math.round(summary.dimensions.interfaceFocusScore - 50),
|
|
237
|
+
description: `${rawData.bloatedInterfaces} interfaces have >10 methods`
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: "Observability",
|
|
241
|
+
impact: Math.round(summary.dimensions.observabilityScore - 50),
|
|
242
|
+
description: `${rawData.externalStateMutations} functions mutate external state`
|
|
243
|
+
}
|
|
244
|
+
];
|
|
245
|
+
const recs = recommendations.map(
|
|
246
|
+
(action) => ({
|
|
247
|
+
action,
|
|
248
|
+
estimatedImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
|
|
249
|
+
priority: summary.aiChangeSafetyRating === "blind-risk" || summary.aiChangeSafetyRating === "high-risk" ? "high" : "medium"
|
|
250
|
+
})
|
|
251
|
+
);
|
|
252
|
+
return {
|
|
253
|
+
toolName: ToolName.TestabilityIndex,
|
|
254
|
+
score: summary.score,
|
|
255
|
+
rawMetrics: {
|
|
256
|
+
...rawData,
|
|
257
|
+
rating: summary.rating,
|
|
258
|
+
aiChangeSafetyRating: summary.aiChangeSafetyRating,
|
|
259
|
+
coverageRatio: summary.coverageRatio
|
|
260
|
+
},
|
|
261
|
+
factors,
|
|
262
|
+
recommendations: recs
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export {
|
|
267
|
+
analyzeTestability,
|
|
268
|
+
calculateTestabilityScore
|
|
269
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -142,6 +142,7 @@ async function analyzeTestability(options) {
|
|
|
142
142
|
totalInterfaces: 0,
|
|
143
143
|
externalStateMutations: 0
|
|
144
144
|
};
|
|
145
|
+
const fileDetails = [];
|
|
145
146
|
let processed = 0;
|
|
146
147
|
for (const f of sourceFiles) {
|
|
147
148
|
processed++;
|
|
@@ -156,6 +157,11 @@ async function analyzeTestability(options) {
|
|
|
156
157
|
for (const key of Object.keys(aggregated)) {
|
|
157
158
|
aggregated[key] += a[key];
|
|
158
159
|
}
|
|
160
|
+
fileDetails.push({
|
|
161
|
+
filePath: f,
|
|
162
|
+
pureFunctions: a.pureFunctions,
|
|
163
|
+
totalFunctions: a.totalFunctions
|
|
164
|
+
});
|
|
159
165
|
}
|
|
160
166
|
const hasTestFramework = detectTestFramework(options.rootDir);
|
|
161
167
|
const indexResult = (0, import_core.calculateTestabilityIndex)({
|
|
@@ -168,7 +174,8 @@ async function analyzeTestability(options) {
|
|
|
168
174
|
bloatedInterfaces: aggregated.bloatedInterfaces,
|
|
169
175
|
totalInterfaces: Math.max(1, aggregated.totalInterfaces),
|
|
170
176
|
externalStateMutations: aggregated.externalStateMutations,
|
|
171
|
-
hasTestFramework
|
|
177
|
+
hasTestFramework,
|
|
178
|
+
fileDetails
|
|
172
179
|
});
|
|
173
180
|
const issues = [];
|
|
174
181
|
const minCoverage = options.minCoverageRatio ?? 0.3;
|
package/dist/cli.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -146,6 +146,7 @@ async function analyzeTestability(options) {
|
|
|
146
146
|
totalInterfaces: 0,
|
|
147
147
|
externalStateMutations: 0
|
|
148
148
|
};
|
|
149
|
+
const fileDetails = [];
|
|
149
150
|
let processed = 0;
|
|
150
151
|
for (const f of sourceFiles) {
|
|
151
152
|
processed++;
|
|
@@ -160,6 +161,11 @@ async function analyzeTestability(options) {
|
|
|
160
161
|
for (const key of Object.keys(aggregated)) {
|
|
161
162
|
aggregated[key] += a[key];
|
|
162
163
|
}
|
|
164
|
+
fileDetails.push({
|
|
165
|
+
filePath: f,
|
|
166
|
+
pureFunctions: a.pureFunctions,
|
|
167
|
+
totalFunctions: a.totalFunctions
|
|
168
|
+
});
|
|
163
169
|
}
|
|
164
170
|
const hasTestFramework = detectTestFramework(options.rootDir);
|
|
165
171
|
const indexResult = (0, import_core.calculateTestabilityIndex)({
|
|
@@ -172,7 +178,8 @@ async function analyzeTestability(options) {
|
|
|
172
178
|
bloatedInterfaces: aggregated.bloatedInterfaces,
|
|
173
179
|
totalInterfaces: Math.max(1, aggregated.totalInterfaces),
|
|
174
180
|
externalStateMutations: aggregated.externalStateMutations,
|
|
175
|
-
hasTestFramework
|
|
181
|
+
hasTestFramework,
|
|
182
|
+
fileDetails
|
|
176
183
|
});
|
|
177
184
|
const issues = [];
|
|
178
185
|
const minCoverage = options.minCoverageRatio ?? 0.3;
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/testability",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -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.
|
|
43
|
+
"@aiready/core": "0.23.9"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^24.0.0",
|
package/src/analyzer.ts
CHANGED
|
@@ -170,6 +170,13 @@ export async function analyzeTestability(
|
|
|
170
170
|
externalStateMutations: 0,
|
|
171
171
|
};
|
|
172
172
|
|
|
173
|
+
// Collect file-level details for smarter scoring
|
|
174
|
+
const fileDetails: Array<{
|
|
175
|
+
filePath: string;
|
|
176
|
+
pureFunctions: number;
|
|
177
|
+
totalFunctions: number;
|
|
178
|
+
}> = [];
|
|
179
|
+
|
|
173
180
|
let processed = 0;
|
|
174
181
|
for (const f of sourceFiles) {
|
|
175
182
|
processed++;
|
|
@@ -185,6 +192,13 @@ export async function analyzeTestability(
|
|
|
185
192
|
for (const key of Object.keys(aggregated) as Array<keyof FileAnalysis>) {
|
|
186
193
|
aggregated[key] += a[key];
|
|
187
194
|
}
|
|
195
|
+
|
|
196
|
+
// Collect file-level data
|
|
197
|
+
fileDetails.push({
|
|
198
|
+
filePath: f,
|
|
199
|
+
pureFunctions: a.pureFunctions,
|
|
200
|
+
totalFunctions: a.totalFunctions,
|
|
201
|
+
});
|
|
188
202
|
}
|
|
189
203
|
|
|
190
204
|
const hasTestFramework = detectTestFramework(options.rootDir);
|
|
@@ -200,6 +214,7 @@ export async function analyzeTestability(
|
|
|
200
214
|
totalInterfaces: Math.max(1, aggregated.totalInterfaces),
|
|
201
215
|
externalStateMutations: aggregated.externalStateMutations,
|
|
202
216
|
hasTestFramework,
|
|
217
|
+
fileDetails,
|
|
203
218
|
});
|
|
204
219
|
|
|
205
220
|
// Build issues
|