docguard-cli 0.9.10 → 0.9.11

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.
@@ -494,8 +494,30 @@ function calcTestingScore(dir, config) {
494
494
  }
495
495
 
496
496
  // ── Check 4: CI test step (15 pts) ──
497
- const ciFiles = ['.github/workflows/ci.yml', '.github/workflows/test.yml'];
498
- const hasCITest = ciFiles.some(f => existsSync(resolve(dir, f)));
497
+ // Support multiple CI systems — not just GitHub Actions
498
+ const ciFiles = [
499
+ '.github/workflows/ci.yml', '.github/workflows/test.yml',
500
+ '.github/workflows/ci.yaml', '.github/workflows/test.yaml',
501
+ 'buildspec.yml', 'buildspec.test.yml', // AWS CodeBuild
502
+ 'amplify.yml', // AWS Amplify
503
+ 'Jenkinsfile', // Jenkins
504
+ '.circleci/config.yml', // CircleCI
505
+ '.gitlab-ci.yml', // GitLab CI
506
+ '.travis.yml', // Travis CI
507
+ ];
508
+ let hasCITest = ciFiles.some(f => existsSync(resolve(dir, f)));
509
+
510
+ // Also check turbo.json for "test" pipeline task
511
+ if (!hasCITest) {
512
+ const turboPath = resolve(dir, 'turbo.json');
513
+ if (existsSync(turboPath)) {
514
+ try {
515
+ const turboContent = readFileSync(turboPath, 'utf-8');
516
+ if (/"test"/.test(turboContent)) hasCITest = true;
517
+ } catch { /* skip */ }
518
+ }
519
+ }
520
+
499
521
  if (hasCITest) score += 15;
500
522
 
501
523
  return Math.min(100, score);
@@ -74,3 +74,46 @@ export function shouldIgnore(relPath, config, validatorKey) {
74
74
 
75
75
  return false;
76
76
  }
