docguard-cli 0.7.3 → 0.8.2

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.
package/README.md CHANGED
@@ -81,6 +81,27 @@ diagnose → AI reads prompts → AI fixes docs → guard verifies
81
81
 
82
82
  ---
83
83
 
84
+ ## Usage
85
+
86
+ ```bash
87
+ # Initialize CDD docs for your project
88
+ npx docguard-cli init
89
+
90
+ # Reverse-engineer docs from existing code
91
+ npx docguard-cli generate
92
+
93
+ # Validate project — use as CI gate
94
+ npx docguard-cli guard
95
+
96
+ # Get AI-actionable fix prompts
97
+ npx docguard-cli diagnose
98
+
99
+ # Check CDD maturity score
100
+ npx docguard-cli score
101
+ ```
102
+
103
+ ---
104
+
84
105
  ## 13 Commands
85
106
 
86
107
  ### 🔮 Generate — Reverse-engineer docs from code
@@ -202,7 +223,7 @@ $ npx docguard-cli guard
202
223
 
203
224
  ---
204
225
 
205
- ## 9 Validators
226
+ ## 14 Validators
206
227
 
207
228
  | # | Validator | What It Checks | Default |
208
229
  |---|-----------|---------------|---------|
@@ -213,8 +234,15 @@ $ npx docguard-cli guard
213
234
  | 5 | **Changelog** | CHANGELOG.md has [Unreleased] section | ✅ On |
214
235
  | 6 | **Test-Spec** | Tests exist per TEST-SPEC.md rules | ✅ On |
215
236
  | 7 | **Environment** | Env vars documented, .env.example exists | ✅ On |
216
- | 8 | **Security** | No hardcoded secrets in source code | Off |
217
- | 9 | **Architecture** | Imports follow layer boundaries | Off |
237
+ | 8 | **Security** | No hardcoded secrets in source code | On |
238
+ | 9 | **Architecture** | Imports follow layer boundaries | On |
239
+ | 10 | **Freshness** | Docs not stale relative to code changes | ✅ On |
240
+ | 11 | **Traceability** | Canonical docs linked to source code | ✅ On |
241
+ | 12 | **Docs-Diff** | Code artifacts match documented entities | ✅ On |
242
+ | 13 | **Metadata-Sync** | Version refs consistent across docs | ✅ On |
243
+ | 14 | **Docs-Coverage** | Code features referenced in documentation | ✅ On |
244
+ | 15 | **Metrics-Consistency** | Hardcoded numbers match actual counts | ✅ On |
245
+ | 16 | **Docs-Sync** | Source files have matching doc entries | ✅ On |
218
246
 
219
247
  ---
220
248
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
7
  import { resolve, dirname } from 'node:path';
8
- import { c } from '../docguard.mjs';
8
+ import { c } from '../shared.mjs';
9
9
 
