@paths.design/caws-cli 2.0.1 → 3.0.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.
Files changed (50) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +101 -96
  3. package/package.json +3 -2
  4. package/templates/agents.md +820 -0
  5. package/templates/apps/tools/caws/COMPLETION_REPORT.md +331 -0
  6. package/templates/apps/tools/caws/MIGRATION_SUMMARY.md +360 -0
  7. package/templates/apps/tools/caws/README.md +463 -0
  8. package/templates/apps/tools/caws/TEST_STATUS.md +365 -0
  9. package/templates/apps/tools/caws/attest.js +357 -0
  10. package/templates/apps/tools/caws/ci-optimizer.js +642 -0
  11. package/templates/apps/tools/caws/config.ts +245 -0
  12. package/templates/apps/tools/caws/cross-functional.js +876 -0
  13. package/templates/apps/tools/caws/dashboard.js +1112 -0
  14. package/templates/apps/tools/caws/flake-detector.ts +362 -0
  15. package/templates/apps/tools/caws/gates.js +198 -0
  16. package/templates/apps/tools/caws/gates.ts +237 -0
  17. package/templates/apps/tools/caws/language-adapters.ts +381 -0
  18. package/templates/apps/tools/caws/language-support.d.ts +367 -0
  19. package/templates/apps/tools/caws/language-support.d.ts.map +1 -0
  20. package/templates/apps/tools/caws/language-support.js +585 -0
  21. package/templates/apps/tools/caws/legacy-assessment.ts +408 -0
  22. package/templates/apps/tools/caws/legacy-assessor.js +764 -0
  23. package/templates/apps/tools/caws/mutant-analyzer.js +734 -0
  24. package/templates/apps/tools/caws/perf-budgets.ts +349 -0
  25. package/templates/apps/tools/caws/property-testing.js +707 -0
  26. package/templates/apps/tools/caws/provenance.d.ts +14 -0
  27. package/templates/apps/tools/caws/provenance.d.ts.map +1 -0
  28. package/templates/apps/tools/caws/provenance.js +132 -0
  29. package/templates/apps/tools/caws/provenance.ts +211 -0
  30. package/templates/apps/tools/caws/schemas/waivers.schema.json +30 -0
  31. package/templates/apps/tools/caws/schemas/working-spec.schema.json +115 -0
  32. package/templates/apps/tools/caws/scope-guard.js +208 -0
  33. package/templates/apps/tools/caws/security-provenance.ts +483 -0
  34. package/templates/apps/tools/caws/shared/base-tool.ts +281 -0
  35. package/templates/apps/tools/caws/shared/config-manager.ts +366 -0
  36. package/templates/apps/tools/caws/shared/gate-checker.ts +597 -0
  37. package/templates/apps/tools/caws/shared/types.ts +444 -0
  38. package/templates/apps/tools/caws/shared/validator.ts +305 -0
  39. package/templates/apps/tools/caws/shared/waivers-manager.ts +174 -0
  40. package/templates/apps/tools/caws/spec-test-mapper.ts +391 -0
  41. package/templates/apps/tools/caws/templates/working-spec.template.yml +60 -0
  42. package/templates/apps/tools/caws/test-quality.js +578 -0
  43. package/templates/apps/tools/caws/tools-allow.json +331 -0
  44. package/templates/apps/tools/caws/validate.js +76 -0
  45. package/templates/apps/tools/caws/validate.ts +228 -0
  46. package/templates/apps/tools/caws/waivers.js +344 -0
  47. package/templates/apps/tools/caws/waivers.yml +19 -0
  48. package/templates/codemod/README.md +1 -0
  49. package/templates/codemod/test.js +1 -0
  50. package/templates/docs/README.md +150 -0
