@aiready/testability 0.2.5 → 0.4.0

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.2.4 build /Users/pengcao/projects/aiready/packages/testability
3
+ > @aiready/testability@0.3.0 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
+ CJS dist/cli.js 18.44 KB
13
+ CJS dist/index.js 13.56 KB
14
+ CJS ⚡️ Build success in 51ms
12
15
  ESM dist/cli.mjs 5.75 KB
13
- ESM dist/chunk-ULOUVO3Q.mjs 11.02 KB
14
- ESM dist/index.mjs 152.00 B
15
- ESM ⚡️ Build success in 133ms
16
- CJS dist/cli.js 18.38 KB
17
- CJS dist/index.js 12.28 KB
18
- CJS ⚡️ Build success in 136ms
16
+ ESM dist/chunk-2QLLQPTL.mjs 11.08 KB
17
+ ESM dist/index.mjs 1.28 KB
18
+ ESM ⚡️ Build success in 53ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 4086ms
20
+ DTS ⚡️ Build success in 2884ms
21
21
  DTS dist/cli.d.ts 20.00 B
22
- DTS dist/index.d.ts 2.66 KB
22
+ DTS dist/index.d.ts 2.78 KB
23
23
  DTS dist/cli.d.mts 20.00 B
24
- DTS dist/index.d.mts 2.66 KB
24
+ DTS dist/index.d.mts 2.78 KB
@@ -1,5 +1,5 @@
1
1
 
2
2
  
3
- > @aiready/testability@0.1.6 lint /Users/pengcao/projects/aiready/packages/testability
3
+ > @aiready/testability@0.2.5 lint /Users/pengcao/projects/aiready/packages/testability
4
4
  > eslint src
5
5
 
@@ -1,16 +1,16 @@
1
1
 
2
2
  
3
- > @aiready/testability@0.2.4 test /Users/pengcao/projects/aiready/packages/testability
3
+ > @aiready/testability@0.3.0 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__/analyzer.test.ts (3 tests) 200ms
9
+ ✓ src/__tests__/analyzer.test.ts (3 tests) 29ms
10
10
 
11
11
   Test Files  1 passed (1)
12
12
   Tests  3 passed (3)
13
-  Start at  21:39:02
14
-  Duration  1.24s (transform 226ms, setup 0ms, import 767ms, tests 200ms, environment 0ms)
13
+  Start at  09:33:04
14
+  Duration  463ms (transform 130ms, setup 0ms, import 311ms, tests 29ms, environment 0ms)
15
15
 
