docguard-cli 0.9.0 → 0.9.1

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.
@@ -405,38 +405,77 @@ function calcDocQualityScore(dir, config) {
405
405
  function calcTestingScore(dir, config) {
406
406
  let score = 0;
407
407
 
408
- // Check test directory exists
408
+ // ── Check 1: Test files exist (40 pts) ──
409
+ // Check top-level test directories
409
410
  const testDirs = ['tests', 'test', '__tests__', 'spec', 'e2e'];
410
- const hasTestDir = testDirs.some(d => existsSync(resolve(dir, d)));
411
- if (hasTestDir) score += 40;
411
+ const hasTopLevelTestDir = testDirs.some(d => existsSync(resolve(dir, d)));
412
412
 
413
- // Check test spec exists
413
+ // Check co-located tests: src/**/__tests__/ and src/**/*.test.* / src/**/*.spec.*
414
+ let hasColocatedTests = false;
415
+ if (!hasTopLevelTestDir) {
416
+ hasColocatedTests = findColocatedTests(dir);
417
+ }
418
+
419
+ // Check vitest/jest config for custom test patterns
420
+ let hasConfigTests = false;
421
+ if (!hasTopLevelTestDir && !hasColocatedTests) {
422
+ const testConfigs = ['vitest.config.ts', 'vitest.config.js', 'vitest.config.mts', 'jest.config.ts', 'jest.config.js'];
423
+ for (const cfgFile of testConfigs) {
424
+ const cfgPath = resolve(dir, cfgFile);
425
+ if (existsSync(cfgPath)) {
426
+ try {
427
+ const cfgContent = readFileSync(cfgPath, 'utf-8');
428
+ const includeMatch = cfgContent.match(/include\s*:\s*\[([^\]]+)\]/);
429
+ if (includeMatch) {
430
+ const patterns = includeMatch[1].match(/['"]([^'"]+)['"]/g);
431
+ if (patterns) {
432
+ for (const p of patterns) {
433
+ const pattern = p.replace(/['"]|\s/g, '');
434
+ const rootDir = pattern.split('/')[0];
435
+ if (rootDir && rootDir !== '**' && rootDir !== '*') {
436
+ const fullDir = resolve(dir, rootDir);
437
+ if (existsSync(fullDir)) {
438
+ hasConfigTests = true;
439
+ break;
440
+ }
441
+ }
442
+ }
443
+ }
444
+ }
445
+ } catch { /* config parse may fail */ }
446
+ break;
447
+ }
448
+ }
449
+ }
450
+
451
+ if (hasTopLevelTestDir || hasColocatedTests || hasConfigTests) score += 40;
452
+
453
+ // ── Check 2: TEST-SPEC.md exists (30 pts) ──
414
454
  if (existsSync(resolve(dir, 'docs-canonical/TEST-SPEC.md'))) score += 30;
415
455
 
416
- // Check for test config files OR built-in test runner
417
- const testConfigs = ['jest.config.js', 'jest.config.ts', 'vitest.config.ts', 'vitest.config.js', 'pytest.ini', 'setup.cfg', '.mocharc.yml'];
418
- const hasTestConfig = testConfigs.some(f => existsSync(resolve(dir, f)));
456
+ // ── Check 3: Test config or built-in runner (15 pts) ──
457
+ const testConfigs2 = ['jest.config.js', 'jest.config.ts', 'vitest.config.ts', 'vitest.config.js', 'pytest.ini', 'setup.cfg', '.mocharc.yml'];
458
+ const hasTestConfig = testConfigs2.some(f => existsSync(resolve(dir, f)));
419
459
 
420
460
  if (hasTestConfig) {
421
461
  score += 15;
422
462
  } else {
423
- // Check if using node:test (no config needed) — look in package.json scripts
424
463
  const ptc = config.projectTypeConfig || {};
425
464
  const pkgPath = resolve(dir, 'package.json');
426
465
  if (ptc.testFramework === 'node:test') {
427
- score += 15; // Config says node:test — no config file needed
466
+ score += 15;
428
467
  } else if (existsSync(pkgPath)) {
429
468
  try {
430
469
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
431
470
  const testScript = pkg.scripts?.test || '';
432
471
  if (testScript.includes('node --test') || testScript.includes('node:test')) {
433
- score += 15; // Using built-in test runner
472
+ score += 15;
434
473
  }
435
474
  } catch { /* skip */ }
436
475
  }
437
476
  }
438
477
 
439
- // Check for CI test step
478
+ // ── Check 4: CI test step (15 pts) ──
440
479
  const ciFiles = ['.github/workflows/ci.yml', '.github/workflows/test.yml'];
441
480
  const hasCITest = ciFiles.some(f => existsSync(resolve(dir, f)));
442
481
  if (hasCITest) score += 15;
@@ -444,6 +483,53 @@ function calcTestingScore(dir, config) {
444
483
  return Math.min(100, score);
445
484
  }
446
485
 
486
+ /**
487
+ * Scan common source directories for co-located test files.
488
+ * Checks: __tests__/ dirs, *.test.*, *.spec.* anywhere in src/, app/, lib/, packages/
489
+ * Also checks root-level for *.test.* files.
490
+ */
491
+ function findColocatedTests(dir) {
492
+ // Scan these common source roots for co-located tests
493
+ const sourceRoots = ['src', 'app', 'lib', 'packages', 'modules'];
494
+ const ignoreSet = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.cache']);
495
+
496
+ for (const root of sourceRoots) {
497
+ const rootDir = resolve(dir, root);
498
+ if (!existsSync(rootDir)) continue;
499
+ if (walkForTests(rootDir, ignoreSet)) return true;
500
+ }
501
+
502
+ // Also check root-level for *.test.* files (some projects put tests at root)
503
+ try {
504
+ const rootEntries = readdirSync(dir);
505
+ for (const entry of rootEntries) {
506
+ if (/\.(test|spec)\.[^.]+$/.test(entry)) return true;
507
+ }
508
+ } catch { /* ignore */ }
509
+
510
+ return false;
511
+ }
512
+
513
+ /** Recursively walk a dir looking for test files. Returns true as soon as one is found. */
514
+ function walkForTests(d, ignoreSet) {
515
+ let entries;
516
+ try { entries = readdirSync(d); } catch { return false; }
517
+ for (const entry of entries) {
518
+ if (ignoreSet.has(entry) || entry.startsWith('.')) continue;
519
+ const full = join(d, entry);
520
+ try {
521
+ const s = statSync(full);
522
+ if (s.isDirectory()) {
523
+ if (entry === '__tests__' || entry === '__test__') return true;
524
+ if (walkForTests(full, ignoreSet)) return true;
525
+ } else {
526
+ if (/\.(test|spec)\.[^.]+$/.test(entry)) return true;
527
+ }
528
+ } catch { continue; }
529
+ }
530
+ return false;
531
+ }
532
+
447
533
  function calcSecurityScore(dir, config) {
448
534
  let score = 0;
449
535
  const ptc = config.projectTypeConfig || {};
@@ -3,7 +3,7 @@
3
3
  * Now respects projectTypeConfig (e.g., skip E2E for CLI tools)
4
4
  */
5
5
 
6
- import { existsSync, readFileSync } from 'node:fs';
6
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
8
 
9
9
  export function validateTestSpec(projectDir, config) {
@@ -130,19 +130,66 @@ export function validateTestSpec(projectDir, config) {
130
130
  }
131
131
  }
132
132
 
133
- // If no test spec entries parsed, check if test directory exists
133
+ // If no test spec entries parsed, check if tests exist anywhere
134
134
  if (results.total === 0) {
135
135
  results.total = 1;
136
+
137
+ // 1. Check top-level test dirs
136
138
  const commonTestDirs = ['tests', 'test', '__tests__', 'spec'];
137
139
  const hasTestDir = commonTestDirs.some(d =>
138
140
  existsSync(resolve(projectDir, d))
139
141
  );
140
- if (hasTestDir) {
142
+
143
+ // 2. Check co-located tests (src/**/__tests__/, src/**/*.test.*)
144
+ let hasColocated = false;
145
+ if (!hasTestDir) {
146
+ const sourceRoots = ['src', 'app', 'lib', 'packages'];
147
+ for (const root of sourceRoots) {
148
+ const rootPath = resolve(projectDir, root);
149
+ if (existsSync(rootPath) && hasTestFilesRecursive(rootPath)) {
150
+ hasColocated = true;
151
+ break;
152
+ }
153
+ }
154
+ }
155
+
156
+ // 3. Check vitest/jest config for custom patterns
157
+ let hasConfigTests = false;
158
+ if (!hasTestDir && !hasColocated) {
159
+ const configs = ['vitest.config.ts', 'vitest.config.js', 'jest.config.ts', 'jest.config.js'];
160
+ hasConfigTests = configs.some(f => existsSync(resolve(projectDir, f)));
161
+ }
162
+
163
+ if (hasTestDir || hasColocated || hasConfigTests) {
141
164
  results.passed = 1;
142
165
  } else {
143
- results.warnings.push('No test directory found (expected: tests/, test/, __tests__/)');
166
+ results.warnings.push(
167
+ 'No test directory or co-located test files found. ' +
168
+ 'Expected: tests/, src/**/__tests__/, or src/**/*.test.* files'
169
+ );
144
170
  }
145
171
  }
146
172
 
147
173
  return results;
148
174
  }
175
+
176
+ /** Recursively check if a directory contains test files */
177
+ function hasTestFilesRecursive(dir) {
178
+ const ignore = new Set(['node_modules', '.git', 'dist', 'build', 'coverage']);
179
+ let entries;
180
+ try { entries = readdirSync(dir); } catch { return false; }
181
+ for (const entry of entries) {
182
+ if (ignore.has(entry) || entry.startsWith('.')) continue;
183
+ const full = resolve(dir, entry);
184
+ try {
185
+ const s = statSync(full);
186
+ if (s.isDirectory()) {
187
+ if (entry === '__tests__' || entry === '__test__') return true;
188
+ if (hasTestFilesRecursive(full)) return true;
189
+ } else if (/\.(test|spec)\.[^.]+$/.test(entry)) {
190
+ return true;
191
+ }
192
+ } catch { continue; }
193
+ }
194
+ return false;
195
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "The enforcement tool for Canonical-Driven Development (CDD). Audit, generate, and guard your project documentation.",
5
5
  "type": "module",
6
6
  "bin": {