@@ -0,0 +1,578 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @fileoverview CAWS Test Quality Analyzer
5
+ * Analyzes test files for meaningful assertions and quality indicators beyond coverage
6
+ * @author @darianrosebrook
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ /**
13
+ * Test quality scoring criteria
14
+ */
15
+ const QUALITY_CRITERIA = {
16
+ ASSERTION_DENSITY: {
17
+ weight: 0.25,
18
+ description: 'Ratio of assertions to test functions',
19
+ thresholds: { excellent: 0.8, good: 0.6, poor: 0.3 },
20
+ },
21
+ EDGE_CASE_COVERAGE: {
22
+ weight: 0.2,
23
+ description: 'Coverage of edge cases and error conditions',
24
+ thresholds: { excellent: 0.7, good: 0.5, poor: 0.2 },
25
+ },
26
+ DESCRIPTIVE_NAMING: {
27
+ weight: 0.15,
28
+ description: 'Quality of test names and descriptions',
29
+ thresholds: { excellent: 0.8, good: 0.6, poor: 0.3 },
30
+ },
31
+ SETUP_TEARDOWN: {
32
+ weight: 0.1,
33
+ description: 'Proper test setup and teardown',
34
+ thresholds: { excellent: 0.9, good: 0.7, poor: 0.4 },
35
+ },
36
+ MOCKING_QUALITY: {
37
+ weight: 0.15,
38
+ description: 'Appropriate use of mocks and test doubles',
39
+ thresholds: { excellent: 0.8, good: 0.6, poor: 0.3 },
40
+ },
41
+ SPEC_COVERAGE: {
42
+ weight: 0.15,
43
+ description: 'Alignment with acceptance criteria from working spec',
44
+ thresholds: { excellent: 0.9, good: 0.7, poor: 0.4 },
45
+ },
46
+ };
47
+
48
+ /**
49
+ * Analyze a single test file for quality metrics
50
+ * @param {string} filePath - Path to test file
51
+ * @param {Object} _spec - Working specification for spec coverage check
52
+ * @returns {Object} Quality analysis results
53
+ */
54
+ function analyzeTestFile(filePath, _spec = null) {
55
+ let content = '';
56
+ let language = 'javascript';
57
+
58
+ // Determine language from file extension
59
+ const ext = path.extname(filePath);
60
+ if (ext === '.py') {
61
+ language = 'python';
62
+ } else if (ext === '.java') {
63
+ language = 'java';
64
+ } else if (ext === '.js' || ext === '.ts') {
65
+ language = 'javascript';
66
+ }
67
+
68
+ try {
69
+ content = fs.readFileSync(filePath, 'utf8');
70
+ } catch (error) {
71
+ console.warn(`āš ļø Could not read test file: ${filePath}`);
72
+ return null;
73
+ }
74
+
75
+ const lines = content.split('\n');
76
+ const analysis = {
77
+ file: path.basename(filePath),
78
+ language,
79
+ totalLines: lines.length,
80
+ testFunctions: 0,
81
+ assertions: 0,
82
+ edgeCases: 0,
83
+ descriptiveNames: 0,
84
+ properSetup: false,
85
+ properTeardown: false,
86
+ mocksUsed: false,
87
+ specAlignment: 0,
88
+ issues: [],
89
+ };
90
+
91
+ // Language-specific analysis
92
+ switch (language) {
93
+ case 'javascript':
94
+ analyzeJavaScriptTest(content, lines, analysis, _spec);
95
+ break;
96
+ case 'python':
97
+ analyzePythonTest(content, lines, analysis, _spec);
98
+ break;
99
+ case 'java':
100
+ analyzeJavaTest(content, lines, analysis, _spec);
101
+ break;
102
+ }
103
+
104
+ return analysis;
105
+ }
106
+
107
+ /**
108
+ * Analyze JavaScript/TypeScript test file
109
+ */
110
+ function analyzeJavaScriptTest(content, lines, analysis, _spec) {
111
+ // Count test functions (describe/it/test blocks in Jest/Mocha)
112
+ const testPatterns = [/\b(describe|it|test)\s*\(/g, /\btest\s*\(\s*['"`][^'"`]*['"`]/g];
113
+
114
+ testPatterns.forEach((pattern) => {
115
+ const matches = content.match(pattern);
116
+ if (matches) {
117
+ analysis.testFunctions += matches.length;
118
+ }
119
+ });
120
+
121
+ // Count assertions
122
+ const assertionPatterns = [
123
+ /\.toBe\s*\(/g,
124
+ /\.toEqual\s*\(/g,
125
+ /\.toContain\s*\(/g,
126
+ /\.toHaveBeenCalled/g,
127
+ /\.toHaveBeenCalledWith/g,
128
+ /\bexpect\s*\(/g,
129
+ /\bassert\s*\(/g,
130
+ /\bshould\s*\(/g,
131
+ ];
132
+
133
+ assertionPatterns.forEach((pattern) => {
134
+ const matches = content.match(pattern);
135
+ if (matches) {
136
+ analysis.assertions += matches.length;
137
+ }
138
+ });
139
+
140
+ // Check for edge cases (error conditions, null/undefined, boundaries)
141
+ const edgeCasePatterns = [
142
+ /catch\s*\(/g,
143
+ /throw\s+/g,
144
+ /Error\s*\(/g,
145
+ /null\s*,?\s*undefined/g,
146
+ /boundary|edge|limit|extreme/g,
147
+ /invalid|malformed|corrupt/g,
148
+ ];
149
+
150
+ edgeCasePatterns.forEach((pattern) => {
151
+ const matches = content.match(pattern);
152
+ if (matches) {
153
+ analysis.edgeCases += matches.length;
154
+ }
155
+ });
156
+
157
+ // Check for descriptive naming
158
+ const describeBlocks = content.match(/describe\s*\(\s*['"`]([^'"`]+)['"`]/g);
159
+ if (describeBlocks) {
160
+ describeBlocks.forEach((block) => {
161
+ if (block.length > 20 && !/\btest|spec|should\b/.test(block)) {
162
+ analysis.descriptiveNames++;
163
+ }
164
+ });
165
+ }
166
+
167
+ // Check for setup/teardown
168
+ analysis.properSetup = /\bbeforeEach|beforeAll|setup\b/.test(content);
169
+ analysis.properTeardown = /\bafterEach|afterAll|teardown\b/.test(content);
170
+
171
+ // Check for mocking
172
+ analysis.mocksUsed = /\b(mock|spy|stub|jest\.mock|sinon\.)/.test(content);
173
+
174
+ // Check spec alignment if spec provided
175
+ if (_spec && _spec.acceptance) {
176
+ const specKeywords = _spec.acceptance
177
+ .flatMap((ac) => [ac.given, ac.when, ac.then].filter(Boolean))
178
+ .join(' ')
179
+ .toLowerCase();
180
+
181
+ const testContent = content.toLowerCase();
182
+ const matchedTerms = specKeywords
183
+ .split(/\s+/)
184
+ .filter((term) => term.length > 3 && testContent.includes(term)).length;
185
+
186
+ analysis.specAlignment = Math.min(
187
+ matchedTerms / Math.max(specKeywords.split(/\s+/).length, 1),
188
+ 1
189
+ );
190
+ }
191
+
192
+ // Identify issues
193
+ if (analysis.testFunctions > 0 && analysis.assertions / analysis.testFunctions < 0.5) {
194
+ analysis.issues.push('Low assertion density - tests may not be properly validating behavior');
195
+ }
196
+
197
+ if (analysis.edgeCases === 0 && analysis.testFunctions > 3) {
198
+ analysis.issues.push('No edge case testing detected - consider adding error condition tests');
199
+ }
200
+
201
+ if (!analysis.properSetup && analysis.testFunctions > 5) {
202
+ analysis.issues.push('Missing test setup - consider using beforeEach for common setup');
203
+ }
204
+
205
+ if (!analysis.mocksUsed && /\bimport.*from/.test(content)) {
206
+ analysis.issues.push(
207
+ 'External dependencies detected but no mocking - consider adding mocks for better isolation'
208
+ );
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Analyze Python test file
214
+ */
215
+ function analyzePythonTest(content, lines, analysis, _spec) {
216
+ // Count test functions
217
+ const testMatches = content.match(/\bdef\s+test_\w+/g);
218
+ analysis.testFunctions = testMatches ? testMatches.length : 0;
219
+
220
+ // Count assertions
221
+ const assertionPatterns = [
222
+ /\bassert\s+/g,
223
+ /\bself\.assert/g,
224
+ /\bassertEqual\b/g,
225
+ /\bassertTrue\b/g,
226
+ /\bassertFalse\b/g,
227
+ /\bassertRaises\b/g,
228
+ /\bassertIn\b/g,
229
+ /\bassertIsNone\b/g,
230
+ ];
231
+
232
+ assertionPatterns.forEach((pattern) => {
233
+ const matches = content.match(pattern);
234
+ if (matches) {
235
+ analysis.assertions += matches.length;
236
+ }
237
+ });
238
+
239
+ // Check for edge cases
240
+ const edgeCasePatterns = [
241
+ /with\s+pytest\.raises/g,
242
+ /try:\s*except/g,
243
+ /ValueError|TypeError|Exception/g,
244
+ /None\s*,?\s*null/g,
245
+ /boundary|edge|limit|extreme/g,
246
+ /invalid|malformed|corrupt/g,
247
+ ];
248
+
249
+ edgeCasePatterns.forEach((pattern) => {
250
+ const matches = content.match(pattern);
251
+ if (matches) {
252
+ analysis.edgeCases += matches.length;
253
+ }
254
+ });
255
+
256
+ // Check for descriptive naming
257
+ const classMatches = content.match(/class\s+Test\w+/g);
258
+ if (classMatches) {
259
+ classMatches.forEach((className) => {
260
+ if (className.length > 8 && !/\bTest\b/.test(className)) {
261
+ analysis.descriptiveNames++;
262
+ }
263
+ });
264
+ }
265
+
266
+ // Check for setup/teardown
267
+ analysis.properSetup = /\bsetUp\b|\bfixtures?\b/.test(content);
268
+ analysis.properTeardown = /\btearDown\b/.test(content);
269
+
270
+ // Check for mocking
271
+ analysis.mocksUsed = /\b(mocking|patch|MagicMock|Mock)\b/.test(content);
272
+ }
273
+
274
+ /**
275
+ * Analyze Java test file
276
+ */
277
+ function analyzeJavaTest(content, lines, analysis, _spec) {
278
+ // Count test methods
279
+ const testMatches = content.match(/@\w*Test\s*public\s+void\s+\w+/g);
280
+ analysis.testFunctions = testMatches ? testMatches.length : 0;
281
+
282
+ // Count assertions
283
+ const assertionPatterns = [
284
+ /\bassert/g,
285
+ /\bfail\s*\(/g,
286
+ /\bAssert\.assert/g,
287
+ /\bAssertions\.assert/g,
288
+ ];
289
+
290
+ assertionPatterns.forEach((pattern) => {
291
+ const matches = content.match(pattern);
292
+ if (matches) {
293
+ analysis.assertions += matches.length;
294
+ }
295
+ });
296
+
297
+ // Check for edge cases
298
+ const edgeCasePatterns = [
299
+ /@Test.*expected\s*=/g,
300
+ /try\s*{\s*.*\s*}\s*catch/g,
301
+ /Exception|Error/g,
302
+ /null\s*,?\s*boundary/g,
303
+ /invalid|malformed|corrupt/g,
304
+ ];
305
+
306
+ edgeCasePatterns.forEach((pattern) => {
307
+ const matches = content.match(pattern);
308
+ if (matches) {
309
+ analysis.edgeCases += matches.length;
310
+ }
311
+ });
312
+
313
+ // Check for setup/teardown
314
+ analysis.properSetup = /@BeforeEach|@Before/.test(content);
315
+ analysis.properTeardown = /@AfterEach|@After/.test(content);
316
+
317
+ // Check for mocking
318
+ analysis.mocksUsed = /@Mock|Mockito|when\(|verify\(/.test(content);
319
+ }
320
+
321
+ /**
322
+ * Calculate overall quality score for a test file
323
+ * @param {Object} analysis - Test analysis results
324
+ * @returns {number} Quality score (0-100)
325
+ */
326
+ function calculateQualityScore(analysis) {
327
+ if (analysis.testFunctions === 0) return 0;
328
+
329
+ const scores = {};
330
+
331
+ // Assertion density score
332
+ const assertionDensity =
333
+ analysis.testFunctions > 0 ? analysis.assertions / analysis.testFunctions : 0;
334
+ scores.assertionDensity = normalizeScore(
335
+ assertionDensity,
336
+ QUALITY_CRITERIA.ASSERTION_DENSITY.thresholds
337
+ );
338
+
339
+ // Edge case coverage score
340
+ const edgeCaseRatio =
341
+ analysis.testFunctions > 0 ? analysis.edgeCases / analysis.testFunctions : 0;
342
+ scores.edgeCaseCoverage = normalizeScore(
343
+ edgeCaseRatio,
344
+ QUALITY_CRITERIA.EDGE_CASE_COVERAGE.thresholds
345
+ );
346
+
347
+ // Descriptive naming score (simplified)
348
+ const namingScore = analysis.descriptiveNames > 0 ? 1 : 0.5;
349
+ scores.descriptiveNaming = normalizeScore(
350
+ namingScore,
351
+ QUALITY_CRITERIA.DESCRIPTIVE_NAMING.thresholds
352
+ );
353
+
354
+ // Setup/teardown score
355
+ const setupScore = (analysis.properSetup ? 0.5 : 0) + (analysis.properTeardown ? 0.5 : 0);
356
+ scores.setupTeardown = normalizeScore(setupScore, QUALITY_CRITERIA.SETUP_TEARDOWN.thresholds);
357
+
358
+ // Mocking quality score
359
+ scores.mockingQuality = analysis.mocksUsed
360
+ ? normalizeScore(0.8, QUALITY_CRITERIA.MOCKING_QUALITY.thresholds)
361
+ : normalizeScore(0.3, QUALITY_CRITERIA.MOCKING_QUALITY.thresholds);
362
+
363
+ // Spec alignment score
364
+ scores.specCoverage = normalizeScore(
365
+ analysis.specAlignment,
366
+ QUALITY_CRITERIA.SPEC_COVERAGE.thresholds
367
+ );
368
+
369
+ // Calculate weighted score
370
+ let totalScore = 0;
371
+ Object.keys(QUALITY_CRITERIA).forEach((criterion) => {
372
+ totalScore += scores[criterion.toLowerCase()] * QUALITY_CRITERIA[criterion].weight;
373
+ });
374
+
375
+ return Math.round(totalScore * 100);
376
+ }
377
+
378
+ /**
379
+ * Normalize a raw score to 0-1 scale based on thresholds
380
+ */
381
+ function normalizeScore(value, thresholds) {
382
+ if (value >= thresholds.excellent) return 1.0;
383
+ if (value >= thresholds.good) return 0.8;
384
+ if (value >= thresholds.poor) return 0.5;
385
+ return 0.2;
386
+ }
387
+
388
+ /**
389
+ * Analyze all test files in a directory
390
+ * @param {string} testDir - Directory containing test files
391
+ * @param {Object} _spec - Working specification
392
+ * @returns {Object} Analysis summary
393
+ */
394
+ function analyzeTestDirectory(testDir, _spec = null) {
395
+ const results = {
396
+ files: [],
397
+ summary: {
398
+ totalFiles: 0,
399
+ totalTests: 0,
400
+ totalAssertions: 0,
401
+ averageQualityScore: 0,
402
+ issues: [],
403
+ },
404
+ };
405
+
406
+ try {
407
+ const files = fs.readdirSync(testDir);
408
+
409
+ files.forEach((file) => {
410
+ const filePath = path.join(testDir, file);
411
+ const stat = fs.statSync(filePath);
412
+
413
+ if (stat.isFile() && /\.(test|spec)\.(js|ts|py|java)$/.test(file)) {
414
+ const analysis = analyzeTestFile(filePath, _spec);
415
+ if (analysis) {
416
+ const qualityScore = calculateQualityScore(analysis);
417
+ analysis.qualityScore = qualityScore;
418
+
419
+ results.files.push(analysis);
420
+ results.summary.totalFiles++;
421
+ results.summary.totalTests += analysis.testFunctions;
422
+ results.summary.totalAssertions += analysis.assertions;
423
+
424
+ if (analysis.issues.length > 0) {
425
+ results.summary.issues.push(...analysis.issues.map((issue) => `${file}: ${issue}`));
426
+ }
427
+ }
428
+ }
429
+ });
430
+
431
+ // Calculate average quality score
432
+ if (results.files.length > 0) {
433
+ const totalScore = results.files.reduce((sum, file) => sum + file.qualityScore, 0);
434
+ results.summary.averageQualityScore = Math.round(totalScore / results.files.length);
435
+ }
436
+ } catch (error) {
437
+ console.error(`āŒ Error analyzing test directory: ${error.message}`);
438
+ }
439
+
440
+ return results;
441
+ }
442
+
443
+ /**
444
+ * Generate recommendations based on analysis
445
+ * @param {Object} results - Analysis results
446
+ * @returns {Array} Recommendations
447
+ */
448
+ function generateRecommendations(results) {
449
+ const recommendations = [];
450
+
451
+ if (results.summary.averageQualityScore < 70) {
452
+ recommendations.push({
453
+ type: 'critical',
454
+ message:
455
+ 'Overall test quality is below acceptable threshold. Consider improving test meaningfulness.',
456
+ suggestions: [
457
+ 'Add more assertions per test function',
458
+ 'Include edge case and error condition testing',
459
+ 'Improve test naming for better clarity',
460
+ 'Ensure proper setup/teardown procedures',
461
+ ],
462
+ });
463
+ }
464
+
465
+ if (results.summary.totalAssertions / Math.max(results.summary.totalTests, 1) < 0.5) {
466
+ recommendations.push({
467
+ type: 'warning',
468
+ message: 'Low assertion density detected across tests.',
469
+ suggestions: [
470
+ 'Each test should validate expected behavior with assertions',
471
+ 'Avoid tests that only check if code runs without errors',
472
+ 'Add assertions for return values, side effects, and state changes',
473
+ ],
474
+ });
475
+ }
476
+
477
+ const filesWithoutEdgeCases = results.files.filter(
478
+ (f) => f.edgeCases === 0 && f.testFunctions > 2
479
+ );
480
+ if (filesWithoutEdgeCases.length > 0) {
481
+ recommendations.push({
482
+ type: 'info',
483
+ message: `${filesWithoutEdgeCases.length} test file(s) lack edge case coverage.`,
484
+ suggestions: [
485
+ 'Add tests for null/undefined inputs',
486
+ 'Test boundary conditions and error scenarios',
487
+ 'Include tests for invalid or malformed data',
488
+ ],
489
+ });
490
+ }
491
+
492
+ return recommendations;
493
+ }
494
+
495
+ // CLI interface
496
+ if (require.main === module) {
497
+ const command = process.argv[2];
498
+ const testDir = process.argv[3] || 'tests';
499
+
500
+ switch (command) {
501
+ case 'analyze':
502
+ console.log(`šŸ” Analyzing test quality in: ${testDir}`);
503
+
504
+ // Try to load working spec for spec alignment check
505
+ let spec = null;
506
+ const specPath = '.caws/working-spec.yaml';
507
+ if (fs.existsSync(specPath)) {
508
+ try {
509
+ const yaml = require('js-yaml');
510
+ spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
511
+ console.log('āœ… Loaded working spec for alignment analysis');
512
+ } catch (error) {
513
+ console.warn('āš ļø Could not load working spec for alignment analysis');
514
+ }
515
+ }
516
+
517
+ const results = analyzeTestDirectory(testDir, spec);
518
+
519
+ console.log('\nšŸ“Š Test Quality Analysis Results:');
520
+ console.log(` Files analyzed: ${results.summary.totalFiles}`);
521
+ console.log(` Test functions: ${results.summary.totalTests}`);
522
+ console.log(` Total assertions: ${results.summary.totalAssertions}`);
523
+ console.log(` Average quality score: ${results.summary.averageQualityScore}/100`);
524
+
525
+ if (results.files.length > 0) {
526
+ console.log('\nšŸ“‹ File-by-file breakdown:');
527
+ results.files.forEach((file) => {
528
+ console.log(
529
+ ` ${file.file}: ${file.qualityScore}/100 (${file.testFunctions} tests, ${file.assertions} assertions)`
530
+ );
531
+ });
532
+ }
533
+
534
+ if (results.summary.issues.length > 0) {
535
+ console.log('\nāš ļø Issues found:');
536
+ results.summary.issues.forEach((issue) => {
537
+ console.log(` - ${issue}`);
538
+ });
539
+ }
540
+
541
+ const recommendations = generateRecommendations(results);
542
+ if (recommendations.length > 0) {
543
+ console.log('\nšŸ’” Recommendations:');
544
+ recommendations.forEach((rec) => {
545
+ console.log(` [${rec.type.toUpperCase()}] ${rec.message}`);
546
+ rec.suggestions.forEach((suggestion) => {
547
+ console.log(` • ${suggestion}`);
548
+ });
549
+ });
550
+ }
551
+
552
+ // Exit with error code if quality is poor
553
+ if (results.summary.averageQualityScore < 70) {
554
+ console.error('\nāŒ Test quality below acceptable threshold');
555
+ process.exit(1);
556
+ }
557
+
558
+ break;
559
+
560
+ default:
561
+ console.log('CAWS Test Quality Analyzer');
562
+ console.log('Usage:');
563
+ console.log(' node test-quality.js analyze [test-directory]');
564
+ console.log('');
565
+ console.log('Examples:');
566
+ console.log(' node test-quality.js analyze tests/unit');
567
+ console.log(' node test-quality.js analyze tests/');
568
+ process.exit(1);
569
+ }
570
+ }
571
+
572
+ module.exports = {
573
+ analyzeTestFile,
574
+ analyzeTestDirectory,
575
+ calculateQualityScore,
576
+ generateRecommendations,
577
+ QUALITY_CRITERIA,
578
+ };