16
16
  [?25h
@@ -0,0 +1,339 @@
1
+ // src/analyzer.ts
2
+ import {
3
+ scanFiles,
4
+ calculateTestabilityIndex,
5
+ Severity,
6
+ IssueType
7
+ } from "@aiready/core";
8
+ import { readFileSync, existsSync } from "fs";
9
+ import { join } from "path";
10
+ import { parse } from "@typescript-eslint/typescript-estree";
11
+ function countMethodsInInterface(node) {
12
+ if (node.type === "TSInterfaceDeclaration") {
13
+ return node.body.body.filter(
14
+ (m) => m.type === "TSMethodSignature" || m.type === "TSPropertySignature"
15
+ ).length;
16
+ }
17
+ if (node.type === "TSTypeAliasDeclaration" && node.typeAnnotation.type === "TSTypeLiteral") {
18
+ return node.typeAnnotation.members.length;
19
+ }
20
+ return 0;
21
+ }
22
+ function hasDependencyInjection(node) {
23
+ for (const member of node.body.body) {
24
+ if (member.type === "MethodDefinition" && member.key.type === "Identifier" && member.key.name === "constructor") {
25
+ const fn = member.value;
26
+ if (fn.params && fn.params.length > 0) {
27
+ const typedParams = fn.params.filter((p) => {
28
+ const param = p;
29
+ return param.typeAnnotation != null || param.parameter?.typeAnnotation != null;
30
+ });
31
+ if (typedParams.length > 0) return true;
32
+ }
33
+ }
34
+ }
35
+ return false;
36
+ }
37
+ function isPureFunction(fn) {
38
+ let hasReturn = false;
39
+ let hasSideEffect = false;
40
+ function walk(node) {
41
+ if (node.type === "ReturnStatement" && node.argument) hasReturn = true;
42
+ if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
43
+ hasSideEffect = true;
44
+ if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && ["console", "process", "window", "document", "fs"].includes(
45
+ node.callee.object.name
46
+ ))
47
+ hasSideEffect = true;
48
+ for (const key of Object.keys(node)) {
49
+ if (key === "parent") continue;
50
+ const child = node[key];
51
+ if (child && typeof child === "object") {
52
+ if (Array.isArray(child)) {
53
+ child.forEach((c) => c?.type && walk(c));
54
+ } else if (child.type) {
55
+ walk(child);
56
+ }
57
+ }
58
+ }
59
+ }
60
+ if (fn.body?.type === "BlockStatement") {
61
+ fn.body.body.forEach((s) => walk(s));
62
+ } else if (fn.body) {
63
+ hasReturn = true;
64
+ }
65
+ return hasReturn && !hasSideEffect;
66
+ }
67
+ function hasExternalStateMutation(fn) {
68
+ let found = false;
69
+ function walk(node) {
70
+ if (found) return;
71
+ if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
72
+ found = true;
73
+ for (const key of Object.keys(node)) {
74
+ if (key === "parent") continue;
75
+ const child = node[key];
76
+ if (child && typeof child === "object") {
77
+ if (Array.isArray(child)) child.forEach((c) => c?.type && walk(c));
78
+ else if (child.type) walk(child);
79
+ }
80
+ }
81
+ }
82
+ if (fn.body?.type === "BlockStatement") fn.body.body.forEach((s) => walk(s));
83
+ return found;
84
+ }
85
+ function analyzeFileTestability(filePath) {
86
+ const result = {
87
+ pureFunctions: 0,
88
+ totalFunctions: 0,
89
+ injectionPatterns: 0,
90
+ totalClasses: 0,
91
+ bloatedInterfaces: 0,
92
+ totalInterfaces: 0,
93
+ externalStateMutations: 0
94
+ };
95
+ let code;
96
+ try {
97
+ code = readFileSync(filePath, "utf-8");
98
+ } catch {
99
+ return result;
100
+ }
101
+ let ast;
102
+ try {
103
+ ast = parse(code, {
104
+ jsx: filePath.endsWith(".tsx") || filePath.endsWith(".jsx"),
105
+ range: false,
106
+ loc: false
107
+ });
108
+ } catch {
109
+ return result;
110
+ }
111
+ function visit(node) {
112
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
113
+ result.totalFunctions++;
114
+ if (isPureFunction(node)) result.pureFunctions++;
115
+ if (hasExternalStateMutation(node)) result.externalStateMutations++;
116
+ }
117
+ if (node.type === "ClassDeclaration" || node.type === "ClassExpression") {
118
+ result.totalClasses++;
119
+ if (hasDependencyInjection(node)) result.injectionPatterns++;
120
+ }
121
+ if (node.type === "TSInterfaceDeclaration" || node.type === "TSTypeAliasDeclaration") {
122
+ result.totalInterfaces++;
123
+ const methodCount = countMethodsInInterface(node);
124
+ if (methodCount > 10) result.bloatedInterfaces++;
125
+ }
126
+ for (const key of Object.keys(node)) {
127
+ if (key === "parent") continue;
128
+ const child = node[key];
129
+ if (child && typeof child === "object") {
130
+ if (Array.isArray(child)) child.forEach((c) => c?.type && visit(c));
131
+ else if (child.type) visit(child);
132
+ }
133
+ }
134
+ }
135
+ ast.body.forEach(visit);
136
+ return result;
137
+ }
138
+ function detectTestFramework(rootDir) {
139
+ const pkgPath = join(rootDir, "package.json");
140
+ if (!existsSync(pkgPath)) return false;
141
+ try {
142
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
143
+ const allDeps = {
144
+ ...pkg.dependencies ?? {},
145
+ ...pkg.devDependencies ?? {}
146
+ };
147
+ const testFrameworks = [
148
+ "jest",
149
+ "vitest",
150
+ "mocha",
151
+ "jasmine",
152
+ "ava",
153
+ "tap",
154
+ "pytest",
155
+ "unittest"
156
+ ];
157
+ return testFrameworks.some((fw) => allDeps[fw]);
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+ var TEST_PATTERNS = [
163
+ /\.(test|spec)\.(ts|tsx|js|jsx)$/,
164
+ /__tests__\//,
165
+ /\/tests?\//,
166
+ /\/e2e\//,
167
+ /\/fixtures\//
168
+ ];
169
+ function isTestFile(filePath, extra) {
170
+ if (TEST_PATTERNS.some((p) => p.test(filePath))) return true;
171
+ if (extra) return extra.some((p) => filePath.includes(p));
172
+ return false;
173
+ }
174
+ async function analyzeTestability(options) {
175
+ const allFiles = await scanFiles({
176
+ ...options,
177
+ include: options.include || ["**/*.{ts,tsx,js,jsx}"],
178
+ includeTests: true
179
+ });
180
+ const sourceFiles = allFiles.filter(
181
+ (f) => !isTestFile(f, options.testPatterns)
182
+ );
183
+ const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
184
+ const aggregated = {
185
+ pureFunctions: 0,
186
+ totalFunctions: 0,
187
+ injectionPatterns: 0,
188
+ totalClasses: 0,
189
+ bloatedInterfaces: 0,
190
+ totalInterfaces: 0,
191
+ externalStateMutations: 0
192
+ };
193
+ let processed = 0;
194
+ for (const f of sourceFiles) {
195
+ processed++;
196
+ options.onProgress?.(
197
+ processed,
198
+ sourceFiles.length,
199
+ `testability: analyzing files`
200
+ );
201
+ const a = analyzeFileTestability(f);
202
+ for (const key of Object.keys(aggregated)) {
203
+ aggregated[key] += a[key];
204
+ }
205
+ }
206
+ const hasTestFramework = detectTestFramework(options.rootDir);
207
+ const indexResult = calculateTestabilityIndex({
208
+ testFiles: testFiles.length,
209
+ sourceFiles: sourceFiles.length,
210
+ pureFunctions: aggregated.pureFunctions,
211
+ totalFunctions: Math.max(1, aggregated.totalFunctions),
212
+ injectionPatterns: aggregated.injectionPatterns,
213
+ totalClasses: Math.max(1, aggregated.totalClasses),
214
+ bloatedInterfaces: aggregated.bloatedInterfaces,
215
+ totalInterfaces: Math.max(1, aggregated.totalInterfaces),
216
+ externalStateMutations: aggregated.externalStateMutations,
217
+ hasTestFramework
218
+ });
219
+ const issues = [];
220
+ const minCoverage = options.minCoverageRatio ?? 0.3;
221
+ const actualRatio = sourceFiles.length > 0 ? testFiles.length / sourceFiles.length : 0;
222
+ if (!hasTestFramework) {
223
+ issues.push({
224
+ type: IssueType.LowTestability,
225
+ dimension: "framework",
226
+ severity: Severity.Critical,
227
+ message: "No testing framework detected in package.json \u2014 AI changes cannot be verified at all.",
228
+ location: { file: options.rootDir, line: 0 },
229
+ suggestion: "Add Jest, Vitest, or another testing framework as a devDependency."
230
+ });
231
+ }
232
+ if (actualRatio < minCoverage) {
233
+ const needed = Math.ceil(sourceFiles.length * minCoverage) - testFiles.length;
234
+ issues.push({
235
+ type: IssueType.LowTestability,
236
+ dimension: "test-coverage",
237
+ severity: actualRatio === 0 ? Severity.Critical : Severity.Major,
238
+ message: `Test ratio is ${Math.round(actualRatio * 100)}% (${testFiles.length} test files for ${sourceFiles.length} source files). Need at least ${Math.round(minCoverage * 100)}%.`,
239
+ location: { file: options.rootDir, line: 0 },
240
+ suggestion: `Add ~${needed} test file(s) to reach the ${Math.round(minCoverage * 100)}% minimum for safe AI assistance.`
241
+ });
242
+ }
243
+ if (indexResult.dimensions.purityScore < 50) {
244
+ issues.push({
245
+ type: IssueType.LowTestability,
246
+ dimension: "purity",
247
+ severity: Severity.Major,
248
+ message: `Only ${indexResult.dimensions.purityScore}% of functions are pure \u2014 side-effectful functions require complex test setup.`,
249
+ location: { file: options.rootDir, line: 0 },
250
+ suggestion: "Extract pure transformation logic from I/O and mutation code."
251
+ });
252
+ }
253
+ if (indexResult.dimensions.observabilityScore < 50) {
254
+ issues.push({
255
+ type: IssueType.LowTestability,
256
+ dimension: "observability",
257
+ severity: Severity.Major,
258
+ message: `Many functions mutate external state directly \u2014 outputs are invisible to unit tests.`,
259
+ location: { file: options.rootDir, line: 0 },
260
+ suggestion: "Prefer returning values over mutating shared state."
261
+ });
262
+ }
263
+ return {
264
+ summary: {
265
+ sourceFiles: sourceFiles.length,
266
+ testFiles: testFiles.length,
267
+ coverageRatio: Math.round(actualRatio * 100) / 100,
268
+ score: indexResult.score,
269
+ rating: indexResult.rating,
270
+ aiChangeSafetyRating: indexResult.aiChangeSafetyRating,
271
+ dimensions: indexResult.dimensions
272
+ },
273
+ issues,
274
+ rawData: {
275
+ sourceFiles: sourceFiles.length,
276
+ testFiles: testFiles.length,
277
+ ...aggregated,
278
+ hasTestFramework
279
+ },
280
+ recommendations: indexResult.recommendations
281
+ };
282
+ }
283
+
284
+ // src/scoring.ts
285
+ import { ToolName } from "@aiready/core";
286
+ function calculateTestabilityScore(report) {
287
+ const { summary, rawData, recommendations } = report;
288
+ const factors = [
289
+ {
290
+ name: "Test Coverage",
291
+ impact: Math.round(summary.dimensions.testCoverageRatio - 50),
292
+ description: `${rawData.testFiles} test files / ${rawData.sourceFiles} source files (${Math.round(summary.coverageRatio * 100)}%)`
293
+ },
294
+ {
295
+ name: "Function Purity",
296
+ impact: Math.round(summary.dimensions.purityScore - 50),
297
+ description: `${rawData.pureFunctions}/${rawData.totalFunctions} functions are pure`
298
+ },
299
+ {
300
+ name: "Dependency Injection",
301
+ impact: Math.round(summary.dimensions.dependencyInjectionScore - 50),
302
+ description: `${rawData.injectionPatterns}/${rawData.totalClasses} classes use DI`
303
+ },
304
+ {
305
+ name: "Interface Focus",
306
+ impact: Math.round(summary.dimensions.interfaceFocusScore - 50),
307
+ description: `${rawData.bloatedInterfaces} interfaces have >10 methods`
308
+ },
309
+ {
310
+ name: "Observability",
311
+ impact: Math.round(summary.dimensions.observabilityScore - 50),
312
+ description: `${rawData.externalStateMutations} functions mutate external state`
313
+ }
314
+ ];
315
+ const recs = recommendations.map(
316
+ (action) => ({
317
+ action,
318
+ estimatedImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
319
+ priority: summary.aiChangeSafetyRating === "blind-risk" || summary.aiChangeSafetyRating === "high-risk" ? "high" : "medium"
320
+ })
321
+ );
322
+ return {
323
+ toolName: ToolName.TestabilityIndex,
324
+ score: summary.score,
325
+ rawMetrics: {
326
+ ...rawData,
327
+ rating: summary.rating,
328
+ aiChangeSafetyRating: summary.aiChangeSafetyRating,
329
+ coverageRatio: summary.coverageRatio
330
+ },
331
+ factors,
332
+ recommendations: recs
333
+ };
334
+ }
335
+
336
+ export {
337
+ analyzeTestability,
338
+ calculateTestabilityScore
339
+ };
package/dist/cli.js CHANGED
@@ -305,6 +305,7 @@ async function analyzeTestability(options) {
305
305
  }
306
306
 
307
307
  // src/scoring.ts
308
+ var import_core2 = require("@aiready/core");
308
309
  function calculateTestabilityScore(report) {
309
310
  const { summary, rawData, recommendations } = report;
310
311
  const factors = [
@@ -342,7 +343,7 @@ function calculateTestabilityScore(report) {
342
343
  })
343
344
  );
344
345
  return {
345
- toolName: "testability",
346
+ toolName: import_core2.ToolName.TestabilityIndex,
346
347
  score: summary.score,
347
348
  rawMetrics: {
348
349
  ...rawData,
@@ -359,7 +360,7 @@ function calculateTestabilityScore(report) {
359
360
  var import_chalk = __toESM(require("chalk"));
360
361
  var import_fs2 = require("fs");
361
362
  var import_path2 = require("path");
362
- var import_core2 = require("@aiready/core");
363
+ var import_core3 = require("@aiready/core");
363
364
  var program = new import_commander.Command();
364
365
  program.name("aiready-testability").description(
365
366
  "Measure how safely AI-generated changes can be verified in your codebase"
@@ -394,8 +395,8 @@ EXAMPLES:
394
395
  ).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) => {
395
396
  console.log(import_chalk.default.blue("\u{1F9EA} Analyzing testability...\n"));
396
397
  const startTime = Date.now();
397
- const config = await (0, import_core2.loadConfig)(directory);
398
- const mergedConfig = (0, import_core2.mergeConfigWithDefaults)(config, {
398
+ const config = await (0, import_core3.loadConfig)(directory);
399
+ const mergedConfig = (0, import_core3.mergeConfigWithDefaults)(config, {
399
400
  minCoverageRatio: 0.3
400
401
  });
401
402
  const finalOptions = {
@@ -410,7 +411,7 @@ EXAMPLES:
410
411
  const elapsed = ((Date.now() - startTime) / 1e3).toFixed(2);
411
412
  if (options.output === "json") {
412
413
  const payload = { report, score: scoring };
413
- const outputPath = (0, import_core2.resolveOutputPath)(
414
+ const outputPath = (0, import_core3.resolveOutputPath)(
414
415
  options.outputFile,
415
416
  `testability-report-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`,
416
417
  directory
@@ -443,9 +444,9 @@ function displayConsoleReport(report, scoring, elapsed) {
443
444
  );
444
445
  console.log();
445
446
  }
446
- const safetyColor = (0, import_core2.getSeverityColor)(safetyRating, import_chalk.default);
447
+ const safetyColor = (0, import_core3.getSeverityColor)(safetyRating, import_chalk.default);
447
448
  console.log(
448
- `AI Change Safety: ${safetyColor(`${(0, import_core2.getSafetyIcon)(safetyRating)} ${safetyRating.toUpperCase()}`)}`
449
+ `AI Change Safety: ${safetyColor(`${(0, import_core3.getSafetyIcon)(safetyRating)} ${safetyRating.toUpperCase()}`)}`
449
450
  );
450
451
  console.log(
451
452
  `Score: ${import_chalk.default.bold(summary.score + "/100")} (${summary.rating})`
@@ -468,12 +469,12 @@ function displayConsoleReport(report, scoring, elapsed) {
468
469
  ];
469
470
  for (const [name, val] of dims) {
470
471
  const color = val >= 70 ? import_chalk.default.green : val >= 50 ? import_chalk.default.yellow : import_chalk.default.red;
471
- console.log(` ${name.padEnd(22)} ${color((0, import_core2.getScoreBar)(val))} ${val}/100`);
472
+ console.log(` ${name.padEnd(22)} ${color((0, import_core3.getScoreBar)(val))} ${val}/100`);
472
473
  }
473
474
  if (issues.length > 0) {
474
475
  console.log(import_chalk.default.bold("\n\u26A0\uFE0F Issues\n"));
475
476
  for (const issue of issues) {
476
- const sev = (0, import_core2.getSeverityColor)(issue.severity, import_chalk.default);
477
+ const sev = (0, import_core3.getSeverityColor)(issue.severity, import_chalk.default);
477
478
  console.log(`${sev(issue.severity.toUpperCase())} ${issue.message}`);
478
479
  if (issue.suggestion)
479
480
  console.log(
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  analyzeTestability,
4
4
  calculateTestabilityScore
5
- } from "./chunk-ULOUVO3Q.mjs";
5
+ } from "./chunk-2QLLQPTL.mjs";
6
6
 
7
7
  // src/cli.ts
8
8
  import { Command } from "commander";
package/dist/index.d.mts CHANGED
@@ -1,4 +1,9 @@
1
- import { Issue, IssueType, ToolScoringOutput } from '@aiready/core';
1
+ import { ToolProvider, Issue, IssueType, ToolScoringOutput } from '@aiready/core';
2
+
3
+ /**
4
+ * Testability Tool Provider
5
+ */
6
+ declare const TestabilityProvider: ToolProvider;
2
7
 
3
8
  interface TestabilityOptions {
4
9
  /** Root directory to scan */
@@ -73,4 +78,4 @@ declare function analyzeTestability(options: TestabilityOptions): Promise<Testab
73
78
  */
74
79
  declare function calculateTestabilityScore(report: TestabilityReport): ToolScoringOutput;
75
80
 
76
- export { type TestabilityIssue, type TestabilityOptions, type TestabilityReport, analyzeTestability, calculateTestabilityScore };
81
+ export { type TestabilityIssue, type TestabilityOptions, TestabilityProvider, type TestabilityReport, analyzeTestability, calculateTestabilityScore };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,9 @@
1
- import { Issue, IssueType, ToolScoringOutput } from '@aiready/core';
1
+ import { ToolProvider, Issue, IssueType, ToolScoringOutput } from '@aiready/core';
2
+
3
+ /**
4
+ * Testability Tool Provider
5
+ */
6
+ declare const TestabilityProvider: ToolProvider;
2
7
 
3
8
  interface TestabilityOptions {
4
9
  /** Root directory to scan */
@@ -73,4 +78,4 @@ declare function analyzeTestability(options: TestabilityOptions): Promise<Testab
73
78
  */
74
79
  declare function calculateTestabilityScore(report: TestabilityReport): ToolScoringOutput;
75
80
 
76
- export { type TestabilityIssue, type TestabilityOptions, type TestabilityReport, analyzeTestability, calculateTestabilityScore };
81
+ export { type TestabilityIssue, type TestabilityOptions, TestabilityProvider, type TestabilityReport, analyzeTestability, calculateTestabilityScore };
package/dist/index.js CHANGED
@@ -20,10 +20,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ TestabilityProvider: () => TestabilityProvider,
23
24
  analyzeTestability: () => analyzeTestability,
24
25
  calculateTestabilityScore: () => calculateTestabilityScore
25
26
  });
26
27
  module.exports = __toCommonJS(index_exports);
28
+ var import_core4 = require("@aiready/core");
29
+
30
+ // src/provider.ts
31
+ var import_core3 = require("@aiready/core");
27
32
 
28
33
  // src/analyzer.ts
29
34
  var import_core = require("@aiready/core");
@@ -304,6 +309,7 @@ async function analyzeTestability(options) {
304
309
  }
305
310
 
306
311
  // src/scoring.ts
312
+ var import_core2 = require("@aiready/core");
307
313
  function calculateTestabilityScore(report) {
308
314
  const { summary, rawData, recommendations } = report;
309
315
  const factors = [
@@ -341,7 +347,7 @@ function calculateTestabilityScore(report) {
341
347
  })
342
348
  );
343
349
  return {
344
- toolName: "testability",
350
+ toolName: import_core2.ToolName.TestabilityIndex,
345
351
  score: summary.score,
346
352
  rawMetrics: {
347
353
  ...rawData,
@@ -353,8 +359,47 @@ function calculateTestabilityScore(report) {
353
359
  recommendations: recs
354
360
  };
355
361
  }
362
+
363
+ // src/provider.ts
364
+ var TestabilityProvider = {
365
+ id: import_core3.ToolName.TestabilityIndex,
366
+ alias: ["testability", "tests", "verification"],
367
+ async analyze(options) {
368
+ const report = await analyzeTestability(options);
369
+ const results = report.issues.map((i) => ({
370
+ fileName: i.location.file,
371
+ issues: [i],
372
+ metrics: {
373
+ testabilityScore: report.summary.score
374
+ }
375
+ }));
376
+ return import_core3.SpokeOutputSchema.parse({
377
+ results,
378
+ summary: report.summary,
379
+ metadata: {
380
+ toolName: import_core3.ToolName.TestabilityIndex,
381
+ version: "0.2.5",
382
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
383
+ rawData: report.rawData
384
+ }
385
+ });
386
+ },
387
+ score(output, options) {
388
+ const report = {
389
+ summary: output.summary,
390
+ rawData: output.metadata.rawData,
391
+ recommendations: output.summary.recommendations || []
392
+ };
393
+ return calculateTestabilityScore(report);
394
+ },
395
+ defaultWeight: 10
396
+ };
397
+
398
+ // src/index.ts
399
+ import_core4.ToolRegistry.register(TestabilityProvider);
356
400
  // Annotate the CommonJS export names for ESM import in node:
357
401
  0 && (module.exports = {
402
+ TestabilityProvider,
358
403
  analyzeTestability,
359
404
  calculateTestabilityScore
360
405
  });
package/dist/index.mjs CHANGED
@@ -1,8 +1,54 @@
1
1
  import {
2
2
  analyzeTestability,
3
3
  calculateTestabilityScore
4
- } from "./chunk-ULOUVO3Q.mjs";
4
+ } from "./chunk-2QLLQPTL.mjs";
5
+
6
+ // src/index.ts
7
+ import { ToolRegistry } from "@aiready/core";
8
+
9
+ // src/provider.ts
10
+ import {
11
+ ToolName,
12
+ SpokeOutputSchema
13
+ } from "@aiready/core";
14
+ var TestabilityProvider = {
15
+ id: ToolName.TestabilityIndex,
16
+ alias: ["testability", "tests", "verification"],
17
+ async analyze(options) {
18
+ const report = await analyzeTestability(options);
19
+ const results = report.issues.map((i) => ({
20
+ fileName: i.location.file,
21
+ issues: [i],
22
+ metrics: {
23
+ testabilityScore: report.summary.score
24
+ }
25
+ }));
26
+ return SpokeOutputSchema.parse({
27
+ results,
28
+ summary: report.summary,
29
+ metadata: {
30
+ toolName: ToolName.TestabilityIndex,
31
+ version: "0.2.5",
32
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
33
+ rawData: report.rawData
34
+ }
35
+ });
36
+ },
37
+ score(output, options) {
38
+ const report = {
39
+ summary: output.summary,
40
+ rawData: output.metadata.rawData,
41
+ recommendations: output.summary.recommendations || []
42
+ };
43
+ return calculateTestabilityScore(report);
44
+ },
45
+ defaultWeight: 10
46
+ };
47
+
48
+ // src/index.ts
49
+ ToolRegistry.register(TestabilityProvider);
5
50
  export {
51
+ TestabilityProvider,
6
52
  analyzeTestability,
7
53
  calculateTestabilityScore
8
54
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/testability",
3
- "version": "0.2.5",
3
+ "version": "0.4.0",
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.19.5"
43
+ "@aiready/core": "0.21.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^24.0.0",
package/src/index.ts CHANGED
@@ -1,5 +1,12 @@
1
+ import { ToolRegistry } from '@aiready/core';
2
+ import { TestabilityProvider } from './provider';
3
+
4
+ // Register with global registry
5
+ ToolRegistry.register(TestabilityProvider);
6
+
1
7
  export { analyzeTestability } from './analyzer';
2
8
  export { calculateTestabilityScore } from './scoring';
9
+ export { TestabilityProvider };
3
10
  export type {
4
11
  TestabilityOptions,
5
12
  TestabilityReport,
@@ -0,0 +1,55 @@
1
+ import {
2
+ ToolProvider,
3
+ ToolName,
4
+ SpokeOutput,
5
+ ScanOptions,
6
+ ToolScoringOutput,
7
+ AnalysisResult,
8
+ SpokeOutputSchema,
9
+ } from '@aiready/core';
10
+ import { analyzeTestability } from './analyzer';
11
+ import { calculateTestabilityScore } from './scoring';
12
+ import { TestabilityOptions, TestabilityReport } from './types';
13
+
14
+ /**
15
+ * Testability Tool Provider
16
+ */
17
+ export const TestabilityProvider: ToolProvider = {
18
+ id: ToolName.TestabilityIndex,
19
+ alias: ['testability', 'tests', 'verification'],
20
+
21
+ async analyze(options: ScanOptions): Promise<SpokeOutput> {
22
+ const report = await analyzeTestability(options as TestabilityOptions);
23
+
24
+ const results: AnalysisResult[] = report.issues.map((i) => ({
25
+ fileName: i.location.file,
26
+ issues: [i] as any[],
27
+ metrics: {
28
+ testabilityScore: report.summary.score,
29
+ },
30
+ }));
31
+
32
+ return SpokeOutputSchema.parse({
33
+ results,
34
+ summary: report.summary,
35
+ metadata: {
36
+ toolName: ToolName.TestabilityIndex,
37
+ version: '0.2.5',
38
+ timestamp: new Date().toISOString(),
39
+ rawData: report.rawData,
40
+ },
41
+ });
42
+ },
43
+
44
+ score(output: SpokeOutput, options: ScanOptions): ToolScoringOutput {
45
+ const report = {
46
+ summary: output.summary,
47
+ rawData: (output.metadata as any).rawData,
48
+ recommendations: (output.summary as any).recommendations || [],
49
+ } as unknown as TestabilityReport;
50
+
51
+ return calculateTestabilityScore(report);
52
+ },
53
+
54
+ defaultWeight: 10,
55
+ };
package/src/scoring.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ToolScoringOutput } from '@aiready/core';
1
+ import { type ToolScoringOutput, ToolName } from '@aiready/core';
2
2
  import type { TestabilityReport } from './types';
3
3
 
4
4
  /**
@@ -50,7 +50,7 @@ export function calculateTestabilityScore(
50
50
  );
51
51
 
52
52
  return {
53
- toolName: 'testability',
53
+ toolName: ToolName.TestabilityIndex,
54
54
  score: summary.score,
55
55
  rawMetrics: {
56
56
  ...rawData,