docguard-cli 0.7.3 → 0.8.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.
@@ -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,7 +13,7 @@
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';
@@ -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];
@@ -16,7 +16,7 @@
16
16
  import { existsSync, readFileSync, mkdirSync } from 'node:fs';
17
17
  import { resolve, basename } from 'node:path';
18
18
  import { execSync } from 'node:child_process';
19
- import { c } from '../docguard.mjs';
19
+ import { c } from '../shared.mjs';
20
20
 
21
21
  // ── Document Quality Definitions ───────────────────────────────────────────
22
22
  // What each doc SHOULD contain, and what to look for in the codebase
@@ -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';
@@ -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,7 @@ 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';
21
22
 
22
23
  /**
23
24
  * Internal guard — returns structured data, no console output, no process.exit.
@@ -50,6 +51,7 @@ export function runGuardInternal(projectDir, config) {
50
51
  return { errors, warnings, passed, total: passed + warnings.length + errors.length };
51
52
  }},
52
53
  { key: 'traceability', name: 'Traceability', fn: () => validateTraceability(projectDir, config) },
54
+ { key: 'docsDiff', name: 'Docs-Diff', fn: () => validateDocsDiff(projectDir, config) },
53
55
  ];
54
56
 
55
57
  for (const { key, name, fn } of validatorMap) {
@@ -169,6 +171,12 @@ export function runGuard(projectDir, config, flags) {
169
171
  console.log(` ${c.dim}Run ${c.cyan}docguard diagnose${c.dim} to get AI fix prompts.${c.reset}`);
170
172
  }
171
173
 
174
+ // Badge snippet
175
+ const pct = data.total > 0 ? Math.round((data.passed / data.total) * 100) : 0;
176
+ const bColor = pct >= 90 ? 'brightgreen' : pct >= 70 ? 'green' : pct >= 50 ? 'yellow' : 'red';
177
+ const badgeUrl = `https://img.shields.io/badge/CDD_Guard-${data.passed}%2F${data.total}_passed-${bColor}`;
178
+ console.log(`\n ${c.dim}📎 Badge: ![CDD Guard](${badgeUrl})${c.reset}`);
179
+
172
180
  console.log('');
173
181
 
174
182
  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': {
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Init Command — Initialize CDD documentation from templates
3
+ * Interactive setup: asks which docs the user needs + suggests hooks.
3
4
  */
4
5
 
5
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
6
- import { resolve, dirname, join } from 'node:path';
7
+ import { resolve, dirname } from 'node:path';
7
8
  import { fileURLToPath } from 'node:url';
8
- import { c, PROFILES } from '../docguard.mjs';
9
+ import { createInterface } from 'node:readline';
10
+ import { c, PROFILES } from '../shared.mjs';
9
11
 
