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.
- package/LICENSE +21 -0
- package/PHILOSOPHY.md +150 -0
- package/README.md +309 -0
- package/STANDARD.md +751 -0
- package/cli/commands/agents.mjs +221 -0
- package/cli/commands/audit.mjs +92 -0
- package/cli/commands/badge.mjs +72 -0
- package/cli/commands/ci.mjs +80 -0
- package/cli/commands/diagnose.mjs +273 -0
- package/cli/commands/diff.mjs +360 -0
- package/cli/commands/fix.mjs +610 -0
- package/cli/commands/generate.mjs +842 -0
- package/cli/commands/guard.mjs +158 -0
- package/cli/commands/hooks.mjs +227 -0
- package/cli/commands/init.mjs +249 -0
- package/cli/commands/score.mjs +396 -0
- package/cli/commands/watch.mjs +143 -0
- package/cli/docguard.mjs +458 -0
- package/cli/validators/architecture.mjs +380 -0
- package/cli/validators/changelog.mjs +39 -0
- package/cli/validators/docs-sync.mjs +110 -0
- package/cli/validators/drift.mjs +101 -0
- package/cli/validators/environment.mjs +70 -0
- package/cli/validators/freshness.mjs +224 -0
- package/cli/validators/security.mjs +101 -0
- package/cli/validators/structure.mjs +88 -0
- package/cli/validators/test-spec.mjs +115 -0
- package/docs/ai-integration.md +179 -0
- package/docs/commands.md +239 -0
- package/docs/configuration.md +96 -0
- package/docs/faq.md +155 -0
- package/docs/installation.md +81 -0
- package/docs/profiles.md +103 -0
- package/docs/quickstart.md +79 -0
- package/package.json +57 -0
- package/templates/ADR.md.template +64 -0
- package/templates/AGENTS.md.template +88 -0
- package/templates/ARCHITECTURE.md.template +78 -0
- package/templates/CHANGELOG.md.template +16 -0
- package/templates/CURRENT-STATE.md.template +64 -0
- package/templates/DATA-MODEL.md.template +66 -0
- package/templates/DEPLOYMENT.md.template +66 -0
- package/templates/DRIFT-LOG.md.template +18 -0
- package/templates/ENVIRONMENT.md.template +43 -0
- package/templates/KNOWN-GOTCHAS.md.template +69 -0
- package/templates/ROADMAP.md.template +82 -0
- package/templates/RUNBOOKS.md.template +115 -0
- package/templates/SECURITY.md.template +42 -0
- package/templates/TEST-SPEC.md.template +55 -0
- package/templates/TROUBLESHOOTING.md.template +96 -0
- package/templates/VENDOR-BUGS.md.template +74 -0
- package/templates/ci/github-actions.yml +39 -0
- package/templates/commands/docguard.fix.md +65 -0
- package/templates/commands/docguard.guard.md +40 -0
- package/templates/commands/docguard.init.md +62 -0
- package/templates/commands/docguard.review.md +44 -0
- 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** |  |
|
|
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** |  |
|
|
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** |  |
|
|
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** |  |
|
|
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** |  |
|
|
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
|
+
}
|