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,842 @@
1
+ /**
2
+ * Generate Command — Reverse-engineer canonical docs from an existing codebase
3
+ * Scans source code and creates documentation templates pre-filled with project data.
4
+ *
5
+ * This is the "killer feature" — take any project and auto-generate CDD docs.
6
+ */
7
+
8
+ import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, mkdirSync } from 'node:fs';
9
+ import { resolve, join, extname, basename, relative } from 'node:path';
10
+ import { c } from '../docguard.mjs';
11
+
12
+ const IGNORE_DIRS = new Set([
13
+ 'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
14
+ '.cache', '__pycache__', '.venv', 'vendor', '.turbo', '.vercel',
15
+ '.amplify-hosting', '.serverless',
16
+ ]);
17
+
18
+ const CODE_EXTENSIONS = new Set([
19
+ '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
20
+ '.py', '.java', '.go', '.rs', '.rb', '.php', '.cs',
21
+ ]);
22
+
23
+ export function runGenerate(projectDir, config, flags) {
24
+ console.log(`${c.bold}🔮 DocGuard Generate — ${config.projectName}${c.reset}`);
25
+ console.log(`${c.dim} Directory: ${projectDir}${c.reset}`);
26
+ console.log(`${c.dim} Scanning codebase to generate canonical documentation...${c.reset}\n`);
27
+
28
+ // ── 1. Detect Framework/Stack ──
29
+ const stack = detectStack(projectDir);
30
+ console.log(` ${c.bold}Detected Stack:${c.reset}`);
31
+ for (const [category, tech] of Object.entries(stack)) {
32
+ if (tech) console.log(` ${c.cyan}${category}:${c.reset} ${tech}`);
33
+ }
34
+ console.log('');
35
+
36
+ // ── 2. Scan Project Structure ──
37
+ const scan = scanProject(projectDir);
38
+
39
+ // ── 3. Generate Documents ──
40
+ const docsDir = resolve(projectDir, 'docs-canonical');
41
+ if (!existsSync(docsDir)) {
42
+ mkdirSync(docsDir, { recursive: true });
43
+ }
44
+
45
+ let created = 0;
46
+ let skipped = 0;
47
+
48
+ // Generate ARCHITECTURE.md
49
+ const archResult = generateArchitecture(projectDir, config, stack, scan, flags);
50
+ if (archResult) { created++; } else { skipped++; }
51
+
52
+ // Generate DATA-MODEL.md
53
+ const dataResult = generateDataModel(projectDir, config, stack, scan, flags);
54
+ if (dataResult) { created++; } else { skipped++; }
55
+
56
+ // Generate ENVIRONMENT.md
57
+ const envResult = generateEnvironment(projectDir, config, stack, scan, flags);
58
+ if (envResult) { created++; } else { skipped++; }
59
+
60
+ // Generate TEST-SPEC.md
61
+ const testResult = generateTestSpec(projectDir, config, stack, scan, flags);
62
+ if (testResult) { created++; } else { skipped++; }
63
+
64
+ // Generate SECURITY.md
65
+ const secResult = generateSecurity(projectDir, config, stack, scan, flags);
66
+ if (secResult) { created++; } else { skipped++; }
67
+
68
+ // Generate root files
69
+ const rootResults = generateRootFiles(projectDir, config, stack, scan, flags);
70
+ created += rootResults.created;
71
+ skipped += rootResults.skipped;
72
+
73
+ console.log(`\n${c.bold} ─────────────────────────────────────${c.reset}`);
74
+ console.log(` ${c.green}Generated: ${created}${c.reset} Skipped: ${skipped} (already exist)`);
75
+ console.log(`\n ${c.yellow}${c.bold}⚠️ Review all generated docs!${c.reset}`);
76
+ console.log(` ${c.dim}Generated docs are a starting point — review and refine them.${c.reset}`);
77
+ console.log(` ${c.dim}Run ${c.cyan}docguard score${c.dim} to check your CDD maturity.${c.reset}\n`);
78
+ }
79
+
80
+ // ── Stack Detection ────────────────────────────────────────────────────────
81
+
82
+ function detectStack(dir) {
83
+ const stack = {
84
+ language: null,
85
+ framework: null,
86
+ database: null,
87
+ orm: null,
88
+ testing: null,
89
+ hosting: null,
90
+ css: null,
91
+ auth: null,
92
+ };
93
+
94
+ // Check package.json
95
+ const pkgPath = resolve(dir, 'package.json');
96
+ if (existsSync(pkgPath)) {
97
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
98
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
99
+
100
+ // Language
101
+ if (allDeps.typescript) stack.language = `TypeScript ${allDeps.typescript}`;
102
+ else stack.language = 'JavaScript';
103
+
104
+ // Framework
105
+ if (allDeps.next) stack.framework = `Next.js ${allDeps.next}`;
106
+ else if (allDeps.fastify) stack.framework = `Fastify ${allDeps.fastify}`;
107
+ else if (allDeps.express) stack.framework = `Express ${allDeps.express}`;
108
+ else if (allDeps.hono) stack.framework = `Hono ${allDeps.hono}`;
109
+ else if (allDeps.nuxt) stack.framework = `Nuxt ${allDeps.nuxt}`;
110
+ else if (allDeps.svelte || allDeps['@sveltejs/kit']) stack.framework = 'SvelteKit';
111
+ else if (allDeps.react) stack.framework = `React ${allDeps.react}`;
112
+ else if (allDeps.vue) stack.framework = `Vue ${allDeps.vue}`;
113
+ else if (allDeps.angular || allDeps['@angular/core']) stack.framework = 'Angular';
114
+
115
+ // Database
116
+ if (allDeps['@aws-sdk/client-dynamodb'] || allDeps['aws-sdk']) stack.database = 'DynamoDB';
117
+ else if (allDeps.pg || allDeps['@neondatabase/serverless']) stack.database = 'PostgreSQL';
118
+ else if (allDeps.mysql2) stack.database = 'MySQL';
119
+ else if (allDeps.mongoose || allDeps.mongodb) stack.database = 'MongoDB';
120
+ else if (allDeps['better-sqlite3']) stack.database = 'SQLite';
121
+
122
+ // ORM
123
+ if (allDeps['drizzle-orm']) stack.orm = `Drizzle ${allDeps['drizzle-orm']}`;
124
+ else if (allDeps['@prisma/client'] || allDeps.prisma) stack.orm = 'Prisma';
125
+ else if (allDeps.typeorm) stack.orm = 'TypeORM';
126
+ else if (allDeps.sequelize) stack.orm = 'Sequelize';
127
+ else if (allDeps.knex) stack.orm = 'Knex.js';
128
+
129
+ // Testing
130
+ if (allDeps.vitest) stack.testing = 'Vitest';
131
+ else if (allDeps.jest) stack.testing = 'Jest';
132
+ else if (allDeps.mocha) stack.testing = 'Mocha';
133
+ else if (allDeps.playwright || allDeps['@playwright/test']) stack.testing = 'Playwright';
134
+
135
+ // CSS
136
+ if (allDeps.tailwindcss) stack.css = `Tailwind ${allDeps.tailwindcss}`;
137
+ else if (allDeps['styled-components']) stack.css = 'Styled Components';
138
+
139
+ // Auth
140
+ if (allDeps['next-auth']) stack.auth = 'NextAuth.js';
141
+ else if (allDeps.passport) stack.auth = 'Passport.js';
142
+ else if (allDeps['@auth0/auth0-react']) stack.auth = 'Auth0';
143
+ else if (allDeps.bcryptjs || allDeps.bcrypt) stack.auth = 'Custom (bcrypt)';
144
+ }
145
+
146
+ // Check for Python
147
+ if (existsSync(resolve(dir, 'requirements.txt')) || existsSync(resolve(dir, 'pyproject.toml'))) {
148
+ stack.language = 'Python';
149
+ if (existsSync(resolve(dir, 'manage.py'))) stack.framework = 'Django';
150
+ else if (existsSync(resolve(dir, 'app.py')) || existsSync(resolve(dir, 'main.py'))) stack.framework = 'FastAPI/Flask';
151
+ }
152
+
153
+ // Check for Go
154
+ if (existsSync(resolve(dir, 'go.mod'))) {
155
+ stack.language = 'Go';
156
+ }
157
+
158
+ // Hosting detection
159
+ if (existsSync(resolve(dir, 'amplify.yml'))) stack.hosting = 'AWS Amplify';
160
+ else if (existsSync(resolve(dir, 'vercel.json'))) stack.hosting = 'Vercel';
161
+ else if (existsSync(resolve(dir, 'Dockerfile'))) stack.hosting = 'Docker';
162
+ else if (existsSync(resolve(dir, 'fly.toml'))) stack.hosting = 'Fly.io';
163
+ else if (existsSync(resolve(dir, 'railway.json'))) stack.hosting = 'Railway';
164
+ else if (existsSync(resolve(dir, 'render.yaml'))) stack.hosting = 'Render';
165
+
166
+ return stack;
167
+ }
168
+
169
+ // ── Project Scanner ────────────────────────────────────────────────────────
170
+
171
+ function scanProject(dir) {
172
+ const scan = {
173
+ routes: [],
174
+ models: [],
175
+ services: [],
176
+ tests: [],
177
+ envVars: [],
178
+ components: [],
179
+ middlewares: [],
180
+ totalFiles: 0,
181
+ totalLines: 0,
182
+ };
183
+
184
+ // Find routes
185
+ ['src/app/api', 'src/routes', 'routes', 'api', 'src/api'].forEach(routeDir => {
186
+ const fullDir = resolve(dir, routeDir);
187
+ if (existsSync(fullDir)) {
188
+ const files = getFilesRecursive(fullDir);
189
+ for (const f of files) {
190
+ scan.routes.push(relative(dir, f));
191
+ }
192
+ }
193
+ });
194
+
195
+ // Find models/entities
196
+ ['src/models', 'models', 'src/entities', 'entities', 'src/schema', 'schema', 'prisma'].forEach(modelDir => {
197
+ const fullDir = resolve(dir, modelDir);
198
+ if (existsSync(fullDir)) {
199
+ const files = getFilesRecursive(fullDir);
200
+ for (const f of files) {
201
+ scan.models.push(relative(dir, f));
202
+ }
203
+ }
204
+ });
205
+
206
+ // Find services
207
+ ['src/services', 'services', 'src/lib', 'lib'].forEach(svcDir => {
208
+ const fullDir = resolve(dir, svcDir);
209
+ if (existsSync(fullDir)) {
210
+ const files = getFilesRecursive(fullDir);
211
+ for (const f of files) {
212
+ scan.services.push(relative(dir, f));
213
+ }
214
+ }
215
+ });
216
+
217
+ // Find tests
218
+ ['tests', 'test', '__tests__', 'spec', 'e2e'].forEach(testDir => {
219
+ const fullDir = resolve(dir, testDir);
220
+ if (existsSync(fullDir)) {
221
+ const files = getFilesRecursive(fullDir);
222
+ for (const f of files) {
223
+ scan.tests.push(relative(dir, f));
224
+ }
225
+ }
226
+ });
227
+
228
+ // Find components
229
+ ['src/components', 'components', 'src/ui'].forEach(compDir => {
230
+ const fullDir = resolve(dir, compDir);
231
+ if (existsSync(fullDir)) {
232
+ const files = getFilesRecursive(fullDir);
233
+ for (const f of files) {
234
+ scan.components.push(relative(dir, f));
235
+ }
236
+ }
237
+ });
238
+
239
+ // Find middleware
240
+ ['src/middleware', 'middleware', 'src/middlewares'].forEach(mwDir => {
241
+ const fullDir = resolve(dir, mwDir);
242
+ if (existsSync(fullDir)) {
243
+ const files = getFilesRecursive(fullDir);
244
+ for (const f of files) {
245
+ scan.middlewares.push(relative(dir, f));
246
+ }
247
+ }
248
+ });
249
+
250
+ // Parse .env.example for env vars
251
+ const envExample = resolve(dir, '.env.example');
252
+ if (existsSync(envExample)) {
253
+ const content = readFileSync(envExample, 'utf-8');
254
+ const lines = content.split('\n');
255
+ for (const line of lines) {
256
+ const match = line.match(/^([A-Z][A-Z0-9_]+)\s*=\s*(.*)/);
257
+ if (match) {
258
+ scan.envVars.push({ name: match[1], example: match[2] || '<required>' });
259
+ }
260
+ }
261
+ }
262
+
263
+ // Count files and lines
264
+ countFilesAndLines(dir, scan);
265
+
266
+ return scan;
267
+ }
268
+
269
+ function countFilesAndLines(dir, scan) {
270
+ walkDir(dir, (filePath) => {
271
+ scan.totalFiles++;
272
+ try {
273
+ const content = readFileSync(filePath, 'utf-8');
274
+ scan.totalLines += content.split('\n').length;
275
+ } catch { /* skip binary files */ }
276
+ });
277
+ }
278
+
279
+ // ── Document Generators ────────────────────────────────────────────────────
280
+
281
+ function generateArchitecture(dir, config, stack, scan, flags) {
282
+ const path = resolve(dir, 'docs-canonical/ARCHITECTURE.md');
283
+ if (existsSync(path) && !flags.force) {
284
+ console.log(` ${c.dim}⏭️ ARCHITECTURE.md (exists)${c.reset}`);
285
+ return false;
286
+ }
287
+
288
+ const techRows = Object.entries(stack)
289
+ .filter(([, v]) => v)
290
+ .map(([k, v]) => `| ${k.charAt(0).toUpperCase() + k.slice(1)} | ${v} | | |`)
291
+ .join('\n');
292
+
293
+ const componentRows = [];
294
+ if (scan.routes.length > 0) componentRows.push(`| API Routes | HTTP request handling | ${scan.routes.length > 3 ? scan.routes.slice(0, 3).join(', ') + '...' : scan.routes.join(', ')} | |`);
295
+ if (scan.services.length > 0) componentRows.push(`| Services | Business logic | ${scan.services.length > 3 ? scan.services.slice(0, 3).join(', ') + '...' : scan.services.join(', ')} | |`);
296
+ if (scan.models.length > 0) componentRows.push(`| Models | Data entities | ${scan.models.length > 3 ? scan.models.slice(0, 3).join(', ') + '...' : scan.models.join(', ')} | |`);
297
+ if (scan.components.length > 0) componentRows.push(`| UI Components | Frontend components | ${scan.components.length} files | |`);
298
+ if (scan.middlewares.length > 0) componentRows.push(`| Middleware | Request processing | ${scan.middlewares.join(', ')} | |`);
299
+
300
+ const content = `# Architecture
301
+
302
+ <!-- docguard:version 0.1.0 -->
303
+ <!-- docguard:status draft -->
304
+ <!-- docguard:last-reviewed ${new Date().toISOString().split('T')[0]} -->
305
+ <!-- docguard:generated true -->
306
+
307
+ > **Auto-generated by DocGuard.** Review and refine this document.
308
+
309
+ | Metadata | Value |
310
+ |----------|-------|
311
+ | **Status** | ![Status](https://img.shields.io/badge/status-draft-yellow) |
312
+ | **Version** | \`0.1.0\` |
313
+ | **Last Updated** | ${new Date().toISOString().split('T')[0]} |
314
+ | **Project Size** | ${scan.totalFiles} files, ~${Math.round(scan.totalLines / 1000)}K lines |
315
+
316
+ ---
317
+
318
+ ## System Overview
319
+
320
+ <!-- TODO: Describe what this system does and who it's for -->
321
+ ${config.projectName} is a ${stack.framework || stack.language || 'software'} application.
322
+
323
+ ## Component Map
324
+
325
+ | Component | Responsibility | Location | Tests |
326
+ |-----------|---------------|----------|-------|
327
+ ${componentRows.join('\n') || '| <!-- Add components --> | | | |'}
328
+
329
+ ## Tech Stack
330
+
331
+ | Category | Technology | Version | License |
332
+ |----------|-----------|---------|---------|
333
+ ${techRows || '| <!-- Add technologies --> | | | |'}
334
+
335
+ ## Layer Boundaries
336
+
337
+ <!-- TODO: Define which layers can import from which -->
338
+
339
+ | Layer | Can Import From | Cannot Import From |
340
+ |-------|----------------|-------------------|
341
+ ${scan.routes.length > 0 ? '| Routes/Handlers | Services, Middleware | Models (direct) |' : ''}
342
+ ${scan.services.length > 0 ? '| Services | Repositories, Utils | Routes |' : ''}
343
+ ${scan.models.length > 0 ? '| Models/Repositories | Utils | Services, Routes |' : ''}
344
+
345
+ ## Diagrams
346
+
347
+ \`\`\`mermaid
348
+ graph TD
349
+ A[Client] --> B[${stack.framework || 'API'}]
350
+ B --> C[Services]
351
+ C --> D[${stack.database || 'Database'}]
352
+ \`\`\`
353
+
354
+ ---
355
+
356
+ ## Revision History
357
+
358
+ | Version | Date | Author | Changes |
359
+ |---------|------|--------|---------|
360
+ | 0.1.0 | ${new Date().toISOString().split('T')[0]} | DocGuard Generate | Auto-generated from codebase scan |
361
+ `;
362
+
363
+ writeFileSync(path, content, 'utf-8');
364
+ console.log(` ${c.green}✅ ARCHITECTURE.md${c.reset} (${componentRows.length} components, ${Object.values(stack).filter(Boolean).length} tech)`);
365
+ return true;
366
+ }
367
+
368
+ function generateDataModel(dir, config, stack, scan, flags) {
369
+ const path = resolve(dir, 'docs-canonical/DATA-MODEL.md');
370
+ if (existsSync(path) && !flags.force) {
371
+ console.log(` ${c.dim}⏭️ DATA-MODEL.md (exists)${c.reset}`);
372
+ return false;
373
+ }
374
+
375
+ // Parse model files for entity names
376
+ const entities = [];
377
+ for (const modelFile of scan.models) {
378
+ const name = basename(modelFile, extname(modelFile));
379
+ if (name !== 'index' && name !== 'schema') {
380
+ entities.push({
381
+ name: name.charAt(0).toUpperCase() + name.slice(1),
382
+ file: modelFile,
383
+ });
384
+ }
385
+ }
386
+
387
+ // Check for Prisma schema
388
+ const prismaPath = resolve(dir, 'prisma/schema.prisma');
389
+ if (existsSync(prismaPath)) {
390
+ const prismaContent = readFileSync(prismaPath, 'utf-8');
391
+ const modelRegex = /model\s+(\w+)\s*\{/g;
392
+ let match;
393
+ while ((match = modelRegex.exec(prismaContent)) !== null) {
394
+ if (!entities.find(e => e.name.toLowerCase() === match[1].toLowerCase())) {
395
+ entities.push({ name: match[1], file: 'prisma/schema.prisma' });
396
+ }
397
+ }
398
+ }
399
+
400
+ const entityRows = entities.map(e =>
401
+ `| ${e.name} | ${stack.database || 'TBD'} | ${e.name.toLowerCase()}Id | See \`${e.file}\` |`
402
+ ).join('\n');
403
+
404
+ const content = `# Data Model
405
+
406
+ <!-- docguard:version 0.1.0 -->
407
+ <!-- docguard:status draft -->
408
+ <!-- docguard:last-reviewed ${new Date().toISOString().split('T')[0]} -->
409
+ <!-- docguard:generated true -->
410
+
411
+ > **Auto-generated by DocGuard.** Review and refine this document.
412
+
413
+ | Metadata | Value |
414
+ |----------|-------|
415
+ | **Status** | ![Status](https://img.shields.io/badge/status-draft-yellow) |
416
+ | **Version** | \`0.1.0\` |
417
+ | **Database** | ${stack.database || 'TBD'} |
418
+ | **ORM** | ${stack.orm || 'None detected'} |
419
+
420
+ ---
421
+
422
+ ## Entities
423
+
424
+ | Entity | Storage | Primary Key | Description |
425
+ |--------|---------|-------------|-------------|
426
+ ${entityRows || '| <!-- No models detected --> | | | |'}
427
+
428
+ ${entities.map(e => `### ${e.name}
429
+
430
+ > Source: \`${e.file}\`
431
+
432
+ | Field | Type | Required | Default | Constraints | Description |
433
+ |-------|------|----------|---------|-------------|-------------|
434
+ | <!-- TODO: Fill in fields --> | | | | | |
435
+ `).join('\n')}
436
+
437
+ ## Relationships
438
+
439
+ | From | To | Type | FK/Reference | Cascade |
440
+ |------|-----|------|-------------|---------|
441
+ | <!-- TODO --> | | | | |
442
+
443
+ ## Indexes
444
+
445
+ | Table | Index Name | Fields | Type | Purpose |
446
+ |-------|-----------|--------|------|---------|
447
+ | <!-- TODO --> | | | | |
448
+
449
+ ---
450
+
451
+ ## Revision History
452
+
453
+ | Version | Date | Author | Changes |
454
+ |---------|------|--------|---------|
455
+ | 0.1.0 | ${new Date().toISOString().split('T')[0]} | DocGuard Generate | Auto-generated (${entities.length} entities found) |
456
+ `;
457
+
458
+ writeFileSync(path, content, 'utf-8');
459
+ console.log(` ${c.green}✅ DATA-MODEL.md${c.reset} (${entities.length} entities detected)`);
460
+ return true;
461
+ }
462
+
463
+ function generateEnvironment(dir, config, stack, scan, flags) {
464
+ const path = resolve(dir, 'docs-canonical/ENVIRONMENT.md');
465
+ if (existsSync(path) && !flags.force) {
466
+ console.log(` ${c.dim}⏭️ ENVIRONMENT.md (exists)${c.reset}`);
467
+ return false;
468
+ }
469
+
470
+ const envVarRows = scan.envVars.map(v =>
471
+ `| \`${v.name}\` | ${categorizeEnvVar(v.name)} | Yes | \`${v.example}\` | |`
472
+ ).join('\n');
473
+
474
+ const content = `# Environment
475
+
476
+ <!-- docguard:version 0.1.0 -->
477
+ <!-- docguard:status draft -->
478
+ <!-- docguard:last-reviewed ${new Date().toISOString().split('T')[0]} -->
479
+ <!-- docguard:generated true -->
480
+
481
+ > **Auto-generated by DocGuard.** Review and refine this document.
482
+
483
+ | Metadata | Value |
484
+ |----------|-------|
485
+ | **Status** | ![Status](https://img.shields.io/badge/status-draft-yellow) |
486
+ | **Version** | \`0.1.0\` |
487
+
488
+ ---
489
+
490
+ ## Prerequisites
491
+
492
+ | Tool | Version | Installation |
493
+ |------|---------|-------------|
494
+ ${stack.language ? `| ${stack.language.split(' ')[0]} | ${stack.language.split(' ')[1] || 'latest'} | |` : ''}
495
+ ${stack.framework ? `| ${stack.framework.split(' ')[0]} | ${stack.framework.split(' ')[1] || 'latest'} | |` : ''}
496
+ ${stack.database ? `| ${stack.database} | latest | |` : ''}
497
+
498
+ ## Environment Variables
499
+
500
+ | Variable | Category | Required | Example | Description |
501
+ |----------|----------|:--------:|---------|-------------|
502
+ ${envVarRows || '| <!-- No .env.example found --> | | | | |'}
503
+
504
+ ## Setup Steps
505
+
506
+ 1. Clone the repository
507
+ 2. Install dependencies: \`${existsSync(resolve(dir, 'pnpm-lock.yaml')) ? 'pnpm install' : 'npm install'}\`
508
+ 3. Copy environment file: \`cp .env.example .env.local\`
509
+ 4. Fill in environment variables
510
+ 5. Start development server: \`${existsSync(resolve(dir, 'pnpm-lock.yaml')) ? 'pnpm' : 'npm'} run dev\`
511
+
512
+ ---
513
+
514
+ ## Revision History
515
+
516
+ | Version | Date | Author | Changes |
517
+ |---------|------|--------|---------|
518
+ | 0.1.0 | ${new Date().toISOString().split('T')[0]} | DocGuard Generate | Auto-generated (${scan.envVars.length} env vars found) |
519
+ `;
520
+
521
+ writeFileSync(path, content, 'utf-8');
522
+ console.log(` ${c.green}✅ ENVIRONMENT.md${c.reset} (${scan.envVars.length} env vars detected)`);
523
+ return true;
524
+ }
525
+
526
+ function generateTestSpec(dir, config, stack, scan, flags) {
527
+ const path = resolve(dir, 'docs-canonical/TEST-SPEC.md');
528
+ if (existsSync(path) && !flags.force) {
529
+ console.log(` ${c.dim}⏭️ TEST-SPEC.md (exists)${c.reset}`);
530
+ return false;
531
+ }
532
+
533
+ // Build service-to-test map
534
+ const serviceMap = [];
535
+ for (const svc of scan.services) {
536
+ const svcName = basename(svc, extname(svc));
537
+ const matchingTest = scan.tests.find(t =>
538
+ t.includes(svcName) || t.includes(svcName.replace('.', '.test.'))
539
+ );
540
+ serviceMap.push({
541
+ source: svc,
542
+ test: matchingTest || '—',
543
+ status: matchingTest ? '✅' : '❌',
544
+ });
545
+ }
546
+
547
+ const serviceRows = serviceMap.map(s =>
548
+ `| \`${s.source}\` | \`${s.test}\` | — | ${s.status} |`
549
+ ).join('\n');
550
+
551
+ const content = `# Test Specification
552
+
553
+ <!-- docguard:version 0.1.0 -->
554
+ <!-- docguard:status draft -->
555
+ <!-- docguard:last-reviewed ${new Date().toISOString().split('T')[0]} -->
556
+ <!-- docguard:generated true -->
557
+
558
+ > **Auto-generated by DocGuard.** Review and refine this document.
559
+
560
+ | Metadata | Value |
561
+ |----------|-------|
562
+ | **Status** | ![Status](https://img.shields.io/badge/status-draft-yellow) |
563
+ | **Test Framework** | ${stack.testing || 'Not detected'} |
564
+ | **Test Files Found** | ${scan.tests.length} |
565
+
566
+ ---
567
+
568
+ ## Test Categories
569
+
570
+ | Category | Framework | Location | Run Command |
571
+ |----------|-----------|----------|-------------|
572
+ | Unit | ${stack.testing || 'TBD'} | tests/unit/ | \`npm test\` |
573
+ | Integration | ${stack.testing || 'TBD'} | tests/integration/ | \`npm run test:integration\` |
574
+ | E2E | Playwright | tests/e2e/ | \`npm run test:e2e\` |
575
+
576
+ ## Coverage Rules
577
+
578
+ | Metric | Target | Current |
579
+ |--------|:------:|:-------:|
580
+ | Line Coverage | 80% | <!-- TODO --> |
581
+ | Branch Coverage | 70% | <!-- TODO --> |
582
+ | Function Coverage | 80% | <!-- TODO --> |
583
+
584
+ ## Service-to-Test Map
585
+
586
+ | Source File | Unit Test | Integration Test | Status |
587
+ |------------|-----------|-----------------|:------:|
588
+ ${serviceRows || '| <!-- No services found --> | | | |'}
589
+
590
+ ## Critical User Journeys
591
+
592
+ | # | Journey | Test File | Status |
593
+ |---|---------|-----------|:------:|
594
+ | 1 | <!-- e.g. User Registration --> | <!-- test file --> | ❌ |
595
+ | 2 | <!-- e.g. Login Flow --> | | ❌ |
596
+
597
+ ---
598
+
599
+ ## Revision History
600
+
601
+ | Version | Date | Author | Changes |
602
+ |---------|------|--------|---------|
603
+ | 0.1.0 | ${new Date().toISOString().split('T')[0]} | DocGuard Generate | Auto-generated (${scan.tests.length} test files, ${serviceMap.filter(s => s.status === '✅').length}/${serviceMap.length} mapped) |
604
+ `;
605
+
606
+ writeFileSync(path, content, 'utf-8');
607
+ console.log(` ${c.green}✅ TEST-SPEC.md${c.reset} (${scan.tests.length} tests, ${serviceMap.filter(s => s.status === '✅').length}/${serviceMap.length} services mapped)`);
608
+ return true;
609
+ }
610
+
611
+ function generateSecurity(dir, config, stack, scan, flags) {
612
+ const path = resolve(dir, 'docs-canonical/SECURITY.md');
613
+ if (existsSync(path) && !flags.force) {
614
+ console.log(` ${c.dim}⏭️ SECURITY.md (exists)${c.reset}`);
615
+ return false;
616
+ }
617
+
618
+ const content = `# Security
619
+
620
+ <!-- docguard:version 0.1.0 -->
621
+ <!-- docguard:status draft -->
622
+ <!-- docguard:last-reviewed ${new Date().toISOString().split('T')[0]} -->
623
+ <!-- docguard:generated true -->
624
+
625
+ > **Auto-generated by DocGuard.** Review and refine this document.
626
+
627
+ | Metadata | Value |
628
+ |----------|-------|
629
+ | **Status** | ![Status](https://img.shields.io/badge/status-draft-yellow) |
630
+
631
+ ---
632
+
633
+ ## Authentication
634
+
635
+ | Method | Provider | Token Type | Expiry |
636
+ |--------|---------|-----------|--------|
637
+ | ${stack.auth || '<!-- TODO -->'} | | | |
638
+
639
+ ## Authorization
640
+
641
+ | Role | Permissions | Notes |
642
+ |------|-----------|-------|
643
+ | <!-- e.g. admin --> | <!-- All --> | |
644
+ | <!-- e.g. user --> | <!-- Read/Write --> | |
645
+
646
+ ## Secrets Management
647
+
648
+ | Secret | Storage | Rotation | Access |
649
+ |--------|---------|----------|--------|
650
+ ${scan.envVars.filter(v => isSecretVar(v.name)).map(v =>
651
+ `| \`${v.name}\` | Environment Variable | <!-- TODO --> | Application |`
652
+ ).join('\n') || '| <!-- TODO --> | | | |'}
653
+
654
+ ## Security Rules
655
+
656
+ - [ ] All secrets stored in environment variables (never in code)
657
+ - [ ] \`.env\` is in \`.gitignore\`
658
+ - [ ] API endpoints require authentication
659
+ - [ ] Input validation on all user inputs
660
+ - [ ] HTTPS enforced in production
661
+ - [ ] CORS configured appropriately
662
+
663
+ ---
664
+
665
+ ## Revision History
666
+
667
+ | Version | Date | Author | Changes |
668
+ |---------|------|--------|---------|
669
+ | 0.1.0 | ${new Date().toISOString().split('T')[0]} | DocGuard Generate | Auto-generated |
670
+ `;
671
+
672
+ writeFileSync(path, content, 'utf-8');
673
+ console.log(` ${c.green}✅ SECURITY.md${c.reset} (auth: ${stack.auth || 'not detected'})`);
674
+ return true;
675
+ }
676
+
677
+ function generateRootFiles(dir, config, stack, scan, flags) {
678
+ let created = 0;
679
+ let skipped = 0;
680
+
681
+ // AGENTS.md
682
+ const agentsPath = resolve(dir, 'AGENTS.md');
683
+ if (!existsSync(agentsPath) || flags.force) {
684
+ const content = `# AI Agent Instructions — ${config.projectName}
685
+
686
+ > This project follows **Canonical-Driven Development (CDD)**.
687
+ > Documentation is the source of truth. Read before coding.
688
+
689
+ ## Workflow
690
+
691
+ 1. **Read** \`docs-canonical/\` before suggesting changes
692
+ 2. **Check** existing patterns in the codebase
693
+ 3. **Confirm** your approach before writing code
694
+ 4. **Implement** matching existing code style
695
+ 5. **Log** any deviations in \`DRIFT-LOG.md\` with \`// DRIFT: reason\`
696
+ 6. **Run DocGuard** after changes — \`npx docguard guard\`
697
+
698
+ ## Project Stack
699
+
700
+ ${Object.entries(stack).filter(([, v]) => v).map(([k, v]) => `- **${k}**: ${v}`).join('\n')}
701
+
702
+ ## Key Files
703
+
704
+ | File | Purpose |
705
+ |------|---------|
706
+ | \`docs-canonical/ARCHITECTURE.md\` | System design |
707
+ | \`docs-canonical/DATA-MODEL.md\` | Database schemas |
708
+ | \`docs-canonical/SECURITY.md\` | Auth & secrets |
709
+ | \`docs-canonical/TEST-SPEC.md\` | Test requirements |
710
+ | \`docs-canonical/ENVIRONMENT.md\` | Environment setup |
711
+ | \`CHANGELOG.md\` | Change tracking |
712
+ | \`DRIFT-LOG.md\` | Documented deviations |
713
+
714
+ ## DocGuard — Documentation Enforcement
715
+
716
+ \`\`\`bash
717
+ npx docguard guard # Validate compliance
718
+ npx docguard fix # Find issues with fix instructions
719
+ npx docguard fix --format prompt # AI-ready fix prompt
720
+ npx docguard fix --auto # Auto-fix missing files
721
+ npx docguard score # CDD maturity score
722
+ \`\`\`
723
+
724
+ ### AI Agent Workflow (IMPORTANT)
725
+
726
+ 1. **Before work**: Run \`npx docguard guard\` — understand compliance state
727
+ 2. **After changes**: Run \`npx docguard fix --format prompt\` — get fix instructions
728
+ 3. **Fix issues**: Each issue has an \`ai_instruction\` — follow it exactly
729
+ 4. **Verify**: Run \`npx docguard guard\` again — must pass before commit
730
+ 5. **Update CHANGELOG**: All changes need a changelog entry
731
+
732
+ ## Rules
733
+
734
+ - Never commit without updating CHANGELOG.md
735
+ - If code deviates from docs, add \`// DRIFT: reason\`
736
+ - Security rules in SECURITY.md are mandatory
737
+ - Test requirements in TEST-SPEC.md must be met
738
+ - Documentation changes must pass \`docguard guard\`
739
+ `;
740
+ writeFileSync(agentsPath, content, 'utf-8');
741
+ console.log(` ${c.green}✅ AGENTS.md${c.reset}`);
742
+ created++;
743
+ } else {
744
+ console.log(` ${c.dim}⏭️ AGENTS.md (exists)${c.reset}`);
745
+ skipped++;
746
+ }
747
+
748
+ // CHANGELOG.md
749
+ const changelogPath = resolve(dir, 'CHANGELOG.md');
750
+ if (!existsSync(changelogPath) || flags.force) {
751
+ const content = `# Changelog
752
+
753
+ All notable changes to this project will be documented in this file.
754
+
755
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
756
+
757
+ ## [Unreleased]
758
+
759
+ ### Added
760
+ - CDD documentation via DocGuard generate
761
+ `;
762
+ writeFileSync(changelogPath, content, 'utf-8');
763
+ console.log(` ${c.green}✅ CHANGELOG.md${c.reset}`);
764
+ created++;
765
+ } else {
766
+ console.log(` ${c.dim}⏭️ CHANGELOG.md (exists)${c.reset}`);
767
+ skipped++;
768
+ }
769
+
770
+ // DRIFT-LOG.md
771
+ const driftPath = resolve(dir, 'DRIFT-LOG.md');
772
+ if (!existsSync(driftPath) || flags.force) {
773
+ const content = `# Drift Log
774
+
775
+ > Documents conscious deviations from canonical specifications.
776
+ > Every \`// DRIFT: reason\` in code must have a corresponding entry here.
777
+
778
+ | Date | File | Canonical Doc | Drift Description | Severity | Resolution |
779
+ |------|------|---------------|-------------------|----------|------------|
780
+ | | | | | | |
781
+ `;
782
+ writeFileSync(driftPath, content, 'utf-8');
783
+ console.log(` ${c.green}✅ DRIFT-LOG.md${c.reset}`);
784
+ created++;
785
+ } else {
786
+ console.log(` ${c.dim}⏭️ DRIFT-LOG.md (exists)${c.reset}`);
787
+ skipped++;
788
+ }
789
+
790
+ return { created, skipped };
791
+ }
792
+
793
+ // ── Utility Functions ──────────────────────────────────────────────────────
794
+
795
+ function categorizeEnvVar(name) {
796
+ if (name.includes('SECRET') || name.includes('KEY') || name.includes('TOKEN') || name.includes('PASSWORD')) return '🔐 Secret';
797
+ if (name.includes('DATABASE') || name.includes('DB_') || name.includes('REDIS')) return '🗃️ Database';
798
+ if (name.includes('AUTH') || name.includes('JWT') || name.includes('SESSION')) return '🔒 Auth';
799
+ if (name.includes('AWS') || name.includes('CLOUD') || name.includes('S3')) return '☁️ Cloud';
800
+ if (name.includes('URL') || name.includes('HOST') || name.includes('PORT')) return '🌐 Network';
801
+ return '⚙️ Config';
802
+ }
803
+
804
+ function isSecretVar(name) {
805
+ return name.includes('SECRET') || name.includes('KEY') || name.includes('TOKEN') || name.includes('PASSWORD');
806
+ }
807
+
808
+ function walkDir(dir, callback) {
809
+ if (!existsSync(dir)) return;
810
+ const entries = readdirSync(dir);
811
+ for (const entry of entries) {
812
+ if (IGNORE_DIRS.has(entry) || entry.startsWith('.')) continue;
813
+ const fullPath = join(dir, entry);
814
+ try {
815
+ const stat = statSync(fullPath);
816
+ if (stat.isDirectory()) {
817
+ walkDir(fullPath, callback);
818
+ } else if (stat.isFile() && CODE_EXTENSIONS.has(extname(fullPath))) {
819
+ callback(fullPath);
820
+ }
821
+ } catch { /* skip */ }
822
+ }
823
+ }
824
+
825
+ function getFilesRecursive(dir) {
826
+ const results = [];
827
+ if (!existsSync(dir)) return results;
828
+ const entries = readdirSync(dir);
829
+ for (const entry of entries) {
830
+ if (IGNORE_DIRS.has(entry) || entry.startsWith('.')) continue;
831
+ const fullPath = join(dir, entry);
832
+ try {
833
+ const stat = statSync(fullPath);
834
+ if (stat.isDirectory()) {
835
+ results.push(...getFilesRecursive(fullPath));
836
+ } else if (stat.isFile()) {
837
+ results.push(fullPath);
838
+ }
839
+ } catch { /* skip */ }
840
+ }
841
+ return results;
842
+ }