@aiready/testability 0.6.19 → 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.
@@ -1,6 +1,6 @@
1
1
 
2
2
  
3
- > @aiready/testability@0.6.19 build /Users/pengcao/projects/aiready/packages/testability
3
+ > @aiready/testability@0.6.20 build /Users/pengcao/projects/aiready/packages/testability
4
4
  > tsup src/index.ts src/cli.ts --format cjs,esm --dts
5
5
 
6
6
  CLI Building entry: src/cli.ts, src/index.ts
@@ -9,16 +9,16 @@
9
9
  CLI Target: es2020
10
10
  CJS Build start
11
11
  ESM Build start
12
- ESM dist/cli.mjs 5.75 KB
12
+ CJS dist/cli.js 13.00 KB
13
+ CJS dist/index.js 9.74 KB
14
+ CJS ⚡️ Build success in 166ms
15
+ ESM dist/cli.mjs 4.33 KB
13
16
  ESM dist/index.mjs 1.17 KB
14
- ESM dist/chunk-JL4S6RHJ.mjs 8.47 KB
15
- ESM ⚡️ Build success in 231ms
16
- CJS dist/cli.js 15.78 KB
17
- CJS dist/index.js 10.78 KB
18
- CJS ⚡️ Build success in 258ms
17
+ ESM dist/chunk-QMDUZA7H.mjs 7.44 KB
18
+ ESM ⚡️ Build success in 167ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 5698ms
20
+ DTS ⚡️ Build success in 5538ms
21
21
  DTS dist/cli.d.ts 20.00 B
22
- DTS dist/index.d.ts 2.62 KB
22
+ DTS dist/index.d.ts 2.42 KB
23
23
  DTS dist/cli.d.mts 20.00 B
24
- DTS dist/index.d.mts 2.62 KB
24
+ DTS dist/index.d.mts 2.42 KB
@@ -1,20 +1,20 @@
1
1
 
2
2
  
3
- > @aiready/testability@0.6.18 test /Users/pengcao/projects/aiready/packages/testability
3
+ > @aiready/testability@0.6.19 test /Users/pengcao/projects/aiready/packages/testability
4
4
  > vitest run
5
5
 
6
6
  [?25l
7
7
   RUN  v4.0.18 /Users/pengcao/projects/aiready/packages/testability
8
8
 
9
- ✓ src/__tests__/types.test.ts (4 tests) 3ms
10
- ✓ src/__tests__/scoring.test.ts (3 tests) 3ms
11
- ✓ src/__tests__/provider.test.ts (2 tests) 4ms
12
- ✓ src/__tests__/analyzer.test.ts (5 tests) 4805ms
13
- ✓ detects test frameworks in multiple languages  4773ms
9
+ ✓ src/__tests__/types.test.ts (4 tests) 2ms
10
+ ✓ src/__tests__/scoring.test.ts (3 tests) 2ms
11
+ ✓ src/__tests__/provider.test.ts (2 tests) 2ms
12
+ ✓ src/__tests__/analyzer.test.ts (5 tests) 785ms
13
+ ✓ detects test frameworks in multiple languages  758ms
14
14
 
15
15
   Test Files  4 passed (4)
16
16
   Tests  14 passed (14)
17
-  Start at  13:03:38
18
-  Duration  9.81s (transform 1.59s, setup 0ms, import 11.23s, tests 4.82s, environment 1ms)
17
+  Start at  23:32:56
18
+  Duration  1.30s (transform 482ms, setup 0ms, import 1.22s, tests 791ms, environment 0ms)
19
19
 
20
20
  [?25h
@@ -0,0 +1,239 @@
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
+ recommendationImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
232
+ rating: summary.aiChangeSafetyRating || summary.rating
233
+ });
234
+ }
235
+
236
+ export {
237
+ analyzeTestability,
238
+ calculateTestabilityScore
239
+ };
@@ -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 = 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
+ };