@aiready/testability 0.7.6 → 0.7.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-format-check.log +7 -0
- package/.turbo/turbo-lint.log +5 -4
- package/.turbo/turbo-test.log +20 -18
- package/.turbo/turbo-type-check.log +5 -0
- package/dist/chunk-YSXDYH7O.mjs +243 -0
- package/dist/cli.js +48 -4
- package/dist/cli.mjs +1 -1
- package/dist/index.js +48 -4
- package/dist/index.mjs +1 -1
- package/package.json +5 -3
- package/src/analyzer.ts +59 -4
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.7.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
|
+
[32mESM[39m [1mdist/chunk-YSXDYH7O.mjs [22m[32m8.48 KB[39m
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m1.17 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/cli.mjs [22m[32m4.33 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 23ms
|
|
16
|
+
[32mCJS[39m [1mdist/cli.js [22m[32m14.11 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js [22m[32m10.85 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 23ms
|
|
19
|
+
DTS Build start
|
|
20
|
+
DTS ⚡️ Build success in 2347ms
|
|
21
|
+
DTS dist/cli.d.ts 20.00 B
|
|
22
|
+
DTS dist/index.d.ts 2.42 KB
|
|
23
|
+
DTS dist/cli.d.mts 20.00 B
|
|
24
|
+
DTS dist/index.d.mts 2.42 KB
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @aiready/testability@0.7.8 format-check /Users/pengcao/projects/aiready/packages/testability
|
|
4
|
+
> prettier --check . --ignore-path ../../.prettierignore
|
|
5
|
+
|
|
6
|
+
Checking formatting...
|
|
7
|
+
package.json[2K[1GREADME.md[2K[1Gsrc/__tests__/analyzer.test.ts[2K[1Gsrc/__tests__/provider.test.ts[2K[1Gsrc/__tests__/scoring.test.ts[2K[1Gsrc/__tests__/types.test.ts[2K[1Gsrc/analyzer.ts[2K[1Gsrc/cli.ts[2K[1Gsrc/index.ts[2K[1Gsrc/provider.ts[2K[1Gsrc/scoring.ts[2K[1Gsrc/types.ts[2K[1Gtsconfig.json[2K[1GAll matched files use Prettier code style!
|
package/.turbo/turbo-lint.log
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
>
|
|
4
|
-
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @aiready/testability@0.7.8 lint /Users/pengcao/projects/aiready/packages/testability
|
|
4
|
+
> eslint src
|
|
5
|
+
|
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__/provider.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m
|
|
11
|
-
[32m✓[39m src/__tests__/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
[2m
|
|
16
|
-
[2m
|
|
17
|
-
[2m
|
|
18
|
-
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
> @aiready/testability@0.7.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__/provider.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
11
|
+
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m6 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 2017[2mms[22m[39m
|
|
13
|
+
[33m[2m✓[22m[39m detects test frameworks in multiple languages [33m 1988[2mms[22m[39m
|
|
14
|
+
|
|
15
|
+
[2m Test Files [22m [1m[32m4 passed[39m[22m[90m (4)[39m
|
|
16
|
+
[2m Tests [22m [1m[32m17 passed[39m[22m[90m (17)[39m
|
|
17
|
+
[2m Start at [22m 00:08:36
|
|
18
|
+
[2m Duration [22m 3.74s[2m (transform 1.07s, setup 0ms, import 4.57s, tests 2.03s, environment 0ms)[22m
|
|
19
|
+
|
|
20
|
+
[?25h
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// src/analyzer.ts
|
|
2
|
+
import {
|
|
3
|
+
scanFiles,
|
|
4
|
+
calculateTestabilityIndex,
|
|
5
|
+
Severity,
|
|
6
|
+
IssueType,
|
|
7
|
+
runBatchAnalysis,
|
|
8
|
+
getParser,
|
|
9
|
+
isTestFile,
|
|
10
|
+
isIgnorableSourceFile,
|
|
11
|
+
detectTestFramework
|
|
12
|
+
} from "@aiready/core";
|
|
13
|
+
import { readFileSync } from "fs";
|
|
14
|
+
async function analyzeFileTestability(filePath) {
|
|
15
|
+
const result = {
|
|
16
|
+
pureFunctions: 0,
|
|
17
|
+
totalFunctions: 0,
|
|
18
|
+
injectionPatterns: 0,
|
|
19
|
+
totalClasses: 0,
|
|
20
|
+
bloatedInterfaces: 0,
|
|
21
|
+
totalInterfaces: 0,
|
|
22
|
+
externalStateMutations: 0
|
|
23
|
+
};
|
|
24
|
+
const parser = await getParser(filePath);
|
|
25
|
+
if (!parser) return result;
|
|
26
|
+
let code;
|
|
27
|
+
try {
|
|
28
|
+
code = readFileSync(filePath, "utf-8");
|
|
29
|
+
} catch {
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
await parser.initialize();
|
|
34
|
+
const parseResult = parser.parse(code, filePath);
|
|
35
|
+
for (const exp of parseResult.exports) {
|
|
36
|
+
if (exp.type === "function") {
|
|
37
|
+
result.totalFunctions++;
|
|
38
|
+
if (exp.isPure) result.pureFunctions++;
|
|
39
|
+
if (exp.hasSideEffects) result.externalStateMutations++;
|
|
40
|
+
}
|
|
41
|
+
if (exp.type === "class") {
|
|
42
|
+
result.totalClasses++;
|
|
43
|
+
if (exp.parameters && exp.parameters.length > 0) {
|
|
44
|
+
result.injectionPatterns++;
|
|
45
|
+
}
|
|
46
|
+
const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
|
|
47
|
+
if (total > 10) {
|
|
48
|
+
result.bloatedInterfaces++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (exp.type === "interface") {
|
|
52
|
+
result.totalInterfaces++;
|
|
53
|
+
const total = (exp.methodCount || 0) + (exp.propertyCount || 0);
|
|
54
|
+
if (total > 10) {
|
|
55
|
+
result.bloatedInterfaces++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.warn(`Testability: Failed to parse ${filePath}: ${error}`);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
async function analyzeTestability(options) {
|
|
65
|
+
const allFiles = await scanFiles({
|
|
66
|
+
...options,
|
|
67
|
+
include: options.include || ["**/*.{ts,tsx,js,jsx,py,java,cs,go}"],
|
|
68
|
+
includeTests: true
|
|
69
|
+
});
|
|
70
|
+
const sourceFiles = allFiles.filter(
|
|
71
|
+
(f) => !isTestFile(f, options.testPatterns) && !isIgnorableSourceFile(f)
|
|
72
|
+
);
|
|
73
|
+
const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
|
|
74
|
+
const aggregated = {
|
|
75
|
+
pureFunctions: 0,
|
|
76
|
+
totalFunctions: 0,
|
|
77
|
+
injectionPatterns: 0,
|
|
78
|
+
totalClasses: 0,
|
|
79
|
+
bloatedInterfaces: 0,
|
|
80
|
+
totalInterfaces: 0,
|
|
81
|
+
externalStateMutations: 0
|
|
82
|
+
};
|
|
83
|
+
const fileDetails = [];
|
|
84
|
+
await runBatchAnalysis(
|
|
85
|
+
sourceFiles,
|
|
86
|
+
"analyzing files",
|
|
87
|
+
"testability",
|
|
88
|
+
options.onProgress,
|
|
89
|
+
async (f) => ({
|
|
90
|
+
filePath: f,
|
|
91
|
+
analysis: await analyzeFileTestability(f)
|
|
92
|
+
}),
|
|
93
|
+
(result) => {
|
|
94
|
+
const a = result.analysis;
|
|
95
|
+
for (const key of Object.keys(aggregated)) {
|
|
96
|
+
aggregated[key] += a[key];
|
|
97
|
+
}
|
|
98
|
+
fileDetails.push({
|
|
99
|
+
filePath: result.filePath,
|
|
100
|
+
pureFunctions: a.pureFunctions,
|
|
101
|
+
totalFunctions: a.totalFunctions
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
const hasTestFramework = detectTestFramework(options.rootDir);
|
|
106
|
+
const indexResult = calculateTestabilityIndex({
|
|
107
|
+
testFiles: testFiles.length,
|
|
108
|
+
sourceFiles: sourceFiles.length,
|
|
109
|
+
pureFunctions: aggregated.pureFunctions,
|
|
110
|
+
totalFunctions: Math.max(1, aggregated.totalFunctions),
|
|
111
|
+
injectionPatterns: aggregated.injectionPatterns,
|
|
112
|
+
totalClasses: Math.max(1, aggregated.totalClasses),
|
|
113
|
+
bloatedInterfaces: aggregated.bloatedInterfaces,
|
|
114
|
+
totalInterfaces: Math.max(1, aggregated.totalInterfaces),
|
|
115
|
+
externalStateMutations: aggregated.externalStateMutations,
|
|
116
|
+
hasTestFramework,
|
|
117
|
+
fileDetails
|
|
118
|
+
});
|
|
119
|
+
const issues = [];
|
|
120
|
+
const minCoverage = options.minCoverageRatio ?? 0.3;
|
|
121
|
+
const actualRatio = sourceFiles.length > 0 ? testFiles.length / sourceFiles.length : 0;
|
|
122
|
+
if (!hasTestFramework) {
|
|
123
|
+
issues.push({
|
|
124
|
+
type: IssueType.LowTestability,
|
|
125
|
+
dimension: "framework",
|
|
126
|
+
severity: Severity.Critical,
|
|
127
|
+
message: "No major testing framework detected \u2014 AI changes cannot be safely verified.",
|
|
128
|
+
location: { file: options.rootDir, line: 0 },
|
|
129
|
+
suggestion: "Add a testing framework (e.g., Jest, Pytest, JUnit) to enable automated verification."
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (actualRatio < minCoverage) {
|
|
133
|
+
const needed = Math.ceil(sourceFiles.length * minCoverage) - testFiles.length;
|
|
134
|
+
issues.push({
|
|
135
|
+
type: IssueType.LowTestability,
|
|
136
|
+
dimension: "test-coverage",
|
|
137
|
+
severity: actualRatio === 0 ? Severity.Critical : Severity.Major,
|
|
138
|
+
message: `Test ratio is ${Math.round(actualRatio * 100)}% (${testFiles.length} test files for ${sourceFiles.length} source files). Need at least ${Math.round(minCoverage * 100)}%.`,
|
|
139
|
+
location: { file: options.rootDir, line: 0 },
|
|
140
|
+
suggestion: `Add ~${needed} test file(s) to reach the ${Math.round(minCoverage * 100)}% minimum for safe AI assistance.`
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (indexResult.dimensions.purityScore < 70) {
|
|
144
|
+
const worstPurityFiles = (indexResult.fileMetrics || []).filter((m) => !m.isEntryPoint && m.purityScore < 50).sort((a, b) => a.purityScore - b.purityScore).slice(0, 5);
|
|
145
|
+
if (worstPurityFiles.length > 0) {
|
|
146
|
+
worstPurityFiles.forEach((file) => {
|
|
147
|
+
issues.push({
|
|
148
|
+
type: IssueType.LowTestability,
|
|
149
|
+
dimension: "purity",
|
|
150
|
+
severity: Severity.Major,
|
|
151
|
+
message: `File has only ${file.purityScore}% pure functions \u2014 logic is hard for AI to verify safely.`,
|
|
152
|
+
location: { file: file.filePath, line: 1 },
|
|
153
|
+
suggestion: "Extract side-effectful logic into pure, testable functions."
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
issues.push({
|
|
158
|
+
type: IssueType.LowTestability,
|
|
159
|
+
dimension: "purity",
|
|
160
|
+
severity: Severity.Major,
|
|
161
|
+
message: `Only ${indexResult.dimensions.purityScore}% of project functions are pure \u2014 reduces AI verification safety.`,
|
|
162
|
+
location: { file: options.rootDir, line: 0 },
|
|
163
|
+
suggestion: "Promote pure function patterns to improve codebase testability."
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (indexResult.dimensions.dependencyInjectionScore < 60) {
|
|
168
|
+
issues.push({
|
|
169
|
+
type: IssueType.LowTestability,
|
|
170
|
+
dimension: "dependency-injection",
|
|
171
|
+
severity: Severity.Major,
|
|
172
|
+
message: `Only ${indexResult.dimensions.dependencyInjectionScore}% of classes use dependency injection \u2014 hard to mock for AI verification.`,
|
|
173
|
+
location: { file: options.rootDir, line: 0 },
|
|
174
|
+
suggestion: "Use constructor-based dependency injection to make components mockable."
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (aggregated.bloatedInterfaces > 0) {
|
|
178
|
+
issues.push({
|
|
179
|
+
type: IssueType.LowTestability,
|
|
180
|
+
dimension: "interface-focus",
|
|
181
|
+
severity: Severity.Minor,
|
|
182
|
+
message: `Found ${aggregated.bloatedInterfaces} bloated interfaces/classes \u2014 large interfaces are harder for AI to implement correctly.`,
|
|
183
|
+
location: { file: options.rootDir, line: 0 },
|
|
184
|
+
suggestion: "Split large interfaces into smaller, focused ones (Interface Segregation Principle)."
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (indexResult.dimensions.observabilityScore < 60) {
|
|
188
|
+
issues.push({
|
|
189
|
+
type: IssueType.LowTestability,
|
|
190
|
+
dimension: "observability",
|
|
191
|
+
severity: Severity.Major,
|
|
192
|
+
message: `High rate of external state mutations detected (${indexResult.dimensions.observabilityScore}/100 observability) \u2014 hard for AI to reason about state changes.`,
|
|
193
|
+
location: { file: options.rootDir, line: 0 },
|
|
194
|
+
suggestion: "Prefer immutable data patterns and return values instead of mutating external state."
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
summary: {
|
|
199
|
+
sourceFiles: sourceFiles.length,
|
|
200
|
+
testFiles: testFiles.length,
|
|
201
|
+
coverageRatio: Math.round(actualRatio * 100) / 100,
|
|
202
|
+
score: indexResult.score,
|
|
203
|
+
rating: indexResult.rating,
|
|
204
|
+
aiChangeSafetyRating: indexResult.aiChangeSafetyRating,
|
|
205
|
+
dimensions: indexResult.dimensions
|
|
206
|
+
},
|
|
207
|
+
issues,
|
|
208
|
+
rawData: {
|
|
209
|
+
sourceFiles: sourceFiles.length,
|
|
210
|
+
testFiles: testFiles.length,
|
|
211
|
+
...aggregated,
|
|
212
|
+
hasTestFramework
|
|
213
|
+
},
|
|
214
|
+
recommendations: indexResult.recommendations
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/scoring.ts
|
|
219
|
+
import { ToolName, buildStandardToolScore } from "@aiready/core";
|
|
220
|
+
function calculateTestabilityScore(report) {
|
|
221
|
+
const { summary, rawData, recommendations } = report;
|
|
222
|
+
return buildStandardToolScore({
|
|
223
|
+
toolName: ToolName.TestabilityIndex,
|
|
224
|
+
score: summary.score,
|
|
225
|
+
rawData,
|
|
226
|
+
dimensions: summary.dimensions,
|
|
227
|
+
dimensionNames: {
|
|
228
|
+
testCoverageRatio: "Test Coverage",
|
|
229
|
+
purityScore: "Function Purity",
|
|
230
|
+
dependencyInjectionScore: "Dependency Injection",
|
|
231
|
+
interfaceFocusScore: "Interface Focus",
|
|
232
|
+
observabilityScore: "Observability"
|
|
233
|
+
},
|
|
234
|
+
recommendations,
|
|
235
|
+
recommendationImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
|
|
236
|
+
rating: summary.aiChangeSafetyRating || summary.rating
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export {
|
|
241
|
+
analyzeTestability,
|
|
242
|
+
calculateTestabilityScore
|
|
243
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -158,14 +158,58 @@ async function analyzeTestability(options) {
|
|
|
158
158
|
suggestion: `Add ~${needed} test file(s) to reach the ${Math.round(minCoverage * 100)}% minimum for safe AI assistance.`
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
|
-
if (indexResult.dimensions.purityScore <
|
|
161
|
+
if (indexResult.dimensions.purityScore < 70) {
|
|
162
|
+
const worstPurityFiles = (indexResult.fileMetrics || []).filter((m) => !m.isEntryPoint && m.purityScore < 50).sort((a, b) => a.purityScore - b.purityScore).slice(0, 5);
|
|
163
|
+
if (worstPurityFiles.length > 0) {
|
|
164
|
+
worstPurityFiles.forEach((file) => {
|
|
165
|
+
issues.push({
|
|
166
|
+
type: import_core.IssueType.LowTestability,
|
|
167
|
+
dimension: "purity",
|
|
168
|
+
severity: import_core.Severity.Major,
|
|
169
|
+
message: `File has only ${file.purityScore}% pure functions \u2014 logic is hard for AI to verify safely.`,
|
|
170
|
+
location: { file: file.filePath, line: 1 },
|
|
171
|
+
suggestion: "Extract side-effectful logic into pure, testable functions."
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
} else {
|
|
175
|
+
issues.push({
|
|
176
|
+
type: import_core.IssueType.LowTestability,
|
|
177
|
+
dimension: "purity",
|
|
178
|
+
severity: import_core.Severity.Major,
|
|
179
|
+
message: `Only ${indexResult.dimensions.purityScore}% of project functions are pure \u2014 reduces AI verification safety.`,
|
|
180
|
+
location: { file: options.rootDir, line: 0 },
|
|
181
|
+
suggestion: "Promote pure function patterns to improve codebase testability."
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (indexResult.dimensions.dependencyInjectionScore < 60) {
|
|
186
|
+
issues.push({
|
|
187
|
+
type: import_core.IssueType.LowTestability,
|
|
188
|
+
dimension: "dependency-injection",
|
|
189
|
+
severity: import_core.Severity.Major,
|
|
190
|
+
message: `Only ${indexResult.dimensions.dependencyInjectionScore}% of classes use dependency injection \u2014 hard to mock for AI verification.`,
|
|
191
|
+
location: { file: options.rootDir, line: 0 },
|
|
192
|
+
suggestion: "Use constructor-based dependency injection to make components mockable."
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (aggregated.bloatedInterfaces > 0) {
|
|
196
|
+
issues.push({
|
|
197
|
+
type: import_core.IssueType.LowTestability,
|
|
198
|
+
dimension: "interface-focus",
|
|
199
|
+
severity: import_core.Severity.Minor,
|
|
200
|
+
message: `Found ${aggregated.bloatedInterfaces} bloated interfaces/classes \u2014 large interfaces are harder for AI to implement correctly.`,
|
|
201
|
+
location: { file: options.rootDir, line: 0 },
|
|
202
|
+
suggestion: "Split large interfaces into smaller, focused ones (Interface Segregation Principle)."
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (indexResult.dimensions.observabilityScore < 60) {
|
|
162
206
|
issues.push({
|
|
163
207
|
type: import_core.IssueType.LowTestability,
|
|
164
|
-
dimension: "
|
|
208
|
+
dimension: "observability",
|
|
165
209
|
severity: import_core.Severity.Major,
|
|
166
|
-
message: `
|
|
210
|
+
message: `High rate of external state mutations detected (${indexResult.dimensions.observabilityScore}/100 observability) \u2014 hard for AI to reason about state changes.`,
|
|
167
211
|
location: { file: options.rootDir, line: 0 },
|
|
168
|
-
suggestion: "
|
|
212
|
+
suggestion: "Prefer immutable data patterns and return values instead of mutating external state."
|
|
169
213
|
});
|
|
170
214
|
}
|
|
171
215
|
return {
|
package/dist/cli.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -162,14 +162,58 @@ async function analyzeTestability(options) {
|
|
|
162
162
|
suggestion: `Add ~${needed} test file(s) to reach the ${Math.round(minCoverage * 100)}% minimum for safe AI assistance.`
|
|
163
163
|
});
|
|
164
164
|
}
|
|
165
|
-
if (indexResult.dimensions.purityScore <
|
|
165
|
+
if (indexResult.dimensions.purityScore < 70) {
|
|
166
|
+
const worstPurityFiles = (indexResult.fileMetrics || []).filter((m) => !m.isEntryPoint && m.purityScore < 50).sort((a, b) => a.purityScore - b.purityScore).slice(0, 5);
|
|
167
|
+
if (worstPurityFiles.length > 0) {
|
|
168
|
+
worstPurityFiles.forEach((file) => {
|
|
169
|
+
issues.push({
|
|
170
|
+
type: import_core.IssueType.LowTestability,
|
|
171
|
+
dimension: "purity",
|
|
172
|
+
severity: import_core.Severity.Major,
|
|
173
|
+
message: `File has only ${file.purityScore}% pure functions \u2014 logic is hard for AI to verify safely.`,
|
|
174
|
+
location: { file: file.filePath, line: 1 },
|
|
175
|
+
suggestion: "Extract side-effectful logic into pure, testable functions."
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
issues.push({
|
|
180
|
+
type: import_core.IssueType.LowTestability,
|
|
181
|
+
dimension: "purity",
|
|
182
|
+
severity: import_core.Severity.Major,
|
|
183
|
+
message: `Only ${indexResult.dimensions.purityScore}% of project functions are pure \u2014 reduces AI verification safety.`,
|
|
184
|
+
location: { file: options.rootDir, line: 0 },
|
|
185
|
+
suggestion: "Promote pure function patterns to improve codebase testability."
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (indexResult.dimensions.dependencyInjectionScore < 60) {
|
|
190
|
+
issues.push({
|
|
191
|
+
type: import_core.IssueType.LowTestability,
|
|
192
|
+
dimension: "dependency-injection",
|
|
193
|
+
severity: import_core.Severity.Major,
|
|
194
|
+
message: `Only ${indexResult.dimensions.dependencyInjectionScore}% of classes use dependency injection \u2014 hard to mock for AI verification.`,
|
|
195
|
+
location: { file: options.rootDir, line: 0 },
|
|
196
|
+
suggestion: "Use constructor-based dependency injection to make components mockable."
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (aggregated.bloatedInterfaces > 0) {
|
|
200
|
+
issues.push({
|
|
201
|
+
type: import_core.IssueType.LowTestability,
|
|
202
|
+
dimension: "interface-focus",
|
|
203
|
+
severity: import_core.Severity.Minor,
|
|
204
|
+
message: `Found ${aggregated.bloatedInterfaces} bloated interfaces/classes \u2014 large interfaces are harder for AI to implement correctly.`,
|
|
205
|
+
location: { file: options.rootDir, line: 0 },
|
|
206
|
+
suggestion: "Split large interfaces into smaller, focused ones (Interface Segregation Principle)."
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (indexResult.dimensions.observabilityScore < 60) {
|
|
166
210
|
issues.push({
|
|
167
211
|
type: import_core.IssueType.LowTestability,
|
|
168
|
-
dimension: "
|
|
212
|
+
dimension: "observability",
|
|
169
213
|
severity: import_core.Severity.Major,
|
|
170
|
-
message: `
|
|
214
|
+
message: `High rate of external state mutations detected (${indexResult.dimensions.observabilityScore}/100 observability) \u2014 hard for AI to reason about state changes.`,
|
|
171
215
|
location: { file: options.rootDir, line: 0 },
|
|
172
|
-
suggestion: "
|
|
216
|
+
suggestion: "Prefer immutable data patterns and return values instead of mutating external state."
|
|
173
217
|
});
|
|
174
218
|
}
|
|
175
219
|
return {
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/testability",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.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.24.
|
|
43
|
+
"@aiready/core": "0.24.8"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^24.0.0",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
"test": "vitest run",
|
|
58
58
|
"lint": "eslint src",
|
|
59
59
|
"clean": "rm -rf dist",
|
|
60
|
-
"release": "pnpm build && pnpm publish --no-git-checks"
|
|
60
|
+
"release": "pnpm build && pnpm publish --no-git-checks",
|
|
61
|
+
"type-check": "tsc --noEmit",
|
|
62
|
+
"format-check": "prettier --check . --ignore-path ../../.prettierignore"
|
|
61
63
|
}
|
|
62
64
|
}
|
package/src/analyzer.ts
CHANGED
|
@@ -197,15 +197,70 @@ export async function analyzeTestability(
|
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
if (indexResult.dimensions.purityScore <
|
|
200
|
+
if (indexResult.dimensions.purityScore < 70) {
|
|
201
|
+
const worstPurityFiles = (indexResult.fileMetrics || [])
|
|
202
|
+
.filter((m) => !m.isEntryPoint && m.purityScore < 50)
|
|
203
|
+
.sort((a, b) => a.purityScore - b.purityScore)
|
|
204
|
+
.slice(0, 5);
|
|
205
|
+
|
|
206
|
+
if (worstPurityFiles.length > 0) {
|
|
207
|
+
worstPurityFiles.forEach((file) => {
|
|
208
|
+
issues.push({
|
|
209
|
+
type: IssueType.LowTestability,
|
|
210
|
+
dimension: 'purity',
|
|
211
|
+
severity: Severity.Major,
|
|
212
|
+
message: `File has only ${file.purityScore}% pure functions — logic is hard for AI to verify safely.`,
|
|
213
|
+
location: { file: file.filePath, line: 1 },
|
|
214
|
+
suggestion:
|
|
215
|
+
'Extract side-effectful logic into pure, testable functions.',
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
} else {
|
|
219
|
+
issues.push({
|
|
220
|
+
type: IssueType.LowTestability,
|
|
221
|
+
dimension: 'purity',
|
|
222
|
+
severity: Severity.Major,
|
|
223
|
+
message: `Only ${indexResult.dimensions.purityScore}% of project functions are pure — reduces AI verification safety.`,
|
|
224
|
+
location: { file: options.rootDir, line: 0 },
|
|
225
|
+
suggestion:
|
|
226
|
+
'Promote pure function patterns to improve codebase testability.',
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (indexResult.dimensions.dependencyInjectionScore < 60) {
|
|
232
|
+
issues.push({
|
|
233
|
+
type: IssueType.LowTestability,
|
|
234
|
+
dimension: 'dependency-injection',
|
|
235
|
+
severity: Severity.Major,
|
|
236
|
+
message: `Only ${indexResult.dimensions.dependencyInjectionScore}% of classes use dependency injection — hard to mock for AI verification.`,
|
|
237
|
+
location: { file: options.rootDir, line: 0 },
|
|
238
|
+
suggestion:
|
|
239
|
+
'Use constructor-based dependency injection to make components mockable.',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (aggregated.bloatedInterfaces > 0) {
|
|
244
|
+
issues.push({
|
|
245
|
+
type: IssueType.LowTestability,
|
|
246
|
+
dimension: 'interface-focus',
|
|
247
|
+
severity: Severity.Minor,
|
|
248
|
+
message: `Found ${aggregated.bloatedInterfaces} bloated interfaces/classes — large interfaces are harder for AI to implement correctly.`,
|
|
249
|
+
location: { file: options.rootDir, line: 0 },
|
|
250
|
+
suggestion:
|
|
251
|
+
'Split large interfaces into smaller, focused ones (Interface Segregation Principle).',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (indexResult.dimensions.observabilityScore < 60) {
|
|
201
256
|
issues.push({
|
|
202
257
|
type: IssueType.LowTestability,
|
|
203
|
-
dimension: '
|
|
258
|
+
dimension: 'observability',
|
|
204
259
|
severity: Severity.Major,
|
|
205
|
-
message: `
|
|
260
|
+
message: `High rate of external state mutations detected (${indexResult.dimensions.observabilityScore}/100 observability) — hard for AI to reason about state changes.`,
|
|
206
261
|
location: { file: options.rootDir, line: 0 },
|
|
207
262
|
suggestion:
|
|
208
|
-
'
|
|
263
|
+
'Prefer immutable data patterns and return values instead of mutating external state.',
|
|
209
264
|
});
|
|
210
265
|
}
|
|
211
266
|
|