@aiready/testability 0.1.5 → 0.1.6

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.1.5 build /Users/pengcao/projects/aiready/packages/testability
3
+ > @aiready/testability@0.1.6 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,15 +9,15 @@
9
9
  CLI Target: es2020
10
10
  CJS Build start
11
11
  ESM Build start
12
- CJS dist/cli.js 19.50 KB
13
- CJS dist/index.js 12.85 KB
14
- CJS ⚡️ Build success in 379ms
12
+ CJS dist/cli.js 19.75 KB
13
+ CJS dist/index.js 12.97 KB
14
+ CJS ⚡️ Build success in 138ms
15
15
  ESM dist/index.mjs 152.00 B
16
- ESM dist/chunk-CYZ7DTWN.mjs 11.64 KB
17
- ESM dist/cli.mjs 6.24 KB
18
- ESM ⚡️ Build success in 402ms
16
+ ESM dist/chunk-YLYLRZRS.mjs 11.76 KB
17
+ ESM dist/cli.mjs 6.37 KB
18
+ ESM ⚡️ Build success in 138ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 23828ms
20
+ DTS ⚡️ Build success in 4492ms
21
21
  DTS dist/cli.d.ts 20.00 B
22
22
  DTS dist/index.d.ts 2.54 KB
23
23
  DTS dist/cli.d.mts 20.00 B
@@ -1,18 +1,16 @@
1
1
 
2
2
  
3
- > @aiready/testability@0.1.5 test /Users/pengcao/projects/aiready/packages/testability
3
+ > @aiready/testability@0.1.6 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) 1236ms
10
- ✓ should calculate ratio of test files to source files  732ms
11
- ✓ should detect interfaces with too many methods  473ms
9
+ ✓ src/__tests__/analyzer.test.ts (3 tests) 88ms
12
10
 
13
11
   Test Files  1 passed (1)
14
12
   Tests  3 passed (3)
15
-  Start at  14:38:43
16
-  Duration  21.61s (transform 5.52s, setup 0ms, import 17.77s, tests 1.24s, environment 1ms)
13
+  Start at  22:19:15
14
+  Duration  1.19s (transform 173ms, setup 0ms, import 878ms, tests 88ms, environment 0ms)
17
15
 