77
+
78
+ /**
79
+ * Convert a glob pattern to a RegExp for POSITIVE matching.
80
+ * Unlike globToRegex (used for ignore filtering), this anchors the match
81
+ * to the full relative path from the project root.
82
+ *
83
+ * Supports: * (any chars except /), ** (any path segments), . (literal dot).
84
+ *
85
+ * @param {string} pattern - Glob pattern (e.g., "backend/**\/__tests__/**\/*.test.ts")
86
+ * @returns {RegExp}
87
+ */
88
+ function globToMatchRegex(pattern) {
89
+ // Normalize: replace **/ with a placeholder that means "zero or more path segments"
90
+ let escaped = pattern
91
+ .replace(/\./g, '\\.')
92
+ .replace(/\*\*\//g, '§STARSTAR§') // **/ → zero-or-more segments
93
+ .replace(/\*\*/g, '.*') // standalone ** → any chars
94
+ .replace(/\*/g, '[^/]*') // single * → any chars except /
95
+ .replace(/§STARSTAR§/g, '(.*/)?'); // **/ → optional path prefix
96
+ return new RegExp(`^${escaped}$`);
97
+ }
98
+
99
+ /**
100
+ * Check if a relative path matches ANY of the given glob patterns.
101
+ * Purpose-built for POSITIVE matching (e.g., "is this a test file?").
102
+ *
103
+ * ALWAYS rejects paths containing node_modules at any depth.
104
+ * This is the correct function for test file discovery — do NOT use
105
+ * buildIgnoreFilter() for this purpose.
106
+ *
107
+ * @param {string} relPath - Relative path from project root
108
+ * @param {string[]} patterns - Array of glob patterns to match against
109
+ * @returns {boolean} - true if path matches a pattern AND is not in node_modules
110
+ */
111
+ export function globMatch(relPath, patterns) {
112
+ if (!relPath || !patterns || patterns.length === 0) return false;
113
+
114
+ // Always reject paths containing node_modules at any depth
115
+ if (/(?:^|[/\\])node_modules(?:[/\\]|$)/.test(relPath)) return false;
116
+
117
+ const regexes = patterns.map(p => globToMatchRegex(p));
118
+ return regexes.some(r => r.test(relPath));
119
+ }
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
13
13
  import { resolve, join, extname, basename, relative } from 'node:path';
14
- import { shouldIgnore, buildIgnoreFilter } from '../shared-ignore.mjs';
14
+ import { shouldIgnore, globMatch } from '../shared-ignore.mjs';
15
15
 
16
16
  const IGNORE_DIRS = new Set([
17
17
  'node_modules', '.git', '.next', 'dist', 'build',
@@ -139,7 +139,7 @@ function diffEnvVars(dir) {
139
139
  * Diff test files between TEST-SPEC.md and actual code.
140
140
  * Uses config.testPatterns if available, otherwise falls back to
141
141
  * scanning standard test directories.
142
- * Always ignores node_modules via shared ignore filter.
142
+ * Always ignores node_modules via globMatch().
143
143
  */
144
144
  function diffTests(dir, config) {
145
145
  const testSpecPath = resolve(dir, 'docs-canonical/TEST-SPEC.md');
@@ -153,17 +153,12 @@ function diffTests(dir, config) {
153
153
  docTests.add(match[1]);
154
154
  }
155
155
 
156
+ // Collect test files from disk using globMatch (always excludes node_modules)
156
157
  const codeTests = new Set();
157
-
158
- // Use testPatterns from config if available
159
158
  const testPatterns = config?.testPatterns || [];
159
+
160
160
  if (testPatterns.length > 0) {
161
- // Use configured patterns to find test files
162
- const patternFilter = buildIgnoreFilter(testPatterns.map(p => {
163
- // Invert the pattern: we WANT files matching these patterns
164
- return p;
165
- }));
166
- // Walk the project and collect matching test files
161
+ // Use configured patterns globMatch handles node_modules exclusion
167
162
  const allTestFiles = getTestFilesFromPatterns(dir, testPatterns, config);
168
163
  for (const f of allTestFiles) {
169
164
  codeTests.add(f);
@@ -192,17 +187,18 @@ function diffTests(dir, config) {
192
187
 
193
188
  /**
194
189
  * Find test files matching configured testPatterns.
195
- * Walks the project tree, skipping node_modules and ignored dirs.
190
+ * Uses globMatch() for pattern matching always excludes node_modules.
191
+ * Results are deduplicated via Set (handles overlapping patterns).
196
192
  */
197
193
  function getTestFilesFromPatterns(dir, patterns, config) {
198
- const results = [];
199
- const testFileRegex = /\.(test|spec)\.(mjs|cjs|[jt]sx?)$/;
194
+ const results = new Set();
200
195
 
201
196
  function walk(currentDir) {
202
197
  let entries;
203
198
  try { entries = readdirSync(currentDir); } catch { return; }
204
199
 
205
200
  for (const entry of entries) {
201
+ // Skip node_modules and other ignored dirs at directory level (fast path)
206
202
  if (IGNORE_DIRS.has(entry) || entry.startsWith('.')) continue;
207
203
  const fullPath = join(currentDir, entry);
208
204
  try {
@@ -211,15 +207,11 @@ function getTestFilesFromPatterns(dir, patterns, config) {
211
207
  walk(fullPath);
212
208
  } else if (stat.isFile()) {
213
209
  const relPath = relative(dir, fullPath);
214
- // Skip files in ignored paths
210
+ // Skip files in globally ignored paths
215
211
  if (config && shouldIgnore(relPath, config)) continue;
216
- // Check if it matches test file naming patterns
217
- if (testFileRegex.test(entry) || /__(tests|test)__/.test(relPath)) {
218
- // Check if it matches any of the configured test patterns
219
- const patternFilter = buildIgnoreFilter(patterns);
220
- if (patternFilter(relPath)) {
221
- results.push(relPath);
222
- }
212
+ // Use globMatch for positive pattern matching (rejects node_modules internally)
213
+ if (globMatch(relPath, patterns)) {
214
+ results.add(relPath);
223
215
  }
224
216
  }
225
217
  } catch { /* skip */ }
@@ -227,7 +219,7 @@ function getTestFilesFromPatterns(dir, patterns, config) {
227
219
  }
228
220
 
229
221
  walk(dir);
230
- return results;
222
+ return [...results];
231
223
  }
232
224
 
233
225
  function getFilesRecursive(dir, config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
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": {