docguard-cli 0.5.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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/PHILOSOPHY.md +150 -0
  3. package/README.md +309 -0
  4. package/STANDARD.md +751 -0
  5. package/cli/commands/agents.mjs +221 -0
  6. package/cli/commands/audit.mjs +92 -0
  7. package/cli/commands/badge.mjs +72 -0
  8. package/cli/commands/ci.mjs +80 -0
  9. package/cli/commands/diagnose.mjs +273 -0
  10. package/cli/commands/diff.mjs +360 -0
  11. package/cli/commands/fix.mjs +610 -0
  12. package/cli/commands/generate.mjs +842 -0
  13. package/cli/commands/guard.mjs +158 -0
  14. package/cli/commands/hooks.mjs +227 -0
  15. package/cli/commands/init.mjs +249 -0
  16. package/cli/commands/score.mjs +396 -0
  17. package/cli/commands/watch.mjs +143 -0
  18. package/cli/docguard.mjs +458 -0
  19. package/cli/validators/architecture.mjs +380 -0
  20. package/cli/validators/changelog.mjs +39 -0
  21. package/cli/validators/docs-sync.mjs +110 -0
  22. package/cli/validators/drift.mjs +101 -0
  23. package/cli/validators/environment.mjs +70 -0
  24. package/cli/validators/freshness.mjs +224 -0
  25. package/cli/validators/security.mjs +101 -0
  26. package/cli/validators/structure.mjs +88 -0
  27. package/cli/validators/test-spec.mjs +115 -0
  28. package/docs/ai-integration.md +179 -0
  29. package/docs/commands.md +239 -0
  30. package/docs/configuration.md +96 -0
  31. package/docs/faq.md +155 -0
  32. package/docs/installation.md +81 -0
  33. package/docs/profiles.md +103 -0
  34. package/docs/quickstart.md +79 -0
  35. package/package.json +57 -0
  36. package/templates/ADR.md.template +64 -0
  37. package/templates/AGENTS.md.template +88 -0
  38. package/templates/ARCHITECTURE.md.template +78 -0
  39. package/templates/CHANGELOG.md.template +16 -0
  40. package/templates/CURRENT-STATE.md.template +64 -0
  41. package/templates/DATA-MODEL.md.template +66 -0
  42. package/templates/DEPLOYMENT.md.template +66 -0
  43. package/templates/DRIFT-LOG.md.template +18 -0
  44. package/templates/ENVIRONMENT.md.template +43 -0
  45. package/templates/KNOWN-GOTCHAS.md.template +69 -0
  46. package/templates/ROADMAP.md.template +82 -0
  47. package/templates/RUNBOOKS.md.template +115 -0
  48. package/templates/SECURITY.md.template +42 -0
  49. package/templates/TEST-SPEC.md.template +55 -0
  50. package/templates/TROUBLESHOOTING.md.template +96 -0
  51. package/templates/VENDOR-BUGS.md.template +74 -0
  52. package/templates/ci/github-actions.yml +39 -0
  53. package/templates/commands/docguard.fix.md +65 -0
  54. package/templates/commands/docguard.guard.md +40 -0
  55. package/templates/commands/docguard.init.md +62 -0
  56. package/templates/commands/docguard.review.md +44 -0
  57. package/templates/commands/docguard.update.md +44 -0
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Score Command — Calculate CDD maturity score (0-100)
3
+ * Shows category breakdown with weighted scoring.
4
+ */
5
+
6
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
7
+ import { resolve, join, extname } from 'node:path';
8
+ import { execSync } from 'node:child_process';
9
+ import { c } from '../docguard.mjs';
10
+
11
+ const WEIGHTS = {
12
+ structure: 25, // Required files exist
13
+ docQuality: 20, // Docs have required sections + content
14
+ testing: 15, // Test spec alignment
15
+ security: 10, // No hardcoded secrets, .gitignore
16
+ environment: 10, // Env docs, .env.example
17
+ drift: 10, // Drift tracking discipline
18
+ changelog: 5, // Changelog maintenance
19
+ architecture: 5, // Layer boundary compliance
20
+ };
21
+
22
+ export function runScore(projectDir, config, flags) {
23
+ console.log(`${c.bold}📊 DocGuard Score — ${config.projectName}${c.reset}`);
24
+ console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
25
+
26
+ const { scores, totalScore, grade } = calcAllScores(projectDir, config);
27
+
28
+ // ── Display Results ──
29
+ if (flags.format === 'json') {
30
+ const result = {
31
+ project: config.projectName,
32
+ score: totalScore,
33
+ grade,
34
+ categories: {},
35
+ };
36
+ for (const [cat, score] of Object.entries(scores)) {
37
+ result.categories[cat] = {
38
+ score,
39
+ weight: WEIGHTS[cat],
40
+ weighted: Math.round((score / 100) * WEIGHTS[cat]),
41
+ };
42
+ }
43
+ console.log(JSON.stringify(result, null, 2));
44
+ return;
45
+ }
46
+
47
+ // Visual display
48
+ console.log(` ${c.bold}Category Breakdown${c.reset}\n`);
49
+
50
+ for (const [category, score] of Object.entries(scores)) {
51
+ const bar = renderBar(score);
52
+ const label = category.padEnd(14);
53
+ const weight = `(×${WEIGHTS[category]})`.padEnd(5);
54
+ const weighted = Math.round((score / 100) * WEIGHTS[category]);
55
+ console.log(` ${label} ${bar} ${score}% ${c.dim}${weight} = ${weighted} pts${c.reset}`);
56
+ }
57
+
58
+ console.log(`\n${c.bold} ─────────────────────────────────────${c.reset}`);
59
+
60
+ const gradeColor = totalScore >= 80 ? c.green : totalScore >= 60 ? c.yellow : c.red;
61
+ console.log(` ${gradeColor}${c.bold}CDD Maturity Score: ${totalScore}/100 (${grade})${c.reset}`);
62
+
63
+ // Grade description
64
+ const descriptions = {
65
+ 'A+': 'Excellent — CDD fully adopted',
66
+ 'A': 'Great — Strong CDD compliance',
67
+ 'B': 'Good — Most CDD practices in place',
68
+ 'C': 'Fair — Partial CDD adoption',
69
+ 'D': 'Needs Work — Significant gaps',
70
+ 'F': 'Not Started — Run `docguard init` first',
71
+ };
72
+ console.log(` ${c.dim}${descriptions[grade]}${c.reset}\n`);
73
+
74
+ // Suggestions
75
+ const weakest = Object.entries(scores)
76
+ .filter(([, s]) => s < 100)
77
+ .sort((a, b) => a[1] - b[1])
78
+ .slice(0, 3);
79
+
80
+ if (weakest.length > 0) {
81
+ console.log(` ${c.bold}Top improvements:${c.reset}`);
82
+ for (const [cat, score] of weakest) {
83
+ const suggestion = getSuggestion(cat, score);
84
+ console.log(` ${c.yellow}→ ${cat}${c.reset}: ${suggestion}`);
85
+ }
86
+ console.log('');
87
+ }
88
+
89
+ // ── Tax Estimate (--tax flag) ──
90
+ if (flags.tax) {
91
+ const tax = estimateDocTax(projectDir, config, scores);
92
+ const taxColor = tax.level === 'LOW' ? c.green : tax.level === 'MEDIUM' ? c.yellow : c.red;
93
+
94
+ console.log(` ${c.bold}📋 Documentation Tax Estimate${c.reset}`);
95
+ console.log(` ${c.dim}─────────────────────────────────${c.reset}`);
96
+ console.log(` Tracked docs: ${c.cyan}${tax.docCount}${c.reset} files`);
97
+ console.log(` Active profile: ${c.cyan}${config.profile || 'standard'}${c.reset}`);
98
+ console.log(` Est. maintenance: ${c.bold}~${tax.minutesPerWeek} min/week${c.reset}`);
99
+ console.log(` Tax-to-value ratio: ${taxColor}${c.bold}${tax.level}${c.reset}`);
100
+ console.log(` ${c.dim}${tax.recommendation}${c.reset}\n`);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Internal scoring — returns data without printing.
106
+ * Used by badge, ci, and other commands that need the score.
107
+ */
108
+ export function runScoreInternal(projectDir, config) {
109
+ const { scores, totalScore, grade } = calcAllScores(projectDir, config);
110
+ return { score: totalScore, grade, categories: scores };
111
+ }
112
+
113
+ function calcAllScores(projectDir, config) {
114
+ const scores = {};
115
+ scores.structure = calcStructureScore(projectDir, config);
116
+ scores.docQuality = calcDocQualityScore(projectDir, config);
117
+ scores.testing = calcTestingScore(projectDir, config);
118
+ scores.security = calcSecurityScore(projectDir, config);
119
+ scores.environment = calcEnvironmentScore(projectDir, config);
120
+ scores.drift = calcDriftScore(projectDir, config);
121
+ scores.changelog = calcChangelogScore(projectDir, config);
122
+ scores.architecture = calcArchitectureScore(projectDir, config);
123
+
124
+ let totalScore = 0;
125
+ for (const [category, score] of Object.entries(scores)) {
126
+ totalScore += (score / 100) * WEIGHTS[category];
127
+ }
128
+ totalScore = Math.round(totalScore);
129
+
130
+ return { scores, totalScore, grade: getGrade(totalScore) };
131
+ }
132
+
133
+ // ── Scoring Functions ──────────────────────────────────────────────────────
134
+
135
+ function calcStructureScore(dir, config) {
136
+ let found = 0;
137
+ let total = 0;
138
+
139
+ for (const file of config.requiredFiles.canonical) {
140
+ total++;
141
+ if (existsSync(resolve(dir, file))) found++;
142
+ }
143
+
144
+ total++;
145
+ const hasAgent = config.requiredFiles.agentFile.some(f => existsSync(resolve(dir, f)));
146
+ if (hasAgent) found++;
147
+
148
+ total++;
149
+ if (existsSync(resolve(dir, config.requiredFiles.changelog))) found++;
150
+
151
+ total++;
152
+ if (existsSync(resolve(dir, config.requiredFiles.driftLog))) found++;
153
+
154
+ return total === 0 ? 0 : Math.round((found / total) * 100);
155
+ }
156
+
157
+ function calcDocQualityScore(dir, config) {
158
+ const checks = {
159
+ 'docs-canonical/ARCHITECTURE.md': ['## System Overview', '## Component Map', '## Tech Stack'],
160
+ 'docs-canonical/DATA-MODEL.md': ['## Entities'],
161
+ 'docs-canonical/SECURITY.md': ['## Authentication', '## Secrets Management'],
162
+ 'docs-canonical/TEST-SPEC.md': ['## Test Categories', '## Coverage Rules'],
163
+ 'docs-canonical/ENVIRONMENT.md': ['## Environment Variables', '## Setup Steps'],
164
+ };
165
+
166
+ let found = 0;
167
+ let total = 0;
168
+
169
+ for (const [file, sections] of Object.entries(checks)) {
170
+ const fullPath = resolve(dir, file);
171
+ if (!existsSync(fullPath)) continue;
172
+
173
+ const content = readFileSync(fullPath, 'utf-8');
174
+
175
+ for (const section of sections) {
176
+ total++;
177
+ if (content.includes(section)) found++;
178
+ }
179
+
180
+ // Bonus: check if doc has docguard metadata
181
+ total++;
182
+ if (content.includes('docguard:version')) found++;
183
+
184
+ // Bonus: check if doc has more than just template placeholders
185
+ total++;
186
+ const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#') && !l.startsWith('|') && !l.startsWith('>') && !l.startsWith('<!--'));
187
+ if (lines.length > 5) found++;
188
+ }
189
+
190
+ return total === 0 ? 0 : Math.round((found / total) * 100);
191
+ }
192
+
193
+ function calcTestingScore(dir) {
194
+ let score = 0;
195
+
196
+ // Check test directory exists
197
+ const testDirs = ['tests', 'test', '__tests__', 'spec', 'e2e'];
198
+ const hasTestDir = testDirs.some(d => existsSync(resolve(dir, d)));
199
+ if (hasTestDir) score += 40;
200
+
201
+ // Check test spec exists
202
+ if (existsSync(resolve(dir, 'docs-canonical/TEST-SPEC.md'))) score += 30;
203
+
204
+ // Check for test config files
205
+ const testConfigs = ['jest.config.js', 'jest.config.ts', 'vitest.config.ts', 'vitest.config.js', 'pytest.ini', 'setup.cfg', '.mocharc.yml'];
206
+ const hasTestConfig = testConfigs.some(f => existsSync(resolve(dir, f)));
207
+ if (hasTestConfig) score += 15;
208
+
209
+ // Check for CI test step
210
+ const ciFiles = ['.github/workflows/ci.yml', '.github/workflows/test.yml'];
211
+ const hasCITest = ciFiles.some(f => existsSync(resolve(dir, f)));
212
+ if (hasCITest) score += 15;
213
+
214
+ return Math.min(100, score);
215
+ }
216
+
217
+ function calcSecurityScore(dir) {
218
+ let score = 0;
219
+
220
+ // SECURITY.md exists
221
+ if (existsSync(resolve(dir, 'docs-canonical/SECURITY.md'))) score += 30;
222
+
223
+ // .gitignore exists and includes .env
224
+ const gitignorePath = resolve(dir, '.gitignore');
225
+ if (existsSync(gitignorePath)) {
226
+ score += 20;
227
+ const content = readFileSync(gitignorePath, 'utf-8');
228
+ if (content.includes('.env')) score += 20;
229
+ }
230
+
231
+ // No .env file committed (check if .env exists but .gitignore covers it)
232
+ if (!existsSync(resolve(dir, '.env')) || existsSync(gitignorePath)) score += 15;
233
+
234
+ // .env.example exists (safe template)
235
+ if (existsSync(resolve(dir, '.env.example'))) score += 15;
236
+
237
+ return Math.min(100, score);
238
+ }
239
+
240
+ function calcEnvironmentScore(dir) {
241
+ let score = 0;
242
+
243
+ if (existsSync(resolve(dir, 'docs-canonical/ENVIRONMENT.md'))) score += 40;
244
+ if (existsSync(resolve(dir, '.env.example'))) score += 30;
245
+
246
+ // Check for setup documentation
247
+ const readmePath = resolve(dir, 'README.md');
248
+ if (existsSync(readmePath)) {
249
+ const content = readFileSync(readmePath, 'utf-8');
250
+ if (content.includes('## Setup') || content.includes('## Getting Started') || content.includes('Quick Start')) {
251
+ score += 30;
252
+ } else {
253
+ score += 15; // README exists but no setup section
254
+ }
255
+ }
256
+
257
+ return Math.min(100, score);
258
+ }
259
+
260
+ function calcDriftScore(dir, config) {
261
+ // Perfect score if drift log exists and no unlogged drift comments
262
+ if (!existsSync(resolve(dir, config.requiredFiles.driftLog))) return 0;
263
+
264
+ let score = 50; // Drift log exists
265
+
266
+ const content = readFileSync(resolve(dir, config.requiredFiles.driftLog), 'utf-8');
267
+
268
+ // Has structure (headers)
269
+ if (content.includes('## ') || content.includes('| ')) score += 25;
270
+
271
+ // Has entries (not just template)
272
+ const lines = content.split('\n').filter(l => l.trim() && !l.startsWith('#') && !l.startsWith('<!--'));
273
+ if (lines.length > 3) score += 25;
274
+
275
+ return Math.min(100, score);
276
+ }
277
+
278
+ function calcChangelogScore(dir, config) {
279
+ const path = resolve(dir, config.requiredFiles.changelog);
280
+ if (!existsSync(path)) return 0;
281
+
282
+ let score = 40; // Exists
283
+ const content = readFileSync(path, 'utf-8');
284
+
285
+ if (content.includes('[Unreleased]') || content.includes('[unreleased]')) score += 30;
286
+ if (/## \[[\d.]+\]/.test(content)) score += 30;
287
+
288
+ return Math.min(100, score);
289
+ }
290
+
291
+ function calcArchitectureScore(dir) {
292
+ const archPath = resolve(dir, 'docs-canonical/ARCHITECTURE.md');
293
+ if (!existsSync(archPath)) return 0;
294
+
295
+ let score = 30;
296
+ const content = readFileSync(archPath, 'utf-8');
297
+
298
+ if (content.includes('## Layer Boundaries') || content.includes('## Component Map')) score += 25;
299
+ if (content.includes('```mermaid') || content.includes('graph ')) score += 20;
300
+ if (content.includes('## External Dependencies')) score += 15;
301
+ if (content.includes('## Revision History')) score += 10;
302
+
303
+ return Math.min(100, score);
304
+ }
305
+
306
+ // ── Helpers ────────────────────────────────────────────────────────────────
307
+
308
+ function renderBar(score) {
309
+ const filled = Math.round(score / 5);
310
+ const empty = 20 - filled;
311
+ const color = score >= 80 ? c.green : score >= 60 ? c.yellow : c.red;
312
+ return `${color}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset}`;
313
+ }
314
+
315
+ function getGrade(score) {
316
+ if (score >= 95) return 'A+';
317
+ if (score >= 80) return 'A';
318
+ if (score >= 65) return 'B';
319
+ if (score >= 50) return 'C';
320
+ if (score >= 30) return 'D';
321
+ return 'F';
322
+ }
323
+
324
+ function getSuggestion(category, score) {
325
+ const suggestions = {
326
+ structure: 'Run `docguard init` to create missing documentation',
327
+ docQuality: 'Fill in template sections — replace placeholders with real content',
328
+ testing: 'Add tests/ directory and configure TEST-SPEC.md',
329
+ security: 'Create SECURITY.md and add .env to .gitignore',
330
+ environment: 'Document env variables and create .env.example',
331
+ drift: 'Create DRIFT-LOG.md and log any code deviations',
332
+ changelog: 'Maintain CHANGELOG.md with [Unreleased] section',
333
+ architecture: 'Add layer boundaries and Mermaid diagrams to ARCHITECTURE.md',
334
+ };
335
+ return suggestions[category] || 'Review and improve this area';
336
+ }
337
+
338
+ /**
339
+ * Estimate documentation maintenance "tax" — how much time docs cost per week.
340
+ * Based on: doc count, code churn, and current doc quality scores.
341
+ */
342
+ function estimateDocTax(projectDir, config, scores) {
343
+ // Count tracked docs
344
+ const canonicalDir = resolve(projectDir, 'docs-canonical');
345
+ let docCount = 0;
346
+ if (existsSync(canonicalDir)) {
347
+ try {
348
+ docCount = readdirSync(canonicalDir).filter(f => f.endsWith('.md')).length;
349
+ } catch { /* ignore */ }
350
+ }
351
+ // Add root tracking files
352
+ if (existsSync(resolve(projectDir, 'CHANGELOG.md'))) docCount++;
353
+ if (existsSync(resolve(projectDir, 'DRIFT-LOG.md'))) docCount++;
354
+
355
+ // Estimate code churn (commits in last 30 days)
356
+ let recentCommits = 0;
357
+ try {
358
+ const output = execSync('git log --oneline --since="30 days ago" 2>/dev/null | wc -l', {
359
+ cwd: projectDir,
360
+ encoding: 'utf-8',
361
+ }).trim();
362
+ recentCommits = parseInt(output, 10) || 0;
363
+ } catch {
364
+ recentCommits = 10; // Default assumption
365
+ }
366
+
367
+ // Calculate estimated minutes per week
368
+ // Base: ~3 min per tracked doc per week (review time)
369
+ // Churn multiplier: more commits = more potential doc updates
370
+ const baseMinutes = docCount * 3;
371
+ const churnMultiplier = recentCommits > 50 ? 1.5 : recentCommits > 20 ? 1.2 : 1.0;
372
+
373
+ // Quality discount: higher scores = less rework needed
374
+ const avgScore = Object.values(scores).reduce((a, b) => a + b, 0) / Object.values(scores).length;
375
+ const qualityDiscount = avgScore > 80 ? 0.7 : avgScore > 60 ? 0.85 : 1.0;
376
+
377
+ // AI discount: if docs are AI-maintained, tax drops significantly
378
+ const aiDiscount = 0.3; // AI writes ~70% of docs
379
+
380
+ const minutesPerWeek = Math.max(5, Math.round(baseMinutes * churnMultiplier * qualityDiscount * aiDiscount));
381
+
382
+ // Determine tax level
383
+ let level, recommendation;
384
+ if (minutesPerWeek <= 10) {
385
+ level = 'LOW';
386
+ recommendation = 'Docs save more time than they cost. Current setup is sustainable.';
387
+ } else if (minutesPerWeek <= 25) {
388
+ level = 'MEDIUM';
389
+ recommendation = 'Consider using `docguard fix --doc` to let AI handle updates.';
390
+ } else {
391
+ level = 'HIGH';
392
+ recommendation = 'Consider switching to "starter" profile to reduce doc overhead.';
393
+ }
394
+
395
+ return { docCount, minutesPerWeek, level, recommendation };
396
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Watch Command — Live mode that watches for file changes and re-runs guard
3
+ *
4
+ * Like `jest --watch` but for CDD compliance.
5
+ * Uses Node.js fs.watch (zero dependencies).
6
+ *
7
+ * --auto-fix: When guard finds issues, output AI fix prompts automatically.
8
+ */
9
+
10
+ import { watch as fsWatch, existsSync, readdirSync, statSync } from 'node:fs';
11
+ import { resolve, relative, extname } from 'node:path';
12
+ import { c } from '../docguard.mjs';
13
+ import { runGuardInternal } from './guard.mjs';
14
+
15
+ const DEBOUNCE_MS = 500;
16
+ const IGNORE_DIRS = new Set([
17
+ 'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
18
+ '.cache', '__pycache__', '.venv', 'vendor', '.turbo',
19
+ ]);
20
+ const WATCH_EXTS = new Set([
21
+ '.md', '.json', '.mjs', '.js', '.ts', '.tsx', '.jsx', '.py',
22
+ ]);
23
+
24
+ export function runWatch(projectDir, config, flags) {
25
+ console.log(`${c.bold}👁️ DocGuard Watch — ${config.projectName}${c.reset}`);
26
+ console.log(`${c.dim} Directory: ${projectDir}${c.reset}`);
27
+ if (flags.autoFix) {
28
+ console.log(`${c.cyan} Mode: auto-fix (will output AI prompts on failures)${c.reset}`);
29
+ }
30
+ console.log(`${c.dim} Watching for changes... (Ctrl+C to stop)${c.reset}\n`);
31
+
32
+ // Run guard immediately on start
33
+ runGuardQuiet(projectDir, config, flags);
34
+
35
+ // Collect directories to watch
36
+ const watchDirs = collectWatchDirs(projectDir);
37
+ console.log(`${c.dim} Watching ${watchDirs.length} directories${c.reset}\n`);
38
+
39
+ let debounceTimer = null;
40
+ let lastChange = '';
41
+
42
+ for (const dir of watchDirs) {
43
+ try {
44
+ fsWatch(dir, { persistent: true }, (eventType, filename) => {
45
+ if (!filename) return;
46
+ const ext = extname(filename);
47
+ if (!WATCH_EXTS.has(ext)) return;
48
+
49
+ const changePath = relative(projectDir, resolve(dir, filename));
50
+ if (changePath === lastChange) return; // skip duplicates
51
+ lastChange = changePath;
52
+
53
+ // Debounce — wait for rapid saves to settle
54
+ if (debounceTimer) clearTimeout(debounceTimer);
55
+ debounceTimer = setTimeout(() => {
56
+ console.log(`\n${c.dim} Changed: ${c.cyan}${changePath}${c.reset}`);
57
+ runGuardQuiet(projectDir, config, flags);
58
+ lastChange = '';
59
+ }, DEBOUNCE_MS);
60
+ });
61
+ } catch {
62
+ // Some directories may not be watchable
63
+ }
64
+ }
65
+
66
+ // Keep process alive
67
+ process.on('SIGINT', () => {
68
+ console.log(`\n${c.dim} Watch stopped.${c.reset}\n`);
69
+ process.exit(0);
70
+ });
71
+ }
72
+
73
+ function runGuardQuiet(projectDir, config, flags) {
74
+ const timestamp = new Date().toLocaleTimeString();
75
+ console.log(`${c.dim} [${timestamp}] Running guard...${c.reset}`);
76
+
77
+ try {
78
+ const data = runGuardInternal(projectDir, config);
79
+
80
+ if (data.status === 'PASS') {
81
+ console.log(` ${c.green}✅ PASS${c.reset} — ${data.passed}/${data.total} checks passed`);
82
+ } else if (data.status === 'WARN') {
83
+ console.log(` ${c.yellow}⚠️ WARN${c.reset} — ${data.passed}/${data.total} passed, ${data.warnings} warning(s)`);
84
+ } else {
85
+ console.log(` ${c.red}❌ FAIL${c.reset} — ${data.passed}/${data.total} passed, ${data.errors} error(s)`);
86
+ }
87
+
88
+ // Auto-fix: output fix prompts for failures
89
+ if (flags.autoFix && data.status !== 'PASS') {
90
+ console.log(`\n ${c.cyan}${c.bold}🤖 Auto-fix prompts:${c.reset}`);
91
+
92
+ for (const v of data.validators) {
93
+ if (v.status === 'pass' || v.status === 'skipped') continue;
94
+
95
+ const docMap = { 'Architecture': 'architecture', 'Security': 'security', 'Test-Spec': 'test-spec', 'Environment': 'environment' };
96
+ const docTarget = docMap[v.name];
97
+
98
+ for (const msg of [...v.errors, ...v.warnings]) {
99
+ console.log(` ${c.yellow}→${c.reset} [${v.name}] ${msg}`);
100
+ if (docTarget) {
101
+ console.log(` ${c.dim}Fix: docguard fix --doc ${docTarget}${c.reset}`);
102
+ }
103
+ }
104
+ }
105
+
106
+ console.log(`\n ${c.dim}Or run: docguard diagnose (for full AI remediation prompt)${c.reset}`);
107
+ }
108
+ } catch (err) {
109
+ console.log(`${c.red} Guard failed: ${err.message}${c.reset}`);
110
+ }
111
+ }
112
+
113
+ function collectWatchDirs(rootDir) {
114
+ const dirs = [rootDir];
115
+
116
+ function walk(dir) {
117
+ try {
118
+ const entries = readdirSync(dir);
119
+ for (const entry of entries) {
120
+ if (IGNORE_DIRS.has(entry)) continue;
121
+ if (entry.startsWith('.')) continue;
122
+
123
+ const fullPath = resolve(dir, entry);
124
+ try {
125
+ const stat = statSync(fullPath);
126
+ if (stat.isDirectory()) {
127
+ dirs.push(fullPath);
128
+ walk(fullPath);
129
+ }
130
+ } catch { /* skip unreadable */ }
131
+ }
132
+ } catch { /* skip unreadable */ }
133
+ }
134
+
135
+ // Always watch docs-canonical explicitly
136
+ const docsDir = resolve(rootDir, 'docs-canonical');
137
+ if (existsSync(docsDir) && !dirs.includes(docsDir)) {
138
+ dirs.push(docsDir);
139
+ }
140
+
141
+ walk(rootDir);
142
+ return dirs;
143
+ }