@aiready/testability 0.1.5 → 0.1.8

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