10
10
  const AGENT_TARGETS = {
11
11
  cursor: {
@@ -3,7 +3,7 @@
3
3
  * Outputs badge markdown or JSON for README, CI, and dashboards.
4
4
  */
5
5
 
6
- import { c } from '../docguard.mjs';
6
+ import { c } from '../shared.mjs';
7
7
  import { runScoreInternal } from './score.mjs';
8
8
 
9
9
  export function runBadge(projectDir, config, flags) {
@@ -8,7 +8,7 @@
8
8
  * 2 = Guard warnings only
9
9
  */
10
10
 
11
- import { c } from '../docguard.mjs';
11
+ import { c } from '../shared.mjs';
12
12
  import { runGuardInternal } from './guard.mjs';
13
13
  import { runScoreInternal } from './score.mjs';
14
14
 
@@ -13,11 +13,12 @@
13
13
  * --format prompt Full AI-ready prompt (all issues combined)
14
14
  */
15
15
 
16
- import { c } from '../docguard.mjs';
16
+ import { c } from '../shared.mjs';
17
17
  import { runGuardInternal } from './guard.mjs';
18
18
  import { runScoreInternal } from './score.mjs';
19
19
  import { existsSync, readFileSync, mkdirSync } from 'node:fs';
20
- import { resolve } from 'node:path';
20
+ import { resolve, dirname } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
21
22
  import { execSync } from 'node:child_process';
22
23
 
23
24
  // Map validator failures to the right fix --doc target
@@ -103,16 +104,16 @@ export function runDiagnose(projectDir, config, flags) {
103
104
  // ── Step 2: Collect issues ──
104
105
  let issues = collectIssues(guardData);
105
106
 
106
- // ── Step 3: Auto-fix what we can (unless --no-fix) ──
107
- const shouldAutoFix = !flags.noFix && flags.format !== 'json';
108
- if (shouldAutoFix && issues.length > 0) {
107
+ // ── Step 3: Auto-fix (only with --auto flag) or suggest fixes ──
108
+ const shouldAutoFix = flags.auto && flags.format !== 'json';
109
+ if (issues.length > 0) {
109
110
  const autoFixable = issues.filter(i => i.autoFixable);
110
111
  const hasStructural = issues.some(i => i.validator === 'Structure');
111
112
 
112
- if (hasStructural || autoFixable.length > 0) {
113
+ if (shouldAutoFix && (hasStructural || autoFixable.length > 0)) {
113
114
  // Run init to create missing files
114
115
  try {
115
- const cliPath = resolve(import.meta.dirname, '..', 'docguard.mjs');
116
+ const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'docguard.mjs');
116
117
  execSync(`node "${cliPath}" init --dir "${projectDir}"`, {
117
118
  encoding: 'utf-8',
118
119
  stdio: 'pipe',
@@ -121,7 +122,7 @@ export function runDiagnose(projectDir, config, flags) {
121
122
 
122
123
  // Run generate to fill in content
123
124
  try {
124
- const cliPath = resolve(import.meta.dirname, '..', 'docguard.mjs');
125
+ const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'docguard.mjs');
125
126
  execSync(`node "${cliPath}" generate --dir "${projectDir}" --force`, {
126
127
  encoding: 'utf-8',
127
128
  stdio: 'pipe',
@@ -138,6 +139,12 @@ export function runDiagnose(projectDir, config, flags) {
138
139
  console.log(` ${c.green}⚡ Auto-fixed ${fixedCount} issue(s)${c.reset} (created/regenerated docs)\n`);
139
140
  }
140
141
  }
142
+ } else if (!shouldAutoFix && (hasStructural || autoFixable.length > 0) && (!flags.format || flags.format === 'text')) {
143
+ // Suggest-only mode: tell user what they can do
144
+ console.log(` ${c.yellow}💡 ${autoFixable.length + (hasStructural ? 1 : 0)} issue(s) can be auto-fixed.${c.reset} Run with ${c.cyan}--auto${c.reset} to create/regenerate docs, or manually:`);
145
+ if (hasStructural) console.log(` ${c.dim}docguard init --dir .${c.reset}`);
146
+ if (autoFixable.length > 0) console.log(` ${c.dim}docguard generate --dir . --force${c.reset}`);
147
+ console.log('');
141
148
  }
142
149
  }
143
150
 
@@ -340,7 +347,7 @@ function outputPrompt(projectDir, guardData, scoreData, issues, flags) {
340
347
  lines.push('VALIDATION:');
341
348
  lines.push('After making all fixes, run: docguard guard');
342
349
  lines.push('Expected result: All checks pass (0 errors, 0 warnings)');
343
- lines.push(`Target score: ≥${scoreData.score + 5}/100`);
350
+ lines.push(`Target score: ≥${Math.min(scoreData.score + 5, 100)}/100`);
344
351
 
345
352
  // Agent-aware: add explicit checklist for basic-tier agents
346
353
  if (agentTier === 'basic') {
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
7
7
  import { resolve, join, extname, basename } from 'node:path';
8
- import { c } from '../docguard.mjs';
8
+ import { c } from '../shared.mjs';
9
9
 
10
10
  const IGNORE_DIRS = new Set([
11
11
  'node_modules', '.git', '.next', 'dist', 'build',
@@ -99,10 +99,13 @@ function diffRoutes(dir) {
99
99
 
100
100
  // Extract route-like patterns from ARCHITECTURE.md
101
101
  const docRoutes = new Set();
102
- const routeRegex = /(?:\/api\/\S+|GET|POST|PUT|DELETE|PATCH)\s+(\S+)/gi;
102
+ const routeRegex = /(?:\/api\/\S+|(?:GET|POST|PUT|DELETE|PATCH)\s+(\/\S+))/gi;
103
103
  let match;
104
104
  while ((match = routeRegex.exec(content)) !== null) {
105
- docRoutes.add(match[1] || match[0]);
105
+ const route = match[1] || match[0];
106
+ // Skip markdown table syntax and non-route content
107
+ if (route.startsWith('|') || route.startsWith('(') || route.length < 3) continue;
108
+ docRoutes.add(route);
106
109
  }
107
110
 
108
111
  // Also check for paths in tables
@@ -183,6 +186,14 @@ function diffEntities(dir) {
183
186
  'weighted', 'method', 'provider', 'token', 'expiry', 'role',
184
187
  'permissions', 'secret', 'rotation', 'access', 'variable', 'tool',
185
188
  'command', 'run', 'component', 'responsibility', 'location', 'tests',
189
+ // Data types — common in table schemas, not entity names
190
+ 'string', 'boolean', 'number', 'integer', 'float', 'double', 'decimal',
191
+ 'array', 'object', 'null', 'undefined', 'enum', 'varchar', 'text',
192
+ 'timestamp', 'uuid', 'bigint', 'serial', 'json', 'jsonb', 'blob',
193
+ 'char', 'date', 'time', 'datetime', 'binary', 'bit', 'money',
194
+ // Common table headers and template words
195
+ 'true', 'false', 'header', 'checks', 'project', 'count', 'grade',
196
+ 'breakdown', 'issuecount', 'autofixable', 'projectname', 'projecttype',
186
197
  ]);
187
198
  while ((match = tableRegex.exec(content)) !== null) {
188
199
  const name = match[1];
@@ -14,9 +14,10 @@
14
14
  */
15
15
 
16
16
  import { existsSync, readFileSync, mkdirSync } from 'node:fs';
17
- import { resolve, basename } from 'node:path';
17
+ import { resolve, basename, dirname } from 'node:path';
18
18
  import { execSync } from 'node:child_process';
19
- import { c } from '../docguard.mjs';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { c } from '../shared.mjs';
20
21
 
21
22
  // ── Document Quality Definitions ───────────────────────────────────────────
22
23
  // What each doc SHOULD contain, and what to look for in the codebase
@@ -471,7 +472,7 @@ function autoFixIssues(projectDir, config, issues) {
471
472
  }
472
473
 
473
474
  try {
474
- const cliPath = resolve(import.meta.dirname, '..', 'docguard.mjs');
475
+ const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'docguard.mjs');
475
476
  execSync(`node ${cliPath} init --dir "${projectDir}"`, {
476
477
  encoding: 'utf-8',
477
478
  stdio: 'pipe',
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, mkdirSync } from 'node:fs';
9
9
  import { resolve, join, extname, basename, relative, dirname } from 'node:path';
10
- import { c } from '../docguard.mjs';
10
+ import { c } from '../shared.mjs';
11
11
  import { detectDocTools } from '../scanners/doc-tools.mjs';
12
12
  import { scanRoutesDeep } from '../scanners/routes.mjs';
13
13
  import { scanSchemasDeep, generateERDiagram } from '../scanners/schemas.mjs';
@@ -318,7 +318,7 @@ function scanProject(dir) {
318
318
  }
319
319
  });
320
320
 
321
- // Find tests
321
+ // Find tests — top-level test dirs
322
322
  ['tests', 'test', '__tests__', 'spec', 'e2e'].forEach(testDir => {
323
323
  const fullDir = resolve(dir, testDir);
324
324
  if (existsSync(fullDir)) {
@@ -329,6 +329,55 @@ function scanProject(dir) {
329
329
  }
330
330
  });
331
331
 
332
+ // Find co-located tests: src/**/__tests__/ and src/**/*.test.* / src/**/*.spec.*
333
+ const srcDir = resolve(dir, 'src');
334
+ if (existsSync(srcDir)) {
335
+ walkDir(srcDir, (filePath) => {
336
+ const rel = relative(dir, filePath);
337
+ const isTestDir = rel.includes('__tests__') || rel.includes('__test__');
338
+ const isTestFile = /\.(test|spec)\.[^.]+$/.test(rel);
339
+ if ((isTestDir || isTestFile) && !scan.tests.includes(rel)) {
340
+ scan.tests.push(rel);
341
+ }
342
+ });
343
+ }
344
+
345
+ // Read vitest/jest config for custom test patterns
346
+ const testConfigs = ['vitest.config.ts', 'vitest.config.js', 'vitest.config.mts', 'jest.config.ts', 'jest.config.js'];
347
+ for (const cfgFile of testConfigs) {
348
+ const cfgPath = resolve(dir, cfgFile);
349
+ if (existsSync(cfgPath)) {
350
+ try {
351
+ const cfgContent = readFileSync(cfgPath, 'utf-8');
352
+ // Extract include patterns like: include: ['src/**/*.test.ts']
353
+ const includeMatch = cfgContent.match(/include\s*:\s*\[([^\]]+)\]/);
354
+ if (includeMatch) {
355
+ // Parse the test root from the pattern (e.g., 'src/**/*.test.ts' → 'src')
356
+ const patterns = includeMatch[1].match(/['"]([^'"]+)['"]/g);
357
+ if (patterns) {
358
+ for (const p of patterns) {
359
+ const pattern = p.replace(/['"]|\s/g, '');
360
+ // Extract root dir from glob (e.g., 'src/**/*.test.ts' → 'src')
361
+ const rootDir = pattern.split('/')[0];
362
+ if (rootDir && rootDir !== '**' && rootDir !== '*') {
363
+ const fullDir = resolve(dir, rootDir);
364
+ if (existsSync(fullDir)) {
365
+ walkDir(fullDir, (filePath) => {
366
+ const rel = relative(dir, filePath);
367
+ if (/\.(test|spec)\.[^.]+$/.test(rel) && !scan.tests.includes(rel)) {
368
+ scan.tests.push(rel);
369
+ }
370
+ });
371
+ }
372
+ }
373
+ }
374
+ }
375
+ }
376
+ } catch { /* config parse may fail */ }
377
+ break; // Use first found config
378
+ }
379
+ }
380
+
332
381
  // Find components
333
382
  ['src/components', 'components', 'src/ui'].forEach(compDir => {
334
383
  const fullDir = resolve(dir, compDir);
@@ -367,6 +416,15 @@ function scanProject(dir) {
367
416
  // Count files and lines
368
417
  countFilesAndLines(dir, scan);
369
418
 
419
+ // ── Filter test files out of source lists ──
420
+ // Test files (*.test.*, *.spec.*, __tests__/) should NOT appear as source files
421
+ const isTestFile = (f) => f.includes('__tests__') || f.includes('__test__') || /\.(test|spec)\.[^.]+$/.test(f);
422
+ scan.routes = scan.routes.filter(f => !isTestFile(f));
423
+ scan.models = scan.models.filter(f => !isTestFile(f));
424
+ scan.services = scan.services.filter(f => !isTestFile(f));
425
+ scan.components = scan.components.filter(f => !isTestFile(f));
426
+ scan.middlewares = scan.middlewares.filter(f => !isTestFile(f));
427
+
370
428
  return scan;
371
429
  }
372
430
 
@@ -7,7 +7,7 @@
7
7
  * runGuardInternal() → returns data, no side effects (for diagnose, ci)
8
8
  */
9
9
 
10
- import { c } from '../docguard.mjs';
10
+ import { c } from '../shared.mjs';
11
11
  import { validateStructure, validateDocSections } from '../validators/structure.mjs';
12
12
  import { validateDrift } from '../validators/drift.mjs';
13
13
  import { validateChangelog } from '../validators/changelog.mjs';
@@ -18,6 +18,10 @@ import { validateDocsSync } from '../validators/docs-sync.mjs';
18
18
  import { validateArchitecture } from '../validators/architecture.mjs';
19
19
  import { validateFreshness } from '../validators/freshness.mjs';
20
20
  import { validateTraceability } from '../validators/traceability.mjs';
21
+ import { validateDocsDiff } from '../validators/docs-diff.mjs';
22
+ import { validateMetadataSync } from '../validators/metadata-sync.mjs';
23
+ import { validateMetricsConsistency } from '../validators/metrics-consistency.mjs';
24
+ import { validateDocsCoverage } from '../validators/docs-coverage.mjs';
21
25
 
22
26
  /**
23
27
  * Internal guard — returns structured data, no console output, no process.exit.
@@ -50,6 +54,10 @@ export function runGuardInternal(projectDir, config) {
50
54
  return { errors, warnings, passed, total: passed + warnings.length + errors.length };
51
55
  }},
52
56
  { key: 'traceability', name: 'Traceability', fn: () => validateTraceability(projectDir, config) },
57
+ { key: 'docsDiff', name: 'Docs-Diff', fn: () => validateDocsDiff(projectDir, config) },
58
+ { key: 'metadataSync', name: 'Metadata-Sync', fn: () => validateMetadataSync(projectDir, config) },
59
+ { key: 'docsCoverage', name: 'Docs-Coverage', fn: () => validateDocsCoverage(projectDir, config) },
60
+ // Metrics-Consistency runs post-loop (needs guard results)
53
61
  ];
54
62
 
55
63
  for (const { key, name, fn } of validatorMap) {
@@ -82,6 +90,21 @@ export function runGuardInternal(projectDir, config) {
82
90
  }
83
91
  }
84
92
 
93
+ // ── Metrics-Consistency runs AFTER all other validators (needs their results) ──
94
+ if (validators.metricsConsistency !== false) {
95
+ try {
96
+ const result = validateMetricsConsistency(projectDir, config, results);
97
+ const hasErrors = result.errors.length > 0;
98
+ const hasWarnings = result.warnings.length > 0;
99
+ const status = hasErrors ? 'fail' : hasWarnings ? 'warn' : 'pass';
100
+ const ratio = result.total > 0 ? result.passed / result.total : 1;
101
+ const quality = hasErrors ? 'LOW' : hasWarnings ? 'MEDIUM' : ratio >= 0.9 ? 'HIGH' : 'MEDIUM';
102
+ results.push({ ...result, name: 'Metrics-Consistency', key: 'metricsConsistency', status, quality });
103
+ } catch (err) {
104
+ results.push({ name: 'Metrics-Consistency', key: 'metricsConsistency', status: 'fail', quality: 'LOW', errors: [err.message], warnings: [], passed: 0, total: 1 });
105
+ }
106
+ }
107
+
85
108
  const activeResults = results.filter(r => r.status !== 'skipped');
86
109
  const totalErrors = activeResults.reduce((sum, r) => sum + r.errors.length, 0);
87
110
  const totalWarnings = activeResults.reduce((sum, r) => sum + r.warnings.length, 0);
@@ -169,6 +192,12 @@ export function runGuard(projectDir, config, flags) {
169
192
  console.log(` ${c.dim}Run ${c.cyan}docguard diagnose${c.dim} to get AI fix prompts.${c.reset}`);
170
193
  }
171
194
 
195
+ // Badge snippet
196
+ const pct = data.total > 0 ? Math.round((data.passed / data.total) * 100) : 0;
197
+ const bColor = pct >= 90 ? 'brightgreen' : pct >= 70 ? 'green' : pct >= 50 ? 'yellow' : 'red';
198
+ const badgeUrl = `https://img.shields.io/badge/CDD_Guard-${data.passed}%2F${data.total}_passed-${bColor}`;
199
+ console.log(`\n ${c.dim}📎 Badge: ![CDD Guard](${badgeUrl})${c.reset}`);
200
+
172
201
  console.log('');
173
202
 
174
203
  if (data.errors > 0) process.exit(1);
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { existsSync, writeFileSync, mkdirSync, chmodSync, readFileSync, unlinkSync } from 'node:fs';
7
7
  import { resolve } from 'node:path';
8
- import { c } from '../docguard.mjs';
8
+ import { c } from '../shared.mjs';
9
9
 
10
10
  const HOOKS = {
11
11
  'pre-commit': {