18
16
  [?25h
@@ -0,0 +1,363 @@
1
+ // src/analyzer.ts
2
+ import { readdirSync, statSync, readFileSync, existsSync } from "fs";
3
+ import { join, extname } from "path";
4
+ import { parse } from "@typescript-eslint/typescript-estree";
5
+ import { calculateTestabilityIndex } from "@aiready/core";
6
+ var SRC_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
7
+ var DEFAULT_EXCLUDES = [
8
+ "node_modules",
9
+ "dist",
10
+ ".git",
11
+ "coverage",
12
+ ".turbo",
13
+ "build"
14
+ ];
15
+ var TEST_PATTERNS = [
16
+ /\.(test|spec)\.(ts|tsx|js|jsx)$/,
17
+ /__tests__\//,
18
+ /\/tests?\//,
19
+ /\/e2e\//,
20
+ /\/fixtures\//
21
+ ];
22
+ function isTestFile(filePath, extra) {
23
+ if (TEST_PATTERNS.some((p) => p.test(filePath))) return true;
24
+ if (extra) return extra.some((p) => filePath.includes(p));
25
+ return false;
26
+ }
27
+ function isSourceFile(filePath) {
28
+ return SRC_EXTENSIONS.has(extname(filePath));
29
+ }
30
+ function collectFiles(dir, options, depth = 0) {
31
+ if (depth > (options.maxDepth ?? 20)) return [];
32
+ const excludes = [...DEFAULT_EXCLUDES, ...options.exclude ?? []];
33
+ const files = [];
34
+ let entries;
35
+ try {
36
+ entries = readdirSync(dir);
37
+ } catch {
38
+ return files;
39
+ }
40
+ for (const entry of entries) {
41
+ if (excludes.some((ex) => entry === ex || entry.includes(ex))) continue;
42
+ const full = join(dir, entry);
43
+ let stat;
44
+ try {
45
+ stat = statSync(full);
46
+ } catch {
47
+ continue;
48
+ }
49
+ if (stat.isDirectory()) {
50
+ files.push(...collectFiles(full, options, depth + 1));
51
+ } else if (stat.isFile() && isSourceFile(full)) {
52
+ if (!options.include || options.include.some((p) => full.includes(p))) {
53
+ files.push(full);
54
+ }
55
+ }
56
+ }
57
+ return files;
58
+ }
59
+ function countMethodsInInterface(node) {
60
+ if (node.type === "TSInterfaceDeclaration") {
61
+ return node.body.body.filter(
62
+ (m) => m.type === "TSMethodSignature" || m.type === "TSPropertySignature"
63
+ ).length;
64
+ }
65
+ if (node.type === "TSTypeAliasDeclaration" && node.typeAnnotation.type === "TSTypeLiteral") {
66
+ return node.typeAnnotation.members.length;
67
+ }
68
+ return 0;
69
+ }
70
+ function hasDependencyInjection(node) {
71
+ for (const member of node.body.body) {
72
+ if (member.type === "MethodDefinition" && member.key.type === "Identifier" && member.key.name === "constructor") {
73
+ const fn = member.value;
74
+ if (fn.params && fn.params.length > 0) {
75
+ const typedParams = fn.params.filter((p) => {
76
+ const param = p;
77
+ return param.typeAnnotation != null || param.parameter?.typeAnnotation != null;
78
+ });
79
+ if (typedParams.length > 0) return true;
80
+ }
81
+ }
82
+ }
83
+ return false;
84
+ }
85
+ function isPureFunction(fn) {
86
+ let hasReturn = false;
87
+ let hasSideEffect = false;
88
+ function walk(node) {
89
+ if (node.type === "ReturnStatement" && node.argument) hasReturn = true;
90
+ if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
91
+ hasSideEffect = true;
92
+ if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && ["console", "process", "window", "document", "fs"].includes(
93
+ node.callee.object.name
94
+ ))
95
+ hasSideEffect = true;
96
+ for (const key of Object.keys(node)) {
97
+ if (key === "parent") continue;
98
+ const child = node[key];
99
+ if (child && typeof child === "object") {
100
+ if (Array.isArray(child)) {
101
+ child.forEach((c) => c?.type && walk(c));
102
+ } else if (child.type) {
103
+ walk(child);
104
+ }
105
+ }
106
+ }
107
+ }
108
+ if (fn.body?.type === "BlockStatement") {
109
+ fn.body.body.forEach((s) => walk(s));
110
+ } else if (fn.body) {
111
+ hasReturn = true;
112
+ }
113
+ return hasReturn && !hasSideEffect;
114
+ }
115
+ function hasExternalStateMutation(fn) {
116
+ let found = false;
117
+ function walk(node) {
118
+ if (found) return;
119
+ if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
120
+ found = true;
121
+ for (const key of Object.keys(node)) {
122
+ if (key === "parent") continue;
123
+ const child = node[key];
124
+ if (child && typeof child === "object") {
125
+ if (Array.isArray(child)) child.forEach((c) => c?.type && walk(c));
126
+ else if (child.type) walk(child);
127
+ }
128
+ }
129
+ }
130
+ if (fn.body?.type === "BlockStatement") fn.body.body.forEach((s) => walk(s));
131
+ return found;
132
+ }
133
+ function analyzeFileTestability(filePath) {
134
+ const result = {
135
+ pureFunctions: 0,
136
+ totalFunctions: 0,
137
+ injectionPatterns: 0,
138
+ totalClasses: 0,
139
+ bloatedInterfaces: 0,
140
+ totalInterfaces: 0,
141
+ externalStateMutations: 0
142
+ };
143
+ let code;
144
+ try {
145
+ code = readFileSync(filePath, "utf-8");
146
+ } catch {
147
+ return result;
148
+ }
149
+ let ast;
150
+ try {
151
+ ast = parse(code, {
152
+ jsx: filePath.endsWith(".tsx") || filePath.endsWith(".jsx"),
153
+ range: false,
154
+ loc: false
155
+ });
156
+ } catch {
157
+ return result;
158
+ }
159
+ function visit(node) {
160
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
161
+ result.totalFunctions++;
162
+ if (isPureFunction(node)) result.pureFunctions++;
163
+ if (hasExternalStateMutation(node)) result.externalStateMutations++;
164
+ }
165
+ if (node.type === "ClassDeclaration" || node.type === "ClassExpression") {
166
+ result.totalClasses++;
167
+ if (hasDependencyInjection(node)) result.injectionPatterns++;
168
+ }
169
+ if (node.type === "TSInterfaceDeclaration" || node.type === "TSTypeAliasDeclaration") {
170
+ result.totalInterfaces++;
171
+ const methodCount = countMethodsInInterface(node);
172
+ if (methodCount > 10) result.bloatedInterfaces++;
173
+ }
174
+ for (const key of Object.keys(node)) {
175
+ if (key === "parent") continue;
176
+ const child = node[key];
177
+ if (child && typeof child === "object") {
178
+ if (Array.isArray(child)) child.forEach((c) => c?.type && visit(c));
179
+ else if (child.type) visit(child);
180
+ }
181
+ }
182
+ }
183
+ ast.body.forEach(visit);
184
+ return result;
185
+ }
186
+ function detectTestFramework(rootDir) {
187
+ const pkgPath = join(rootDir, "package.json");
188
+ if (!existsSync(pkgPath)) return false;
189
+ try {
190
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
191
+ const allDeps = {
192
+ ...pkg.dependencies ?? {},
193
+ ...pkg.devDependencies ?? {}
194
+ };
195
+ const testFrameworks = [
196
+ "jest",
197
+ "vitest",
198
+ "mocha",
199
+ "jasmine",
200
+ "ava",
201
+ "tap",
202
+ "pytest",
203
+ "unittest"
204
+ ];
205
+ return testFrameworks.some((fw) => allDeps[fw]);
206
+ } catch {
207
+ return false;
208
+ }
209
+ }
210
+ async function analyzeTestability(options) {
211
+ const allFiles = collectFiles(options.rootDir, options);
212
+ const sourceFiles = allFiles.filter(
213
+ (f) => !isTestFile(f, options.testPatterns)
214
+ );
215
+ const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
216
+ const aggregated = {
217
+ pureFunctions: 0,
218
+ totalFunctions: 0,
219
+ injectionPatterns: 0,
220
+ totalClasses: 0,
221
+ bloatedInterfaces: 0,
222
+ totalInterfaces: 0,
223
+ externalStateMutations: 0
224
+ };
225
+ for (const f of sourceFiles) {
226
+ const a = analyzeFileTestability(f);
227
+ for (const key of Object.keys(aggregated)) {
228
+ aggregated[key] += a[key];
229
+ }
230
+ }
231
+ const hasTestFramework = detectTestFramework(options.rootDir);
232
+ const indexResult = calculateTestabilityIndex({
233
+ testFiles: testFiles.length,
234
+ sourceFiles: sourceFiles.length,
235
+ pureFunctions: aggregated.pureFunctions,
236
+ totalFunctions: Math.max(1, aggregated.totalFunctions),
237
+ injectionPatterns: aggregated.injectionPatterns,
238
+ totalClasses: Math.max(1, aggregated.totalClasses),
239
+ bloatedInterfaces: aggregated.bloatedInterfaces,
240
+ totalInterfaces: Math.max(1, aggregated.totalInterfaces),
241
+ externalStateMutations: aggregated.externalStateMutations,
242
+ hasTestFramework
243
+ });
244
+ const issues = [];
245
+ const minCoverage = options.minCoverageRatio ?? 0.3;
246
+ const actualRatio = sourceFiles.length > 0 ? testFiles.length / sourceFiles.length : 0;
247
+ if (!hasTestFramework) {
248
+ issues.push({
249
+ type: "low-testability",
250
+ dimension: "framework",
251
+ severity: "critical",
252
+ message: "No testing framework detected in package.json \u2014 AI changes cannot be verified at all.",
253
+ location: { file: options.rootDir, line: 0 },
254
+ suggestion: "Add Jest, Vitest, or another testing framework as a devDependency."
255
+ });
256
+ }
257
+ if (actualRatio < minCoverage) {
258
+ const needed = Math.ceil(sourceFiles.length * minCoverage) - testFiles.length;
259
+ issues.push({
260
+ type: "low-testability",
261
+ dimension: "test-coverage",
262
+ severity: actualRatio === 0 ? "critical" : "major",
263
+ message: `Test ratio is ${Math.round(actualRatio * 100)}% (${testFiles.length} test files for ${sourceFiles.length} source files). Need at least ${Math.round(minCoverage * 100)}%.`,
264
+ location: { file: options.rootDir, line: 0 },
265
+ suggestion: `Add ~${needed} test file(s) to reach the ${Math.round(minCoverage * 100)}% minimum for safe AI assistance.`
266
+ });
267
+ }
268
+ if (indexResult.dimensions.purityScore < 50) {
269
+ issues.push({
270
+ type: "low-testability",
271
+ dimension: "purity",
272
+ severity: "major",
273
+ message: `Only ${indexResult.dimensions.purityScore}% of functions are pure \u2014 side-effectful functions require complex test setup.`,
274
+ location: { file: options.rootDir, line: 0 },
275
+ suggestion: "Extract pure transformation logic from I/O and mutation code."
276
+ });
277
+ }
278
+ if (indexResult.dimensions.observabilityScore < 50) {
279
+ issues.push({
280
+ type: "low-testability",
281
+ dimension: "observability",
282
+ severity: "major",
283
+ message: `Many functions mutate external state directly \u2014 outputs are invisible to unit tests.`,
284
+ location: { file: options.rootDir, line: 0 },
285
+ suggestion: "Prefer returning values over mutating shared state."
286
+ });
287
+ }
288
+ return {
289
+ summary: {
290
+ sourceFiles: sourceFiles.length,
291
+ testFiles: testFiles.length,
292
+ coverageRatio: Math.round(actualRatio * 100) / 100,
293
+ score: indexResult.score,
294
+ rating: indexResult.rating,
295
+ aiChangeSafetyRating: indexResult.aiChangeSafetyRating,
296
+ dimensions: indexResult.dimensions
297
+ },
298
+ issues,
299
+ rawData: {
300
+ sourceFiles: sourceFiles.length,
301
+ testFiles: testFiles.length,
302
+ ...aggregated,
303
+ hasTestFramework
304
+ },
305
+ recommendations: indexResult.recommendations
306
+ };
307
+ }
308
+
309
+ // src/scoring.ts
310
+ function calculateTestabilityScore(report) {
311
+ const { summary, rawData, recommendations } = report;
312
+ const factors = [
313
+ {
314
+ name: "Test Coverage",
315
+ impact: Math.round(summary.dimensions.testCoverageRatio - 50),
316
+ description: `${rawData.testFiles} test files / ${rawData.sourceFiles} source files (${Math.round(summary.coverageRatio * 100)}%)`
317
+ },
318
+ {
319
+ name: "Function Purity",
320
+ impact: Math.round(summary.dimensions.purityScore - 50),
321
+ description: `${rawData.pureFunctions}/${rawData.totalFunctions} functions are pure`
322
+ },
323
+ {
324
+ name: "Dependency Injection",
325
+ impact: Math.round(summary.dimensions.dependencyInjectionScore - 50),
326
+ description: `${rawData.injectionPatterns}/${rawData.totalClasses} classes use DI`
327
+ },
328
+ {
329
+ name: "Interface Focus",
330
+ impact: Math.round(summary.dimensions.interfaceFocusScore - 50),
331
+ description: `${rawData.bloatedInterfaces} interfaces have >10 methods`
332
+ },
333
+ {
334
+ name: "Observability",
335
+ impact: Math.round(summary.dimensions.observabilityScore - 50),
336
+ description: `${rawData.externalStateMutations} functions mutate external state`
337
+ }
338
+ ];
339
+ const recs = recommendations.map(
340
+ (action) => ({
341
+ action,
342
+ estimatedImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
343
+ priority: summary.aiChangeSafetyRating === "blind-risk" || summary.aiChangeSafetyRating === "high-risk" ? "high" : "medium"
344
+ })
345
+ );
346
+ return {
347
+ toolName: "testability",
348
+ score: summary.score,
349
+ rawMetrics: {
350
+ ...rawData,
351
+ rating: summary.rating,
352
+ aiChangeSafetyRating: summary.aiChangeSafetyRating,
353
+ coverageRatio: summary.coverageRatio
354
+ },
355
+ factors,
356
+ recommendations: recs
357
+ };
358
+ }
359
+
360
+ export {
361
+ analyzeTestability,
362
+ calculateTestabilityScore
363
+ };
package/dist/cli.js CHANGED
@@ -32,7 +32,14 @@ var import_path = require("path");
32
32
  var import_typescript_estree = require("@typescript-eslint/typescript-estree");