10
12
  function detectProjectType(dir) {
11
13
  const pkgPath = resolve(dir, 'package.json');
@@ -29,7 +31,21 @@ const __filename = fileURLToPath(import.meta.url);
29
31
  const __dirname = dirname(__filename);
30
32
  const TEMPLATES_DIR = resolve(__dirname, '../../templates');
31
33
 
32
- export function runInit(projectDir, config, flags) {
34
+ // ── Readline helper ──────────────────────────────────────────────────────
35
+
36
+ function askQuestion(prompt) {
37
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
38
+ return new Promise(res => {
39
+ rl.question(prompt, answer => {
40
+ rl.close();
41
+ res(answer);
42
+ });
43
+ });
44
+ }
45
+
46
+ // ── Init Command ─────────────────────────────────────────────────────────
47
+
48
+ export async function runInit(projectDir, config, flags) {
33
49
  const profileName = flags.profile || 'standard';
34
50
  const profile = PROFILES[profileName];
35
51
 
@@ -43,29 +59,63 @@ export function runInit(projectDir, config, flags) {
43
59
  console.log(`${c.dim} Directory: ${projectDir}${c.reset}`);
44
60
  console.log(`${c.dim} Profile: ${profileName} — ${profile.description}${c.reset}\n`);
45
61
 
62
+ // Detect project type
63
+ const detectedType = detectProjectType(projectDir);
64
+ console.log(` ${c.dim}Auto-detected project type: ${c.cyan}${detectedType}${c.reset}\n`);
65
+
66
+ // ── Doc catalog ────────────────────────────────────────────────────────
67
+ const allDocs = [
68
+ { key: 'ARCHITECTURE', file: 'docs-canonical/ARCHITECTURE.md', template: 'ARCHITECTURE.md.template', desc: 'System architecture, tech stack, layer boundaries', defaultYes: true },
69
+ { key: 'DATA-MODEL', file: 'docs-canonical/DATA-MODEL.md', template: 'DATA-MODEL.md.template', desc: 'Database schemas, entities, relationships', defaultYes: ['webapp', 'api'].includes(detectedType) },
70
+ { key: 'SECURITY', file: 'docs-canonical/SECURITY.md', template: 'SECURITY.md.template', desc: 'Auth, secrets, security controls', defaultYes: ['webapp', 'api'].includes(detectedType) },
71
+ { key: 'TEST-SPEC', file: 'docs-canonical/TEST-SPEC.md', template: 'TEST-SPEC.md.template', desc: 'Test strategy, coverage requirements', defaultYes: true },
72
+ { key: 'ENVIRONMENT', file: 'docs-canonical/ENVIRONMENT.md', template: 'ENVIRONMENT.md.template', desc: 'Environment variables, deployment config', defaultYes: ['webapp', 'api'].includes(detectedType) },
73
+ ];
74
+
75
+ let selectedDocs;
76
+
77
+ if (flags.skipPrompts || flags.force) {
78
+ // Non-interactive — use profile defaults
79
+ const profileCanonical = profile.requiredFiles?.canonical || allDocs.map(d => d.file);
80
+ selectedDocs = allDocs.filter(d => profileCanonical.includes(d.file));
81
+ console.log(` ${c.dim}Non-interactive mode — using ${profileName} profile defaults${c.reset}\n`);
82
+ } else {
83
+ // Interactive — ask about each doc
84
+ console.log(` ${c.bold}Which canonical docs does your project need?${c.reset}`);
85
+ console.log(` ${c.dim}(press Enter for default, type y or n)${c.reset}\n`);
86
+
87
+ selectedDocs = [];
88
+ for (const doc of allDocs) {
89
+ const defaultLabel = doc.defaultYes ? 'Y/n' : 'y/N';
90
+ const answer = await askQuestion(` ${doc.key} — ${doc.desc} [${defaultLabel}]: `);
91
+ const trimmed = answer.trim().toLowerCase();
92
+
93
+ const include = doc.defaultYes
94
+ ? (trimmed === '' || trimmed === 'y' || trimmed === 'yes')
95
+ : (trimmed === 'y' || trimmed === 'yes');
96
+
97
+ if (include) {
98
+ selectedDocs.push(doc);
99
+ }
100
+ }
101
+ console.log('');
102
+ }
103
+
104
+ // ── Create selected doc files ──────────────────────────────────────────
46
105
  const created = [];
47
106
  const skipped = [];
48
107
 
49
- // Map template files to their destinations
50
- const allMappings = [
51
- { template: 'ARCHITECTURE.md.template', dest: 'docs-canonical/ARCHITECTURE.md' },
52
- { template: 'DATA-MODEL.md.template', dest: 'docs-canonical/DATA-MODEL.md' },
53
- { template: 'SECURITY.md.template', dest: 'docs-canonical/SECURITY.md' },
54
- { template: 'TEST-SPEC.md.template', dest: 'docs-canonical/TEST-SPEC.md' },
55
- { template: 'ENVIRONMENT.md.template', dest: 'docs-canonical/ENVIRONMENT.md' },
108
+ // Always create tracking files
109
+ const alwaysCreate = [
56
110
  { template: 'AGENTS.md.template', dest: 'AGENTS.md' },
57
111
  { template: 'CHANGELOG.md.template', dest: 'CHANGELOG.md' },
58
112
  { template: 'DRIFT-LOG.md.template', dest: 'DRIFT-LOG.md' },
59
113
  ];
60
114
 
61
- // Filter based on profile — starter only gets required files
62
- const profileRequiredFiles = profile.requiredFiles
63
- ? new Set([...profile.requiredFiles.canonical, profile.requiredFiles.changelog, profile.requiredFiles.driftLog, ...profile.requiredFiles.agentFile])
64
- : null;
65
-
66
- const fileMappings = profileRequiredFiles
67
- ? allMappings.filter(m => profileRequiredFiles.has(m.dest))
68
- : allMappings;
115
+ const fileMappings = [
116
+ ...selectedDocs.map(d => ({ template: d.template, dest: d.file })),
117
+ ...alwaysCreate,
118
+ ];
69
119
 
70
120
  for (const mapping of fileMappings) {
71
121
  const destPath = resolve(projectDir, mapping.dest);
@@ -77,35 +127,26 @@ export function runInit(projectDir, config, flags) {
77
127
  continue;
78
128
  }
79
129
 
80
- // Ensure directory exists
81
130
  const destDir = dirname(destPath);
82
131
  if (!existsSync(destDir)) {
83
132
  mkdirSync(destDir, { recursive: true });
84
133
  }
85
134
 
86
- // Read template and write
87
135
  if (existsSync(templatePath)) {
88
136
  const content = readFileSync(templatePath, 'utf-8');
89
- // Replace template date placeholder with today's date
90
137
  const today = new Date().toISOString().split('T')[0];
91
138
  const processed = content.replace(/YYYY-MM-DD/g, today);
92
139
  writeFileSync(destPath, processed, 'utf-8');
93
140
  created.push(mapping.dest);
94
141
  console.log(` ${c.green}✅${c.reset} Created: ${c.cyan}${mapping.dest}${c.reset}`);
95
142
  } else {
96
- console.log(
97
- ` ${c.red}❌${c.reset} Template not found: ${mapping.template}`
98
- );
143
+ console.log(` ${c.red}❌${c.reset} Template not found: ${mapping.template}`);
99
144
  }
100
145
  }
101
146
 
102
- // Create .docguard.json if it doesn't exist — auto-detect project type
147
+ // ── Create .docguard.json ──────────────────────────────────────────────
103
148
  const configPath = resolve(projectDir, '.docguard.json');
104
149
  if (!existsSync(configPath)) {
105
- // Detect project type from package.json
106
- const detectedType = detectProjectType(projectDir);
107
-
108
- // Get appropriate defaults for this project type
109
150
  const typeDefaults = {
110
151
  cli: { needsEnvVars: false, needsEnvExample: false, needsE2E: false, needsDatabase: false },
111
152
  library: { needsEnvVars: false, needsEnvExample: false, needsE2E: false, needsDatabase: false },
@@ -122,6 +163,9 @@ export function runInit(projectDir, config, flags) {
122
163
  profile: profileName,
123
164
  projectType: detectedType,
124
165
  projectTypeConfig: ptc,
166
+ requiredFiles: {
167
+ canonical: selectedDocs.map(d => d.file),
168
+ },
125
169
  validators: profile.validators || {
126
170
  structure: true,
127
171
  docsSync: true,
@@ -137,18 +181,17 @@ export function runInit(projectDir, config, flags) {
137
181
 
138
182
  writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf-8');
139
183
  created.push('.docguard.json');
140
- console.log(` ${c.green}✅${c.reset} Created: ${c.cyan}.docguard.json${c.reset} ${c.dim}(auto-detected: ${detectedType})${c.reset}`);
184
+ console.log(` ${c.green}✅${c.reset} Created: ${c.cyan}.docguard.json${c.reset} ${c.dim}(${selectedDocs.length} docs selected, type: ${detectedType})${c.reset}`);
141
185
  } else {
142
186
  skipped.push('.docguard.json');
143
187
  console.log(` ${c.yellow}⏭️${c.reset} .docguard.json ${c.dim}(already exists)${c.reset}`);
144
188
  }
145
189
 
146
- // Install slash commands for AI agents — detect which agents are in use
190
+ // ── Slash commands for AI agents ───────────────────────────────────────
147
191
  const commandsSourceDir = resolve(TEMPLATES_DIR, 'commands');
148
192
  if (existsSync(commandsSourceDir)) {
149
193
  const commandFiles = readdirSync(commandsSourceDir).filter(f => f.endsWith('.md'));
150
194
 
151
- // Detect which AI agent directories exist in the project
152
195
  const agentDirs = [
153
196
  { name: 'GitHub Copilot', path: '.github/commands' },
154
197
  { name: 'Cursor', path: '.cursor/rules' },
@@ -157,12 +200,10 @@ export function runInit(projectDir, config, flags) {
157
200
  { name: 'Antigravity', path: '.agents/workflows' },
158
201
  ];
159
202
 
160
- // Find which agent dirs already exist in the project
161
203
  const detected = agentDirs.filter(a =>
162
- existsSync(resolve(projectDir, a.path.split('/')[0])) // check parent dir exists
204
+ existsSync(resolve(projectDir, a.path.split('/')[0]))
163
205
  );
164
206
 
165
- // If none detected, default to .github/commands (most universal)
166
207
  const targets = detected.length > 0
167
208
  ? detected
168
209
  : [{ name: 'GitHub (default)', path: '.github/commands' }];
@@ -203,47 +244,44 @@ export function runInit(projectDir, config, flags) {
203
244
  }
204
245
  }
205
246
 
206
- // Summary
247
+ // ── Summary ────────────────────────────────────────────────────────────
207
248
  console.log(`\n${c.bold} ─────────────────────────────────────${c.reset}`);
208
249
  console.log(` ${c.green}Created:${c.reset} ${created.length} files`);
209
250
  if (skipped.length > 0) {
210
251
  console.log(` ${c.yellow}Skipped:${c.reset} ${skipped.length} files (already exist)`);
211
252
  }
212
253
 
213
- if (flags.skipPrompts) {
214
- // Simple instructions, no AI prompts
215
- console.log(`\n ${c.bold}Next steps:${c.reset}`);
216
- console.log(` ${c.dim}Run${c.reset} ${c.cyan}docguard diagnose${c.reset} ${c.dim}to get AI prompts for filling docs.${c.reset}`);
217
- console.log(` ${c.dim}Then verify:${c.reset} ${c.cyan}docguard guard${c.reset}\n`);
218
- } else {
219
- // Auto-populate: output AI research prompts for each created canonical doc
220
- const createdDocs = created.filter(f => f.startsWith('docs-canonical/'));
221
-
222
- if (createdDocs.length > 0) {
223
- console.log(`\n ${c.bold}🤖 AI Auto-Populate${c.reset}`);
224
- console.log(` ${c.dim}The files above are skeleton templates. Your AI agent should fill them.${c.reset}`);
225
- console.log(` ${c.dim}Run this single command to get a full remediation plan:${c.reset}\n`);
226
- console.log(` ${c.cyan}${c.bold}docguard diagnose${c.reset}\n`);
227
- console.log(` ${c.dim}Or generate prompts for individual docs:${c.reset}`);
228
-
229
- const docNameMap = {
230
- 'docs-canonical/ARCHITECTURE.md': 'architecture',
231
- 'docs-canonical/DATA-MODEL.md': 'data-model',
232
- 'docs-canonical/SECURITY.md': 'security',
233
- 'docs-canonical/TEST-SPEC.md': 'test-spec',
234
- 'docs-canonical/ENVIRONMENT.md': 'environment',
235
- };
236
-
237
- for (const doc of createdDocs) {
238
- const target = docNameMap[doc];
239
- if (target) {
240
- console.log(` ${c.cyan}docguard fix --doc ${target}${c.reset}`);
241
- }
254
+ // ── Hooks suggestion ──────────────────────────────────────────────────
255
+ console.log(`\n ${c.bold}💡 Automation:${c.reset}`);
256
+ console.log(` ${c.dim}Auto-guard on commit:${c.reset} ${c.cyan}docguard hooks --type pre-commit${c.reset}`);
257
+ console.log(` ${c.dim}Auto-guard on push:${c.reset} ${c.cyan}docguard hooks --type pre-push${c.reset}`);
258
+
259
+ // ── Next steps ─────────────────────────────────────────────────────────
260
+ const createdDocs = created.filter(f => f.startsWith('docs-canonical/'));
261
+
262
+ if (createdDocs.length > 0) {
263
+ console.log(`\n ${c.bold}🤖 AI Auto-Populate${c.reset}`);
264
+ console.log(` ${c.dim}The files above are skeleton templates. Your AI agent should fill them.${c.reset}`);
265
+ console.log(` ${c.dim}Run this single command to get a full remediation plan:${c.reset}\n`);
266
+ console.log(` ${c.cyan}${c.bold}docguard diagnose${c.reset}\n`);
267
+ console.log(` ${c.dim}Or generate prompts for individual docs:${c.reset}`);
268
+
269
+ const docNameMap = {
270
+ 'docs-canonical/ARCHITECTURE.md': 'architecture',
271
+ 'docs-canonical/DATA-MODEL.md': 'data-model',
272
+ 'docs-canonical/SECURITY.md': 'security',
273
+ 'docs-canonical/TEST-SPEC.md': 'test-spec',
274
+ 'docs-canonical/ENVIRONMENT.md': 'environment',
275
+ };
276
+
277
+ for (const doc of createdDocs) {
278
+ const target = docNameMap[doc];
279
+ if (target) {
280
+ console.log(` ${c.cyan}docguard fix --doc ${target}${c.reset}`);
242
281
  }
243
- console.log(`\n ${c.dim}Then verify:${c.reset} ${c.cyan}docguard guard${c.reset}\n`);
244
- } else {
245
- console.log(`\n ${c.dim}Run${c.reset} ${c.cyan}docguard diagnose${c.reset} ${c.dim}to check for issues.${c.reset}\n`);
246
282
  }
283
+ console.log(`\n ${c.dim}Then verify:${c.reset} ${c.cyan}docguard guard${c.reset}\n`);
284
+ } else {
285
+ console.log(`\n ${c.dim}Run${c.reset} ${c.cyan}docguard diagnose${c.reset} ${c.dim}to check for issues.${c.reset}\n`);
247
286
  }
248
287
  }
249
-
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
10
10
  import { resolve, basename } from 'node:path';
11
- import { c } from '../docguard.mjs';
11
+ import { c } from '../shared.mjs';
12
12
 
13
13
  const SUPPORTED_PLATFORMS = ['mintlify'];
14
14
 
@@ -6,7 +6,7 @@
6
6
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
7
7
  import { resolve, join, extname } from 'node:path';
8
8
  import { execSync } from 'node:child_process';
9
- import { c } from '../docguard.mjs';
9
+ import { c } from '../shared.mjs';
10
10
 
11
11
  const WEIGHTS = {
12
12
  structure: 25, // Required files exist
@@ -23,7 +23,7 @@ export function runScore(projectDir, config, flags) {
23
23
  console.log(`${c.bold}📊 DocGuard Score — ${config.projectName}${c.reset}`);
24
24
  console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
25
25
 
26
- const { scores, totalScore, grade } = calcAllScores(projectDir, config);
26
+ const { scores, totalScore, grade, details } = calcAllScores(projectDir, config);
27
27
 
28
28
  // ── Display Results ──
29
29
  if (flags.format === 'json') {
@@ -80,7 +80,7 @@ export function runScore(projectDir, config, flags) {
80
80
  if (weakest.length > 0) {
81
81
  console.log(` ${c.bold}Top improvements:${c.reset}`);
82
82
  for (const [cat, score] of weakest) {
83
- const suggestion = getSuggestion(cat, score);
83
+ const suggestion = getSuggestion(cat, score, details);
84
84
  console.log(` ${c.yellow}→ ${cat}${c.reset}: ${suggestion}`);
85
85
  }
86
86
  console.log('');
@@ -130,6 +130,11 @@ export function runScore(projectDir, config, flags) {
130
130
  console.log(` ${c.dim}Quality labels: HIGH (≥90%), MEDIUM (50-89%), LOW (<50%)${c.reset}`);
131
131
  console.log(` ${c.dim}Methodology: CJE multi-signal composite (Lopez et al., TRACE, IEEE TMLCN 2026)${c.reset}\n`);
132
132
  }
133
+
134
+ // Badge snippet
135
+ const bColor = totalScore >= 90 ? 'brightgreen' : totalScore >= 80 ? 'green' : totalScore >= 70 ? 'yellowgreen' : totalScore >= 60 ? 'yellow' : totalScore >= 50 ? 'orange' : 'red';
136
+ const badgeUrl = `https://img.shields.io/badge/CDD_Score-${totalScore}%2F100_(${grade})-${bColor}`;
137
+ console.log(` ${c.dim}📎 Badge: ![CDD Score](${badgeUrl})${c.reset}\n`);
133
138
  }
134
139
 
135
140
  /**
@@ -143,8 +148,12 @@ export function runScoreInternal(projectDir, config) {
143
148
 
144
149
  function calcAllScores(projectDir, config) {
145
150
  const scores = {};
151
+ const details = {}; // Per-category failure details for actionable suggestions
152
+
146
153
  scores.structure = calcStructureScore(projectDir, config);
147
- scores.docQuality = calcDocQualityScore(projectDir, config);
154
+ const dqResult = calcDocQualityScore(projectDir, config);
155
+ scores.docQuality = dqResult.score;
156
+ details.docQuality = dqResult.failures;
148
157
  scores.testing = calcTestingScore(projectDir, config);
149
158
  scores.security = calcSecurityScore(projectDir, config);
150
159
  scores.environment = calcEnvironmentScore(projectDir, config);
@@ -158,7 +167,7 @@ function calcAllScores(projectDir, config) {
158
167
  }
159
168
  totalScore = Math.round(totalScore);
160
169
 
161
- return { scores, totalScore, grade: getGrade(totalScore) };
170
+ return { scores, totalScore, grade: getGrade(totalScore), details };
162
171
  }
163
172
 
164
173
  // ── Scoring Functions ──────────────────────────────────────────────────────
@@ -196,32 +205,42 @@ function calcDocQualityScore(dir, config) {
196
205
 
197
206
  let found = 0;
198
207
  let total = 0;
208
+ const failures = []; // Track specific failures for actionable suggestions
199
209
 
200
210
  for (const [file, sections] of Object.entries(checks)) {
201
211
  const fullPath = resolve(dir, file);
202
- if (!existsSync(fullPath)) continue;
212
+ if (!existsSync(fullPath)) {
213
+ failures.push({ file, issue: 'file missing' });
214
+ continue;
215
+ }
203
216
 
204
217
  const content = readFileSync(fullPath, 'utf-8');
218
+ const docName = file.replace('docs-canonical/', '').replace('.md', '').toLowerCase();
205
219
 
206
220
  for (const section of sections) {
207
221
  total++;
208
- if (content.includes(section)) found++;
222
+ if (content.includes(section)) {
223
+ found++;
224
+ } else {
225
+ failures.push({ file, issue: `missing section: ${section}`, fixCmd: `docguard fix --doc ${docName}` });
226
+ }
209
227
  }
210
228
 
211
- // Bonus: check if doc has docguard metadata
212
- total++;
213
- if (content.includes('docguard:version')) found++;
214
-
215
- // Bonus: check if doc has more than just template placeholders
229
+ // Check if doc has more than just template placeholders
216
230
  total++;
217
231
  const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#') && !l.startsWith('|') && !l.startsWith('>') && !l.startsWith('<!--'));
218
- if (lines.length > 5) found++;
232
+ if (lines.length > 5) {
233
+ found++;
234
+ } else {
235
+ failures.push({ file, issue: `thin content (${lines.length} lines — need >5)`, fixCmd: `docguard fix --doc ${docName}` });
236
+ }
219
237
  }
220
238
 
221
- return total === 0 ? 0 : Math.round((found / total) * 100);
239
+ const score = total === 0 ? 0 : Math.round((found / total) * 100);
240
+ return { score, failures };
222
241
  }
223
242
 
224
- function calcTestingScore(dir) {
243
+ function calcTestingScore(dir, config) {
225
244
  let score = 0;
226
245
 
227
246
  // Check test directory exists
@@ -232,10 +251,28 @@ function calcTestingScore(dir) {
232
251
  // Check test spec exists
233
252
  if (existsSync(resolve(dir, 'docs-canonical/TEST-SPEC.md'))) score += 30;
234
253
 
235
- // Check for test config files
254
+ // Check for test config files OR built-in test runner
236
255
  const testConfigs = ['jest.config.js', 'jest.config.ts', 'vitest.config.ts', 'vitest.config.js', 'pytest.ini', 'setup.cfg', '.mocharc.yml'];
237
256
  const hasTestConfig = testConfigs.some(f => existsSync(resolve(dir, f)));
238
- if (hasTestConfig) score += 15;
257
+
258
+ if (hasTestConfig) {
259
+ score += 15;
260
+ } else {
261
+ // Check if using node:test (no config needed) — look in package.json scripts
262
+ const ptc = config.projectTypeConfig || {};
263
+ const pkgPath = resolve(dir, 'package.json');
264
+ if (ptc.testFramework === 'node:test') {
265
+ score += 15; // Config says node:test — no config file needed
266
+ } else if (existsSync(pkgPath)) {
267
+ try {
268
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
269
+ const testScript = pkg.scripts?.test || '';
270
+ if (testScript.includes('node --test') || testScript.includes('node:test')) {
271
+ score += 15; // Using built-in test runner
272
+ }
273
+ } catch { /* skip */ }
274
+ }
275
+ }
239
276
 
240
277
  // Check for CI test step
241
278
  const ciFiles = ['.github/workflows/ci.yml', '.github/workflows/test.yml'];
@@ -245,8 +282,9 @@ function calcTestingScore(dir) {
245
282
  return Math.min(100, score);
246
283
  }
247
284
 
248
- function calcSecurityScore(dir) {
285
+ function calcSecurityScore(dir, config) {
249
286
  let score = 0;
287
+ const ptc = config.projectTypeConfig || {};
250
288
 
251
289
  // SECURITY.md exists
252
290
  if (existsSync(resolve(dir, 'docs-canonical/SECURITY.md'))) score += 30;
@@ -262,17 +300,28 @@ function calcSecurityScore(dir) {
262
300
  // No .env file committed (check if .env exists but .gitignore covers it)
263
301
  if (!existsSync(resolve(dir, '.env')) || existsSync(gitignorePath)) score += 15;
264
302
 
265
- // .env.example exists (safe template)
266
- if (existsSync(resolve(dir, '.env.example'))) score += 15;
303
+ // .env.example exists (safe template) — only check if project needs env vars
304
+ if (ptc.needsEnvExample === false) {
305
+ score += 15; // Full marks — project doesn't need env vars
306
+ } else if (existsSync(resolve(dir, '.env.example'))) {
307
+ score += 15;
308
+ }
267
309
 
268
310
  return Math.min(100, score);
269
311
  }
270
312
 
271
- function calcEnvironmentScore(dir) {
313
+ function calcEnvironmentScore(dir, config) {
272
314
  let score = 0;
315
+ const ptc = config.projectTypeConfig || {};
273
316
 
274
317
  if (existsSync(resolve(dir, 'docs-canonical/ENVIRONMENT.md'))) score += 40;
275
- if (existsSync(resolve(dir, '.env.example'))) score += 30;
318
+
319
+ // .env.example — only check if project needs env vars
320
+ if (ptc.needsEnvExample === false) {
321
+ score += 30; // Full marks — project doesn't need env vars
322
+ } else if (existsSync(resolve(dir, '.env.example'))) {
323
+ score += 30;
324
+ }
276
325
 
277
326
  // Check for setup documentation
278
327
  const readmePath = resolve(dir, 'README.md');
@@ -352,16 +401,31 @@ function getGrade(score) {
352
401
  return 'F';
353
402
  }
354
403
 
355
- function getSuggestion(category, score) {
404
+ function getSuggestion(category, score, details) {
405
+ // Dynamic, specific suggestions based on actual failures
406
+ if (category === 'docQuality' && details?.docQuality?.length > 0) {
407
+ const failures = details.docQuality;
408
+ // Group by doc
409
+ const byDoc = {};
410
+ for (const f of failures) {
411
+ const doc = f.file.replace('docs-canonical/', '');
412
+ if (!byDoc[doc]) byDoc[doc] = [];
413
+ byDoc[doc].push(f.issue);
414
+ }
415
+ const parts = Object.entries(byDoc).map(([doc, issues]) => `${doc}: ${issues.join(', ')}`);
416
+ const fixCmd = failures.find(f => f.fixCmd)?.fixCmd || 'docguard fix';
417
+ return `${parts.join(' | ')} → Run \`${fixCmd}\``;
418
+ }
419
+
356
420
  const suggestions = {
357
421
  structure: 'Run `docguard init` to create missing documentation',
358
- docQuality: 'Fill in template sections replace placeholders with real content',
422
+ docQuality: 'Run `docguard fix` to get AI prompts for each doc that needs content',
359
423
  testing: 'Add tests/ directory and configure TEST-SPEC.md',
360
- security: 'Create SECURITY.md and add .env to .gitignore',
361
- environment: 'Document env variables and create .env.example',
424
+ security: 'Create SECURITY.md and add .env to .gitignore → Run `docguard fix --doc security`',
425
+ environment: 'Document env variables and create .env.example → Run `docguard fix --doc environment`',
362
426
  drift: 'Create DRIFT-LOG.md and log any code deviations',
363
427
  changelog: 'Maintain CHANGELOG.md with [Unreleased] section',
364
- architecture: 'Add layer boundaries and Mermaid diagrams to ARCHITECTURE.md',
428
+ architecture: 'Add layer boundaries and Mermaid diagrams Run `docguard fix --doc architecture`',
365
429
  };
366
430
  return suggestions[category] || 'Review and improve this area';
367
431
  }
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
10
10
  import { resolve, join, extname, basename, relative } from 'node:path';
11
- import { c } from '../docguard.mjs';
11
+ import { c } from '../shared.mjs';
12
12
 
13
13
  const IGNORE_DIRS = new Set([
14
14
  'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { watch as fsWatch, existsSync, readdirSync, statSync } from 'node:fs';
11
11
  import { resolve, relative, extname } from 'node:path';
12
- import { c } from '../docguard.mjs';
12
+ import { c } from '../shared.mjs';
13
13
  import { runGuardInternal } from './guard.mjs';
14
14
 
15
15
  const DEBOUNCE_MS = 500;
package/cli/docguard.mjs CHANGED
@@ -22,7 +22,7 @@ import { fileURLToPath } from 'node:url';
22
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
23
  const PKG = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
24
24
  const VERSION = PKG.version;
25
- import { runAudit } from './commands/audit.mjs';
25
+ // audit is now an alias for guard (old audit.mjs deleted — guard does everything it did + more)
26
26
  import { runInit } from './commands/init.mjs';
27
27
  import { runGuard } from './commands/guard.mjs';
28
28
  import { runScore } from './commands/score.mjs';
@@ -38,66 +38,9 @@ import { runDiagnose } from './commands/diagnose.mjs';
38
38
  import { runPublish } from './commands/publish.mjs';
39
39
  import { runTrace } from './commands/trace.mjs';
40
40
 
41
- // ── Colors (ANSI escape codes, zero deps) ──────────────────────────────────
42
- export const c = {
43
- reset: '\x1b[0m',
44
- bold: '\x1b[1m',
45
- dim: '\x1b[2m',
46
- red: '\x1b[31m',
47
- green: '\x1b[32m',
48
- yellow: '\x1b[33m',
49
- blue: '\x1b[34m',
50
- cyan: '\x1b[36m',
51
- white: '\x1b[37m',
52
- bgRed: '\x1b[41m',
53
- bgGreen: '\x1b[42m',
54
- bgYellow: '\x1b[43m',
55
- };
56
- // ── Compliance Profiles ───────────────────────────────────────────────────
57
- // Profiles layer between defaults and user config — they're preset bundles
58
- // that adjust what docs are required and which validators run.
59
- const PROFILES = {
60
- starter: {
61
- description: 'Minimal CDD — just architecture + changelog. For side projects and prototypes.',
62
- requiredFiles: {
63
- canonical: [
64
- 'docs-canonical/ARCHITECTURE.md',
65
- ],
66
- agentFile: ['AGENTS.md', 'CLAUDE.md'],
67
- changelog: 'CHANGELOG.md',
68
- driftLog: 'DRIFT-LOG.md',
69
- },
70
- validators: {
71
- structure: true,
72
- docsSync: true,
73
- drift: false,
74
- changelog: true,
75
- architecture: false,
76
- testSpec: false,
77
- security: false,
78
- environment: false,
79
- freshness: false,
80
- },
81
- },
82
- standard: {
83
- description: 'Full CDD — all 5 canonical docs. For team projects.',
84
- // Uses the defaults — no overrides needed
85
- },
86
- enterprise: {
87
- description: 'Strict CDD — all docs, all validators, freshness enforced. For regulated/enterprise projects.',
88
- validators: {
89
- structure: true,
90
- docsSync: true,
91
- drift: true,
92
- changelog: true,
93
- architecture: true,
94
- testSpec: true,
95
- security: true,
96
- environment: true,
97
- freshness: true,
98
- },
99
- },
100
- };
41
+ // ── Shared constants (imported to break circular dependencies) ──────────
42
+ import { c, PROFILES } from './shared.mjs';
43
+ export { c, PROFILES };
101
44
 
102
45
  // ── Config Loading ─────────────────────────────────────────────────────────
103
46
  export function loadConfig(projectDir) {
@@ -195,8 +138,7 @@ export function loadConfig(projectDir) {
195
138
  return defaults;
196
139
  }
197
140
 
198
- // Export profiles for use by other commands (init --profile)
199
- export { PROFILES };
141
+ // PROFILES is exported from shared.mjs (re-exported at line 43)
200
142
 
201
143
  /**
202
144
  * Auto-detect project type from package.json and file structure.
@@ -273,22 +215,31 @@ function printHelp() {
273
215
  console.log(`${c.bold}Usage:${c.reset}
274
216
  docguard <command> [options]
275
217
 
276
- ${c.bold}Commands:${c.reset}
277
- ${c.green}audit${c.reset} Scan project, report what CDD docs exist or are missing
278
- ${c.green}init${c.reset} Initialize CDD documentation from templates
279
- ${c.green}guard${c.reset} Validate project against its canonical documentation
280
- ${c.green}score${c.reset} Calculate CDD maturity score (0-100)
281
- ${c.green}diagnose${c.reset} AI orchestrator — chains guard→fix in one command
282
- ${c.green}diff${c.reset} Show gaps between canonical docs and code
283
- ${c.green}agents${c.reset} Generate agent-specific config files from AGENTS.md
218
+ ${c.bold}Getting Started:${c.reset}
219
+ ${c.green}init${c.reset} Initialize CDD docs (interactive setup)
284
220
  ${c.green}generate${c.reset} Reverse-engineer canonical docs from existing code
285
- ${c.green}hooks${c.reset} Install git hooks (pre-commit, pre-push, commit-msg)
221
+
222
+ ${c.bold}Enforcement:${c.reset}
223
+ ${c.green}guard${c.reset} Validate project against canonical docs (51+ checks)
224
+ ${c.green}diagnose${c.reset} AI orchestrator — guard → fix in one command
225
+
226
+ ${c.bold}Analysis:${c.reset}
227
+ ${c.green}score${c.reset} CDD maturity score (0-100)
228
+ ${c.green}trace${c.reset} Requirements traceability matrix
229
+ ${c.green}diff${c.reset} Show gaps between docs and code (detailed view)
230
+
231
+ ${c.bold}CI/CD & Automation:${c.reset}
232
+ ${c.green}ci${c.reset} Pipeline gate (guard + score, exit codes)
233
+ ${c.green}hooks${c.reset} Install/manage git hooks
234
+ ${c.green}watch${c.reset} Watch for changes, re-run guard
235
+
236
+ ${c.bold}Utilities:${c.reset}
237
+ ${c.green}fix${c.reset} Generate AI fix instructions for specific docs
238
+ ${c.green}agents${c.reset} Generate agent config files (AGENTS.md, CLAUDE.md)
286
239
  ${c.green}badge${c.reset} Generate CDD score badges for README
287
- ${c.green}ci${c.reset} Single command for CI/CD pipelines (guard + score)
288
- ${c.green}fix${c.reset} Find issues and generate AI fix instructions
289
- ${c.green}watch${c.reset} Watch for file changes and re-run guard automatically
290
- ${c.green}publish${c.reset} Scaffold external docs (Mintlify, Docusaurus)
291
- ${c.green}trace${c.reset} Generate requirements traceability matrix
240
+
241
+ ${c.bold}Experimental:${c.reset}
242
+ ${c.dim}publish${c.reset} Scaffold external doc sites (Mintlify)
292
243
 
293
244
  ${c.bold}Options:${c.reset}
294
245
  --dir <path> Project directory (default: current directory)
@@ -305,7 +256,6 @@ ${c.bold}Options:${c.reset}
305
256
  --auto Auto-fix what's possible (used with fix command)
306
257
  --doc <name> Generate AI prompt for specific doc (architecture, security, etc.)
307
258
  --profile <p> Compliance profile: starter, standard, enterprise (init command)
308
- --platform <p> Doc platform: mintlify (publish command)
309
259
  --tax Show estimated documentation maintenance cost (with score)
310
260
  --help Show this help message
311
261
  --version Show version
@@ -319,12 +269,12 @@ ${c.bold}Examples:${c.reset}
319
269
  ${c.dim}# AI auto-diagnose and fix${c.reset}
320
270
  docguard diagnose
321
271
 
322
- ${c.dim}# Quick start for a side project${c.reset}
323
- docguard init --profile starter
324
-
325
- ${c.dim}# Full CDD init (default)${c.reset}
272
+ ${c.dim}# Interactive setup (asks which docs you need)${c.reset}
326
273
  docguard init
327
274
 
275
+ ${c.dim}# Quick start for a side project${c.reset}
276
+ docguard init --profile starter --skip-prompts
277
+
328
278
  ${c.dim}# See documentation tax estimate${c.reset}
329
279
  docguard score --tax
330
280
 
@@ -339,7 +289,7 @@ ${c.bold}Learn more:${c.reset}
339
289
  }
340
290
 
341
291
  // ── Main ───────────────────────────────────────────────────────────────────
342
- function main() {
292
+ async function main() {
343
293
  const args = process.argv.slice(2);
344
294
  const command = args[0];
345
295
 
@@ -425,10 +375,11 @@ function main() {
425
375
 
426
376
  switch (command) {
427
377
  case 'audit':
428
- runAudit(projectDir, config, flags);
378
+ // audit is an alias for guard — guard does everything the old audit did + 50 more checks
379
+ runGuard(projectDir, config, flags);
429
380
  break;
430
381
  case 'init':
431
- runInit(projectDir, config, flags);
382
+ await runInit(projectDir, config, flags);
432
383
  break;
433
384
  case 'guard':
434
385
  runGuard(projectDir, config, flags);
package/cli/shared.mjs ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Shared constants for DocGuard CLI — colors, profiles, version.
3
+ * Extracted from docguard.mjs to break circular dependencies.
4
+ * All commands import from here instead of docguard.mjs.
5
+ */
6
+
7
+ // ── Colors (ANSI escape codes, zero deps) ──────────────────────────────────
8
+ export const c = {
9
+ reset: '\x1b[0m',
10
+ bold: '\x1b[1m',
11
+ dim: '\x1b[2m',
12
+ red: '\x1b[31m',
13
+ green: '\x1b[32m',
14
+ yellow: '\x1b[33m',
15
+ blue: '\x1b[34m',
16
+ cyan: '\x1b[36m',
17
+ white: '\x1b[37m',
18
+ bgRed: '\x1b[41m',
19
+ bgGreen: '\x1b[42m',
20
+ bgYellow: '\x1b[43m',
21
+ };
22
+
23
+ // ── Compliance Profiles ───────────────────────────────────────────────────
24
+ export const PROFILES = {
25
+ starter: {
26
+ description: 'Minimal CDD — just architecture + changelog. For side projects and prototypes.',
27
+ requiredFiles: {
28
+ canonical: [
29
+ 'docs-canonical/ARCHITECTURE.md',
30
+ ],
31
+ agentFile: ['AGENTS.md', 'CLAUDE.md'],
32
+ changelog: 'CHANGELOG.md',
33
+ driftLog: 'DRIFT-LOG.md',
34
+ },
35
+ validators: {
36
+ structure: true,
37
+ docsSync: true,
38
+ drift: false,
39
+ changelog: true,
40
+ architecture: false,
41
+ testSpec: false,
42
+ security: false,
43
+ environment: false,
44
+ freshness: false,
45
+ },
46
+ },
47
+ standard: {
48
+ description: 'Full CDD — all 5 canonical docs. For team projects.',
49
+ // Uses the defaults — no overrides needed
50
+ },
51
+ enterprise: {
52
+ description: 'Strict CDD — all docs, all validators, freshness enforced. For regulated/enterprise projects.',
53
+ validators: {
54
+ structure: true,
55
+ docsSync: true,
56
+ drift: true,
57
+ changelog: true,
58
+ architecture: true,
59
+ testSpec: true,
60
+ security: true,
61
+ environment: true,
62
+ freshness: true,
63
+ },
64
+ },
65
+ };
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Docs-Diff Validator — Checks alignment between canonical docs and code.
3
+ *
4
+ * Runs as part of `docguard guard` on every invocation.
5
+ * Detects undocumented code artifacts and documented items not found in code.
6
+ * Returns warnings (not errors) since drift is a soft signal.
7
+ */
8
+
9
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
10
+ import { resolve, join, extname, basename } from 'node:path';
11
+
12
+ const IGNORE_DIRS = new Set([
13
+ 'node_modules', '.git', '.next', 'dist', 'build',
14
+ 'coverage', '.cache', '__pycache__', '.venv', 'vendor',
15
+ 'docs-canonical', 'docs-implementation', 'templates',
16
+ ]);
17
+
18
+ const CODE_EXTENSIONS = new Set([
19
+ '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
20
+ '.py', '.java', '.go', '.rs', '.rb', '.php',
21
+ ]);
22
+
23
+ /**
24
+ * Validate doc-code alignment — compares canonical docs vs source code.
25
+ * @returns {{ errors: string[], warnings: string[], passed: number, total: number }}
26
+ */
27
+ export function validateDocsDiff(projectDir, config) {
28
+ const warnings = [];
29
+ let passed = 0;
30
+ let total = 0;
31
+
32
+ const checks = [
33
+ diffTechStack(projectDir),
34
+ diffEnvVars(projectDir),
35
+ diffTests(projectDir),
36
+ ];
37
+
38
+ for (const result of checks) {
39
+ if (!result) continue;
40
+
41
+ total++;
42
+ const undocumented = result.onlyInCode.length;
43
+ const stale = result.onlyInDocs.length;
44
+
45
+ if (undocumented === 0 && stale === 0) {
46
+ passed++;
47
+ } else {
48
+ const parts = [];
49
+ if (undocumented > 0) parts.push(`${undocumented} in code but not documented`);
50
+ if (stale > 0) parts.push(`${stale} documented but not found in code`);
51
+ warnings.push(`${result.title} drift: ${parts.join(', ')}`);
52
+ }
53
+ }
54
+
55
+ return { errors: [], warnings, passed, total };
56
+ }
57
+
58
+ // ── Diff Functions (lightweight versions for validator) ──────────────────
59
+
60
+ function diffTechStack(dir) {
61
+ const archPath = resolve(dir, 'docs-canonical/ARCHITECTURE.md');
62
+ const pkgPath = resolve(dir, 'package.json');
63
+ if (!existsSync(archPath) || !existsSync(pkgPath)) return null;
64
+
65
+ const archContent = readFileSync(archPath, 'utf-8');
66
+ let pkg;
67
+ try { pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); } catch { return null; }
68
+
69
+ const docTech = new Set();
70
+ const techPatterns = ['React', 'Next.js', 'Vue', 'Angular', 'Svelte', 'Express', 'Fastify', 'Hono',
71
+ 'PostgreSQL', 'MySQL', 'MongoDB', 'DynamoDB', 'Redis', 'Prisma', 'Drizzle',
72
+ 'TypeScript', 'Tailwind', 'Docker', 'Terraform'];
73
+
74
+ for (const tech of techPatterns) {
75
+ if (archContent.toLowerCase().includes(tech.toLowerCase())) {
76
+ docTech.add(tech);
77
+ }
78
+ }
79
+
80
+ const codeTech = new Set();
81
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
82
+ const depMap = {
83
+ 'react': 'React', 'next': 'Next.js', 'vue': 'Vue', 'express': 'Express',
84
+ 'fastify': 'Fastify', 'hono': 'Hono', 'prisma': 'Prisma', '@prisma/client': 'Prisma',
85
+ 'drizzle-orm': 'Drizzle', 'typescript': 'TypeScript', 'tailwindcss': 'Tailwind',
86
+ 'redis': 'Redis', 'ioredis': 'Redis', 'pg': 'PostgreSQL', 'mysql2': 'MySQL',
87
+ 'mongoose': 'MongoDB', '@aws-sdk/client-dynamodb': 'DynamoDB',
88
+ };
89
+
90
+ for (const [dep, tech] of Object.entries(depMap)) {
91
+ if (allDeps[dep]) codeTech.add(tech);
92
+ }
93
+
94
+ if (docTech.size === 0 && codeTech.size === 0) return null;
95
+
96
+ return {
97
+ title: 'Tech Stack',
98
+ onlyInDocs: [...docTech].filter(t => !codeTech.has(t)),
99
+ onlyInCode: [...codeTech].filter(t => !docTech.has(t)),
100
+ };
101
+ }
102
+
103
+ function diffEnvVars(dir) {
104
+ const envDocPath = resolve(dir, 'docs-canonical/ENVIRONMENT.md');
105
+ if (!existsSync(envDocPath)) return null;
106
+
107
+ const content = readFileSync(envDocPath, 'utf-8');
108
+ const docVars = new Set();
109
+ const varRegex = /`([A-Z][A-Z0-9_]{2,})`/g;
110
+ let match;
111
+ while ((match = varRegex.exec(content)) !== null) {
112
+ docVars.add(match[1]);
113
+ }
114
+
115
+ const codeVars = new Set();
116
+ const envExamplePath = resolve(dir, '.env.example');
117
+ if (existsSync(envExamplePath)) {
118
+ const envContent = readFileSync(envExamplePath, 'utf-8');
119
+ const envRegex = /^([A-Z][A-Z0-9_]+)\s*=/gm;
120
+ while ((match = envRegex.exec(envContent)) !== null) {
121
+ codeVars.add(match[1]);
122
+ }
123
+ }
124
+
125
+ if (docVars.size === 0 && codeVars.size === 0) return null;
126
+
127
+ return {
128
+ title: 'Environment Variables',
129
+ onlyInDocs: [...docVars].filter(v => !codeVars.has(v)),
130
+ onlyInCode: [...codeVars].filter(v => !docVars.has(v)),
131
+ };
132
+ }
133
+
134
+ function diffTests(dir) {
135
+ const testSpecPath = resolve(dir, 'docs-canonical/TEST-SPEC.md');
136
+ if (!existsSync(testSpecPath)) return null;
137
+
138
+ const content = readFileSync(testSpecPath, 'utf-8');
139
+ const docTests = new Set();
140
+ const testFileRegex = /`([^`]*\.(test|spec)\.[^`]+)`/g;
141
+ let match;
142
+ while ((match = testFileRegex.exec(content)) !== null) {
143
+ docTests.add(match[1]);
144
+ }
145
+
146
+ const codeTests = new Set();
147
+ const testDirs = ['tests', 'test', '__tests__', 'spec', 'e2e'];
148
+ for (const td of testDirs) {
149
+ const testDir = resolve(dir, td);
150
+ if (!existsSync(testDir)) continue;
151
+ const files = getFilesRecursive(testDir);
152
+ for (const f of files) {
153
+ codeTests.add(f.replace(dir + '/', ''));
154
+ }
155
+ }
156
+
157
+ if (docTests.size === 0 && codeTests.size === 0) return null;
158
+
159
+ return {
160
+ title: 'Test Files',
161
+ onlyInDocs: [...docTests].filter(t => !codeTests.has(t)),
162
+ onlyInCode: [...codeTests].filter(t => !docTests.has(t)),
163
+ };
164
+ }
165
+
166
+ function getFilesRecursive(dir) {
167
+ const results = [];
168
+ if (!existsSync(dir)) return results;
169
+ let entries;
170
+ try { entries = readdirSync(dir); } catch { return results; }
171
+
172
+ for (const entry of entries) {
173
+ if (IGNORE_DIRS.has(entry) || entry.startsWith('.')) continue;
174
+ const fullPath = join(dir, entry);
175
+ try {
176
+ const stat = statSync(fullPath);
177
+ if (stat.isDirectory()) {
178
+ results.push(...getFilesRecursive(fullPath));
179
+ } else if (stat.isFile() && CODE_EXTENSIONS.has(extname(fullPath))) {
180
+ results.push(fullPath);
181
+ }
182
+ } catch { /* skip */ }
183
+ }
184
+ return results;
185
+ }
@@ -37,6 +37,7 @@ export function validateTestSpec(projectDir, config) {
37
37
  if (cells.length < 3) continue;
38
38
 
39
39
  const sourceFile = cells[0];
40
+ const testFile = cells[1];
40
41
  const status = cells[cells.length - 1]; // Last column is always status
41
42
 
42
43
  // Skip template/example rows and italic placeholder rows
@@ -56,6 +57,38 @@ export function validateTestSpec(projectDir, config) {
56
57
  results.total++;
57
58
  results.passed++;
58
59
  }
60
+
61
+ // ── File existence checks ───────────────────────────────────────
62
+ // Verify source file still exists (catch stale map entries)
63
+ const cleanSource = sourceFile.replace(/`/g, '').trim();
64
+ if (cleanSource && cleanSource !== '—' && cleanSource !== 'Source File') {
65
+ const sourcePath = resolve(projectDir, cleanSource);
66
+ if (!existsSync(sourcePath)) {
67
+ results.total++;
68
+ results.warnings.push(
69
+ `Source-to-Test Map: source file \`${cleanSource}\` not found on disk — stale entry?`
70
+ );
71
+ } else {
72
+ results.total++;
73
+ results.passed++;
74
+ }
75
+ }
76
+
77
+ // Verify test file exists (catch wrong/stale test references)
78
+ const cleanTest = testFile ? testFile.replace(/`/g, '').trim() : '';
79
+ if (cleanTest && cleanTest !== '—' && cleanTest !== 'Test File' &&
80
+ cleanTest !== 'Unit Test' && !cleanTest.includes('N/A')) {
81
+ const testPath = resolve(projectDir, cleanTest);
82
+ if (!existsSync(testPath)) {
83
+ results.total++;
84
+ results.warnings.push(
85
+ `Source-to-Test Map: test file \`${cleanTest}\` not found — referenced by ${cleanSource}`
86
+ );
87
+ } else {
88
+ results.total++;
89
+ results.passed++;
90
+ }
91
+ }
59
92
  }
60
93
  }
61
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
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": {
@@ -53,3 +53,16 @@
53
53
  |--------|---------------|------|
54
54
  | Health | <!-- e.g. /health returns 200 --> | |
55
55
  | Auth | <!-- e.g. Login flow completes --> | |
56
+
57
+ ## Recommended Test Patterns
58
+
59
+ <!-- DocGuard validates that files listed in the Source-to-Test Map exist on disk.
60
+ Keep the map up to date — stale entries will trigger warnings. -->
61
+
62
+ | Pattern | Description | Priority |
63
+ |---------|-------------|----------|
64
+ | Config-awareness | Test behavior changes per `.docguard.json` / env config | ⚠️ High |
65
+ | Individual functions | Test each module/function directly, not just via CLI | ⚠️ High |
66
+ | Edge cases | Empty inputs, missing files, invalid config | ✅ Medium |
67
+ | Error paths | Verify graceful failure, not just happy path | ✅ Medium |
68
+ | Regression guards | Pin specific bug fixes with dedicated tests | ✅ Medium |
@@ -1,92 +0,0 @@
1
- /**
2
- * Audit Command — Scan project, report what CDD docs exist or are missing
3
- * Now uses the full documentTypes config to show ALL docs with categories.
4
- */
5
-
6
- import { existsSync } from 'node:fs';
7
- import { resolve } from 'node:path';
8
- import { c } from '../docguard.mjs';
9
-
10
- export function runAudit(projectDir, config, flags) {
11
- console.log(`${c.bold}📋 DocGuard Audit — ${config.projectName}${c.reset}`);
12
- console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
13
-
14
- const results = { found: 0, missing: 0, optional: 0, total: 0, details: [] };
15
-
16
- // Use documentTypes from config (all 16 doc types with required/optional)
17
- const docTypes = config.documentTypes || {};
18
-
19
- // Group by category
20
- const categories = {};
21
- for (const [filePath, meta] of Object.entries(docTypes)) {
22
- const cat = meta.category || 'other';
23
- if (!categories[cat]) categories[cat] = [];
24
- categories[cat].push({ filePath, ...meta });
25
- }
26
-
27
- // Category display names and order
28
- const categoryLabels = {
29
- canonical: '📘 Canonical Documentation (Design Intent)',
30
- implementation: '📗 Implementation Documentation (Current State)',
31
- agent: '🤖 Agent Instructions',
32
- tracking: '📑 Change Tracking',
33
- };
34
-
35
- const categoryOrder = ['canonical', 'implementation', 'agent', 'tracking'];
36
-
37
- for (const cat of categoryOrder) {
38
- const docs = categories[cat];
39
- if (!docs || docs.length === 0) continue;
40
-
41
- console.log(`${c.bold} ${categoryLabels[cat] || cat}${c.reset}`);
42
-
43
- for (const doc of docs) {
44
- const fullPath = resolve(projectDir, doc.filePath);
45
- const exists = existsSync(fullPath);
46
-
47
- if (exists) {
48
- results.found++;
49
- results.total++;
50
- results.details.push({ file: doc.filePath, status: 'found', required: doc.required });
51
- console.log(` ${c.green}✅${c.reset} ${doc.filePath} ${c.dim}— ${doc.description}${c.reset}`);
52
- } else if (doc.required) {
53
- results.missing++;
54
- results.total++;
55
- results.details.push({ file: doc.filePath, status: 'missing', required: true });
56
- console.log(` ${c.red}❌${c.reset} ${doc.filePath} ${c.dim}— ${doc.description}${c.reset} ${c.red}(required)${c.reset}`);
57
- } else {
58
- results.optional++;
59
- results.details.push({ file: doc.filePath, status: 'optional', required: false });
60
- if (flags.verbose) {
61
- console.log(` ${c.dim}○ ${doc.filePath} — ${doc.description} (optional)${c.reset}`);
62
- }
63
- }
64
- }
65
- console.log('');
66
- }
67
-
68
- // Score (only required files)
69
- const requiredTotal = results.found + results.missing;
70
- const pct = requiredTotal === 0 ? 100 : Math.round((results.found / requiredTotal) * 100);
71
- const scoreColor = pct >= 80 ? c.green : pct >= 50 ? c.yellow : c.red;
72
-
73
- console.log(`${c.bold} ─────────────────────────────────────${c.reset}`);
74
- console.log(` ${c.bold}Required:${c.reset} ${scoreColor}${results.found - (results.found - requiredTotal + results.missing)}/${requiredTotal} files (${pct}%)${c.reset}`);
75
-
76
- // Show optional count
77
- if (!flags.verbose && results.optional > 0) {
78
- console.log(` ${c.dim}Optional: ${results.optional} not present (use --verbose to see all)${c.reset}`);
79
- }
80
-
81
- if (results.missing > 0) {
82
- console.log(`\n ${c.yellow}💡 Run ${c.cyan}docguard init${c.yellow} to create missing docs from templates.${c.reset}`);
83
- console.log(` ${c.yellow}💡 Run ${c.cyan}docguard generate${c.yellow} to auto-fill docs from your codebase.${c.reset}`);
84
- } else {
85
- console.log(`\n ${c.green}🎉 All required CDD documentation present!${c.reset}`);
86
- console.log(` ${c.dim}Run ${c.cyan}docguard guard${c.dim} to validate content alignment.${c.reset}`);
87
- console.log(` ${c.dim}Run ${c.cyan}docguard score${c.dim} to check your CDD maturity.${c.reset}`);
88
- }
89
-
90
- console.log('');
91
- return results;
92
- }