33
33
  var import_core = require("@aiready/core");
34
34
  var SRC_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
35
- var DEFAULT_EXCLUDES = ["node_modules", "dist", ".git", "coverage", ".turbo", "build"];
35
+ var DEFAULT_EXCLUDES = [
36
+ "node_modules",
37
+ "dist",
38
+ ".git",
39
+ "coverage",
40
+ ".turbo",
41
+ "build"
42
+ ];
36
43
  var TEST_PATTERNS = [
37
44
  /\.(test|spec)\.(ts|tsx|js|jsx)$/,
38
45
  /__tests__\//,
@@ -108,8 +115,12 @@ function isPureFunction(fn) {
108
115
  let hasSideEffect = false;
109
116
  function walk(node) {
110
117
  if (node.type === "ReturnStatement" && node.argument) hasReturn = true;
111
- if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression") hasSideEffect = true;
112
- 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;
118
+ if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
119
+ hasSideEffect = true;
120
+ if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && ["console", "process", "window", "document", "fs"].includes(
121
+ node.callee.object.name
122
+ ))
123
+ hasSideEffect = true;
113
124
  for (const key of Object.keys(node)) {
114
125
  if (key === "parent") continue;
115
126
  const child = node[key];
@@ -133,7 +144,8 @@ function hasExternalStateMutation(fn) {
133
144
  let found = false;
134
145
  function walk(node) {
135
146
  if (found) return;
136
- if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression") found = true;
147
+ if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression")
148
+ found = true;
137
149
  for (const key of Object.keys(node)) {
138
150
  if (key === "parent") continue;
139
151
  const child = node[key];
@@ -208,7 +220,16 @@ function detectTestFramework(rootDir) {
208
220
  ...pkg.dependencies ?? {},
209
221
  ...pkg.devDependencies ?? {}
210
222
  };
211
- const testFrameworks = ["jest", "vitest", "mocha", "jasmine", "ava", "tap", "pytest", "unittest"];
223
+ const testFrameworks = [
224
+ "jest",
225
+ "vitest",
226
+ "mocha",
227
+ "jasmine",
228
+ "ava",
229
+ "tap",
230
+ "pytest",
231
+ "unittest"
232
+ ];
212
233
  return testFrameworks.some((fw) => allDeps[fw]);
213
234
  } catch {
214
235
  return false;
@@ -216,7 +237,9 @@ function detectTestFramework(rootDir) {
216
237
  }
217
238
  async function analyzeTestability(options) {
218
239
  const allFiles = collectFiles(options.rootDir, options);
219
- const sourceFiles = allFiles.filter((f) => !isTestFile(f, options.testPatterns));
240
+ const sourceFiles = allFiles.filter(
241
+ (f) => !isTestFile(f, options.testPatterns)
242
+ );
220
243
  const testFiles = allFiles.filter((f) => isTestFile(f, options.testPatterns));
221
244
  const aggregated = {
222
245
  pureFunctions: 0,
@@ -341,11 +364,13 @@ function calculateTestabilityScore(report) {
341
364
  description: `${rawData.externalStateMutations} functions mutate external state`
342
365
  }
343
366
  ];
344
- const recs = recommendations.map((action) => ({
345
- action,
346
- estimatedImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
347
- priority: summary.aiChangeSafetyRating === "blind-risk" || summary.aiChangeSafetyRating === "high-risk" ? "high" : "medium"
348
- }));
367
+ const recs = recommendations.map(
368
+ (action) => ({
369
+ action,
370
+ estimatedImpact: summary.aiChangeSafetyRating === "blind-risk" ? 15 : 8,
371
+ priority: summary.aiChangeSafetyRating === "blind-risk" || summary.aiChangeSafetyRating === "high-risk" ? "high" : "medium"
372
+ })
373
+ );
349
374
  return {
350
375
  toolName: "testability",
351
376
  score: summary.score,
@@ -366,7 +391,11 @@ var import_fs2 = require("fs");
366
391
  var import_path2 = require("path");
367
392
  var import_core2 = require("@aiready/core");
368
393
  var program = new import_commander.Command();
369
- program.name("aiready-testability").description("Measure how safely AI-generated changes can be verified in your codebase").version("0.1.0").addHelpText("after", `
394
+ program.name("aiready-testability").description(
395
+ "Measure how safely AI-generated changes can be verified in your codebase"
396
+ ).version("0.1.0").addHelpText(
397
+ "after",
398
+ `
370
399
  DIMENSIONS MEASURED:
371
400
  Test Coverage Ratio of test files to source files
372
401
  Function Purity Pure functions are trivially AI-testable
@@ -384,7 +413,15 @@ EXAMPLES:
384
413
  aiready-testability . # Full analysis
385
414
  aiready-testability src/ --output json # JSON report
386
415
  aiready-testability . --min-coverage 0.5 # Stricter 50% threshold
387
- `).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) => {
416
+ `
417
+ ).argument("<directory>", "Directory to analyze").option(
418
+ "--min-coverage <ratio>",
419
+ "Minimum acceptable test/source ratio (default: 0.3)",
420
+ "0.3"
421
+ ).option(
422
+ "--test-patterns <patterns>",
423
+ "Additional test file patterns (comma-separated)"
424
+ ).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) => {
388
425
  console.log(import_chalk.default.blue("\u{1F9EA} Analyzing testability...\n"));
389
426
  const startTime = Date.now();
390
427
  const config = await (0, import_core2.loadConfig)(directory);
@@ -453,18 +490,32 @@ function displayConsoleReport(report, scoring, elapsed) {
453
490
  const safetyRating = summary.aiChangeSafetyRating;
454
491
  console.log(import_chalk.default.bold("\n\u{1F9EA} Testability Analysis\n"));
455
492
  if (safetyRating === "blind-risk") {
456
- console.log(import_chalk.default.bgRed.white.bold(
457
- " \u{1F480} BLIND RISK \u2014 NO TESTS DETECTED. AI-GENERATED CHANGES CANNOT BE VERIFIED. "
458
- ));
493
+ console.log(
494
+ import_chalk.default.bgRed.white.bold(
495
+ " \u{1F480} BLIND RISK \u2014 NO TESTS DETECTED. AI-GENERATED CHANGES CANNOT BE VERIFIED. "
496
+ )
497
+ );
459
498
  console.log();
460
499
  } else if (safetyRating === "high-risk") {
461
- console.log(import_chalk.default.red.bold(` \u{1F534} HIGH RISK \u2014 Insufficient test coverage. AI changes may introduce silent bugs.`));
500
+ console.log(
501
+ import_chalk.default.red.bold(
502
+ ` \u{1F534} HIGH RISK \u2014 Insufficient test coverage. AI changes may introduce silent bugs.`
503
+ )
504
+ );
462
505
  console.log();
463
506
  }
464
- console.log(`AI Change Safety: ${safetyColor(safetyRating)(`${safetyIcon(safetyRating)} ${safetyRating.toUpperCase()}`)}`);
465
- console.log(`Score: ${import_chalk.default.bold(summary.score + "/100")} (${summary.rating})`);
466
- console.log(`Source Files: ${import_chalk.default.cyan(rawData.sourceFiles)} Test Files: ${import_chalk.default.cyan(rawData.testFiles)}`);
467
- console.log(`Coverage Ratio: ${import_chalk.default.bold(Math.round(summary.coverageRatio * 100) + "%")}`);
507
+ console.log(
508
+ `AI Change Safety: ${safetyColor(safetyRating)(`${safetyIcon(safetyRating)} ${safetyRating.toUpperCase()}`)}`
509
+ );
510
+ console.log(
511
+ `Score: ${import_chalk.default.bold(summary.score + "/100")} (${summary.rating})`
512
+ );
513
+ console.log(
514
+ `Source Files: ${import_chalk.default.cyan(rawData.sourceFiles)} Test Files: ${import_chalk.default.cyan(rawData.testFiles)}`
515
+ );
516
+ console.log(
517
+ `Coverage Ratio: ${import_chalk.default.bold(Math.round(summary.coverageRatio * 100) + "%")}`
518
+ );
468
519
  console.log(`Analysis Time: ${import_chalk.default.gray(elapsed + "s")}
469
520
  `);
470
521
  console.log(import_chalk.default.bold("\u{1F4D0} Dimension Scores\n"));
@@ -484,7 +535,10 @@ function displayConsoleReport(report, scoring, elapsed) {
484
535
  for (const issue of issues) {
485
536
  const sev = issue.severity === "critical" ? import_chalk.default.red : issue.severity === "major" ? import_chalk.default.yellow : import_chalk.default.blue;
486
537
  console.log(`${sev(issue.severity.toUpperCase())} ${issue.message}`);
487
- if (issue.suggestion) console.log(` ${import_chalk.default.dim("\u2192")} ${import_chalk.default.italic(issue.suggestion)}`);
538
+ if (issue.suggestion)
539
+ console.log(
540
+ ` ${import_chalk.default.dim("\u2192")} ${import_chalk.default.italic(issue.suggestion)}`
541
+ );
488
542
  console.log();
489
543
  }
490
544
  }