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,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnose Command — The AI Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Chains guard → fix in a single command.
|
|
5
|
+
* Runs guard internally, maps every failure to an AI-actionable
|
|
6
|
+
* fix prompt, and outputs them as one combined remediation plan.
|
|
7
|
+
*
|
|
8
|
+
* This is the command AI agents run to self-heal a project's docs.
|
|
9
|
+
*
|
|
10
|
+
* Output modes:
|
|
11
|
+
* --format text Summary with fix instructions (default)
|
|
12
|
+
* --format json Structured {issues, fixPrompts} for automation
|
|
13
|
+
* --format prompt Full AI-ready prompt (all issues combined)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { c } from '../docguard.mjs';
|
|
17
|
+
import { runGuardInternal } from './guard.mjs';
|
|
18
|
+
import { runScoreInternal } from './score.mjs';
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { resolve } from 'node:path';
|
|
21
|
+
|
|
22
|
+
// Map validator failures to the right fix --doc target
|
|
23
|
+
const VALIDATOR_TO_DOC = {
|
|
24
|
+
'Structure': null, // structural — needs init, not doc fix
|
|
25
|
+
'Doc Sections': null, // section-level — maps to specific doc
|
|
26
|
+
'Docs-Sync': null, // cross-reference — needs manual review
|
|
27
|
+
'Drift': null, // drift log maintenance
|
|
28
|
+
'Changelog': null, // changelog maintenance
|
|
29
|
+
'Test-Spec': 'test-spec',
|
|
30
|
+
'Environment': 'environment',
|
|
31
|
+
'Security': 'security',
|
|
32
|
+
'Architecture': 'architecture',
|
|
33
|
+
'Freshness': null, // freshness — maps to stale doc
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Actionable fix instructions per validator
|
|
37
|
+
const FIX_INSTRUCTIONS = {
|
|
38
|
+
'Structure': {
|
|
39
|
+
action: 'Create missing files',
|
|
40
|
+
command: 'docguard init',
|
|
41
|
+
description: 'Run init to create missing documentation templates.',
|
|
42
|
+
},
|
|
43
|
+
'Doc Sections': {
|
|
44
|
+
action: 'Fill document sections',
|
|
45
|
+
command: 'docguard fix --doc',
|
|
46
|
+
description: 'Documents exist but have missing or placeholder sections. Use fix --doc to generate AI content prompts.',
|
|
47
|
+
},
|
|
48
|
+
'Docs-Sync': {
|
|
49
|
+
action: 'Sync documentation references',
|
|
50
|
+
command: 'docguard fix --doc architecture',
|
|
51
|
+
description: 'Documentation references are out of sync with code. Review and update component maps.',
|
|
52
|
+
},
|
|
53
|
+
'Drift': {
|
|
54
|
+
action: 'Update DRIFT-LOG.md',
|
|
55
|
+
description: 'Code deviates from canonical docs without logged reasons. Add DRIFT entries or update the canonical docs.',
|
|
56
|
+
},
|
|
57
|
+
'Changelog': {
|
|
58
|
+
action: 'Update CHANGELOG.md',
|
|
59
|
+
description: 'CHANGELOG.md is missing or has no [Unreleased] section. Add recent changes.',
|
|
60
|
+
},
|
|
61
|
+
'Test-Spec': {
|
|
62
|
+
action: 'Update TEST-SPEC.md',
|
|
63
|
+
command: 'docguard fix --doc test-spec',
|
|
64
|
+
description: 'Test documentation needs updating to match actual test structure.',
|
|
65
|
+
},
|
|
66
|
+
'Environment': {
|
|
67
|
+
action: 'Update ENVIRONMENT.md',
|
|
68
|
+
command: 'docguard fix --doc environment',
|
|
69
|
+
description: 'Environment documentation is missing or incomplete.',
|
|
70
|
+
},
|
|
71
|
+
'Security': {
|
|
72
|
+
action: 'Update SECURITY.md',
|
|
73
|
+
command: 'docguard fix --doc security',
|
|
74
|
+
description: 'Security documentation needs updating.',
|
|
75
|
+
},
|
|
76
|
+
'Architecture': {
|
|
77
|
+
action: 'Update ARCHITECTURE.md',
|
|
78
|
+
command: 'docguard fix --doc architecture',
|
|
79
|
+
description: 'Architecture documentation doesn\'t match the codebase.',
|
|
80
|
+
},
|
|
81
|
+
'Freshness': {
|
|
82
|
+
action: 'Review stale documents',
|
|
83
|
+
description: 'Documents haven\'t been reviewed since recent code changes. Re-run fix --doc for each stale doc.',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export function runDiagnose(projectDir, config, flags) {
|
|
88
|
+
// ── Step 1: Run guard internally ──
|
|
89
|
+
const guardData = runGuardInternal(projectDir, config);
|
|
90
|
+
const scoreData = runScoreInternal(projectDir, config);
|
|
91
|
+
|
|
92
|
+
// ── Step 2: Collect issues ──
|
|
93
|
+
const issues = [];
|
|
94
|
+
for (const v of guardData.validators) {
|
|
95
|
+
if (v.status === 'skipped' || v.status === 'pass') continue;
|
|
96
|
+
|
|
97
|
+
const fixInfo = FIX_INSTRUCTIONS[v.name] || { action: `Review ${v.name}`, description: 'Manual review needed.' };
|
|
98
|
+
const docTarget = VALIDATOR_TO_DOC[v.name];
|
|
99
|
+
|
|
100
|
+
for (const err of v.errors) {
|
|
101
|
+
issues.push({
|
|
102
|
+
severity: 'error',
|
|
103
|
+
validator: v.name,
|
|
104
|
+
message: err,
|
|
105
|
+
action: fixInfo.action,
|
|
106
|
+
command: fixInfo.command || null,
|
|
107
|
+
docTarget,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
for (const warn of v.warnings) {
|
|
111
|
+
issues.push({
|
|
112
|
+
severity: 'warning',
|
|
113
|
+
validator: v.name,
|
|
114
|
+
message: warn,
|
|
115
|
+
action: fixInfo.action,
|
|
116
|
+
command: fixInfo.command || null,
|
|
117
|
+
docTarget,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Detect stale docs from freshness and map to specific fix --doc targets
|
|
123
|
+
for (const issue of issues) {
|
|
124
|
+
if (issue.validator === 'Freshness' && !issue.docTarget) {
|
|
125
|
+
// Try to extract doc name from warning message
|
|
126
|
+
const match = issue.message.match(/([\w-]+\.md)/i);
|
|
127
|
+
if (match) {
|
|
128
|
+
const docName = match[1].toLowerCase().replace('.md', '');
|
|
129
|
+
const docMap = { 'architecture': 'architecture', 'data-model': 'data-model', 'security': 'security', 'test-spec': 'test-spec', 'environment': 'environment' };
|
|
130
|
+
issue.docTarget = docMap[docName] || null;
|
|
131
|
+
if (issue.docTarget) {
|
|
132
|
+
issue.command = `docguard fix --doc ${issue.docTarget}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Step 3: Output ──
|
|
139
|
+
if (flags.format === 'json') {
|
|
140
|
+
outputJSON(guardData, scoreData, issues);
|
|
141
|
+
} else if (flags.format === 'prompt') {
|
|
142
|
+
outputPrompt(projectDir, guardData, scoreData, issues);
|
|
143
|
+
} else {
|
|
144
|
+
outputText(projectDir, guardData, scoreData, issues);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function outputJSON(guardData, scoreData, issues) {
|
|
149
|
+
const result = {
|
|
150
|
+
project: guardData.project,
|
|
151
|
+
profile: guardData.profile,
|
|
152
|
+
status: guardData.status,
|
|
153
|
+
score: scoreData.score,
|
|
154
|
+
grade: scoreData.grade,
|
|
155
|
+
issueCount: issues.length,
|
|
156
|
+
issues: issues.map(i => ({
|
|
157
|
+
severity: i.severity,
|
|
158
|
+
validator: i.validator,
|
|
159
|
+
message: i.message,
|
|
160
|
+
action: i.action,
|
|
161
|
+
command: i.command,
|
|
162
|
+
docTarget: i.docTarget,
|
|
163
|
+
})),
|
|
164
|
+
// Unique fix commands for automation
|
|
165
|
+
fixCommands: [...new Set(issues.filter(i => i.command).map(i => i.command))],
|
|
166
|
+
timestamp: new Date().toISOString(),
|
|
167
|
+
};
|
|
168
|
+
console.log(JSON.stringify(result, null, 2));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function outputText(projectDir, guardData, scoreData, issues) {
|
|
172
|
+
console.log(`${c.bold}🔍 DocGuard Diagnose — ${guardData.project}${c.reset}`);
|
|
173
|
+
console.log(`${c.dim} Profile: ${guardData.profile} | Score: ${scoreData.score}/100 (${scoreData.grade})${c.reset}`);
|
|
174
|
+
console.log(`${c.dim} Guard: ${guardData.passed}/${guardData.total} passed | Status: ${guardData.status}${c.reset}\n`);
|
|
175
|
+
|
|
176
|
+
if (issues.length === 0) {
|
|
177
|
+
console.log(` ${c.green}${c.bold}✅ All clear!${c.reset} No issues found.\n`);
|
|
178
|
+
console.log(` ${c.dim}Your documentation is healthy. Run \`docguard score --tax\` to see maintenance estimate.${c.reset}\n`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Group by severity
|
|
183
|
+
const errors = issues.filter(i => i.severity === 'error');
|
|
184
|
+
const warnings = issues.filter(i => i.severity === 'warning');
|
|
185
|
+
|
|
186
|
+
if (errors.length > 0) {
|
|
187
|
+
console.log(` ${c.red}${c.bold}Errors (${errors.length}):${c.reset}`);
|
|
188
|
+
for (const e of errors) {
|
|
189
|
+
console.log(` ${c.red}✗${c.reset} [${e.validator}] ${e.message}`);
|
|
190
|
+
if (e.command) console.log(` ${c.dim}Fix: ${e.command}${c.reset}`);
|
|
191
|
+
}
|
|
192
|
+
console.log('');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (warnings.length > 0) {
|
|
196
|
+
console.log(` ${c.yellow}${c.bold}Warnings (${warnings.length}):${c.reset}`);
|
|
197
|
+
for (const w of warnings) {
|
|
198
|
+
console.log(` ${c.yellow}⚠${c.reset} [${w.validator}] ${w.message}`);
|
|
199
|
+
if (w.command) console.log(` ${c.dim}Fix: ${w.command}${c.reset}`);
|
|
200
|
+
}
|
|
201
|
+
console.log('');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Remediation Plan ──
|
|
205
|
+
const commands = [...new Set(issues.filter(i => i.command).map(i => i.command))];
|
|
206
|
+
if (commands.length > 0) {
|
|
207
|
+
console.log(` ${c.bold}📋 Remediation Plan:${c.reset}`);
|
|
208
|
+
for (let i = 0; i < commands.length; i++) {
|
|
209
|
+
console.log(` ${c.cyan}${i + 1}. ${commands[i]}${c.reset}`);
|
|
210
|
+
}
|
|
211
|
+
console.log(` ${c.cyan}${commands.length + 1}. docguard guard${c.reset} ${c.dim}← verify fixes${c.reset}`);
|
|
212
|
+
console.log('');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── AI Prompt (always shown in text mode for easy copy) ──
|
|
216
|
+
console.log(` ${c.bold}🤖 AI-Ready Prompt:${c.reset}`);
|
|
217
|
+
console.log(` ${c.dim}Copy everything below and paste to your AI agent:${c.reset}\n`);
|
|
218
|
+
outputPrompt(undefined, guardData, scoreData, issues);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function outputPrompt(projectDir, guardData, scoreData, issues) {
|
|
222
|
+
if (issues.length === 0) {
|
|
223
|
+
console.log('No issues to fix. Documentation is healthy.');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const lines = [];
|
|
228
|
+
lines.push(`TASK: Fix ${issues.length} documentation issue(s) in project "${guardData.project}"`);
|
|
229
|
+
lines.push(`Profile: ${guardData.profile} | Score: ${scoreData.score}/100 | Guard: ${guardData.status}`);
|
|
230
|
+
lines.push('');
|
|
231
|
+
lines.push('ISSUES FOUND:');
|
|
232
|
+
|
|
233
|
+
for (let i = 0; i < issues.length; i++) {
|
|
234
|
+
const issue = issues[i];
|
|
235
|
+
lines.push(`${i + 1}. [${issue.severity.toUpperCase()}] [${issue.validator}] ${issue.message}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
lines.push('');
|
|
239
|
+
lines.push('REMEDIATION STEPS:');
|
|
240
|
+
|
|
241
|
+
// Group by unique fix commands
|
|
242
|
+
const fixGroups = {};
|
|
243
|
+
for (const issue of issues) {
|
|
244
|
+
const key = issue.command || issue.action;
|
|
245
|
+
if (!fixGroups[key]) {
|
|
246
|
+
fixGroups[key] = { action: issue.action, command: issue.command, docTarget: issue.docTarget, issues: [] };
|
|
247
|
+
}
|
|
248
|
+
fixGroups[key].issues.push(issue.message);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let step = 1;
|
|
252
|
+
for (const [key, group] of Object.entries(fixGroups)) {
|
|
253
|
+
lines.push(`${step}. ${group.action}`);
|
|
254
|
+
if (group.command) {
|
|
255
|
+
lines.push(` Run: ${group.command}`);
|
|
256
|
+
}
|
|
257
|
+
if (group.docTarget) {
|
|
258
|
+
lines.push(` Then research the codebase and write real content for this document.`);
|
|
259
|
+
}
|
|
260
|
+
for (const msg of group.issues) {
|
|
261
|
+
lines.push(` - ${msg}`);
|
|
262
|
+
}
|
|
263
|
+
step++;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
lines.push('');
|
|
267
|
+
lines.push('VALIDATION:');
|
|
268
|
+
lines.push('After making all fixes, run: docguard guard');
|
|
269
|
+
lines.push('Expected result: All checks pass (0 errors, 0 warnings)');
|
|
270
|
+
lines.push(`Target score: ≥${scoreData.score + 5}/100`);
|
|
271
|
+
|
|
272
|
+
console.log(lines.join('\n'));
|
|
273
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff Command — Show differences between canonical docs and implementation
|
|
3
|
+
* Compares what's documented vs what's actually in the code.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
7
|
+
import { resolve, join, extname, basename } from 'node:path';
|
|
8
|
+
import { c } from '../docguard.mjs';
|
|
9
|
+
|
|
10
|
+
const IGNORE_DIRS = new Set([
|
|
11
|
+
'node_modules', '.git', '.next', 'dist', 'build',
|
|
12
|
+
'coverage', '.cache', '__pycache__', '.venv', 'vendor',
|
|
13
|
+
'docs-canonical', 'docs-implementation', 'templates',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const CODE_EXTENSIONS = new Set([
|
|
17
|
+
'.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
|
|
18
|
+
'.py', '.java', '.go', '.rs', '.rb', '.php',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function runDiff(projectDir, config, flags) {
|
|
22
|
+
console.log(`${c.bold}🔍 DocGuard Diff — ${config.projectName}${c.reset}`);
|
|
23
|
+
console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
|
|
24
|
+
|
|
25
|
+
const results = [];
|
|
26
|
+
|
|
27
|
+
// 1. Routes documented vs routes in code
|
|
28
|
+
results.push(diffRoutes(projectDir, config));
|
|
29
|
+
|
|
30
|
+
// 2. Entities documented vs models in code
|
|
31
|
+
results.push(diffEntities(projectDir, config));
|
|
32
|
+
|
|
33
|
+
// 3. Env vars documented vs .env.example
|
|
34
|
+
results.push(diffEnvVars(projectDir, config));
|
|
35
|
+
|
|
36
|
+
// 4. Tech stack documented vs package.json
|
|
37
|
+
results.push(diffTechStack(projectDir, config));
|
|
38
|
+
|
|
39
|
+
// 5. Tests documented vs tests that exist
|
|
40
|
+
results.push(diffTests(projectDir, config));
|
|
41
|
+
|
|
42
|
+
if (flags.format === 'json') {
|
|
43
|
+
console.log(JSON.stringify(results.filter(r => r), null, 2));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Display results
|
|
48
|
+
let hasAnyDiff = false;
|
|
49
|
+
|
|
50
|
+
for (const result of results) {
|
|
51
|
+
if (!result) continue;
|
|
52
|
+
|
|
53
|
+
console.log(` ${c.bold}${result.icon} ${result.title}${c.reset}`);
|
|
54
|
+
|
|
55
|
+
if (result.onlyInDocs.length > 0) {
|
|
56
|
+
hasAnyDiff = true;
|
|
57
|
+
console.log(` ${c.yellow}Documented but not found in code:${c.reset}`);
|
|
58
|
+
for (const item of result.onlyInDocs) {
|
|
59
|
+
console.log(` ${c.yellow}− ${item}${c.reset}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (result.onlyInCode.length > 0) {
|
|
64
|
+
hasAnyDiff = true;
|
|
65
|
+
console.log(` ${c.red}In code but not documented:${c.reset}`);
|
|
66
|
+
for (const item of result.onlyInCode) {
|
|
67
|
+
console.log(` ${c.red}+ ${item}${c.reset}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (result.matched.length > 0 && flags.verbose) {
|
|
72
|
+
console.log(` ${c.green}Matched (${result.matched.length}):${c.reset}`);
|
|
73
|
+
for (const item of result.matched) {
|
|
74
|
+
console.log(` ${c.green}✓ ${item}${c.reset}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (result.onlyInDocs.length === 0 && result.onlyInCode.length === 0) {
|
|
79
|
+
console.log(` ${c.green}✓ In sync${c.reset}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log('');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!hasAnyDiff) {
|
|
86
|
+
console.log(` ${c.green}${c.bold}✅ No drift detected — canonical docs match implementation!${c.reset}\n`);
|
|
87
|
+
} else {
|
|
88
|
+
console.log(` ${c.yellow}${c.bold}⚠️ Drift detected — update canonical docs or code to match.${c.reset}\n`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Diff Functions ─────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
function diffRoutes(dir) {
|
|
95
|
+
const archPath = resolve(dir, 'docs-canonical/ARCHITECTURE.md');
|
|
96
|
+
if (!existsSync(archPath)) return null;
|
|
97
|
+
|
|
98
|
+
const content = readFileSync(archPath, 'utf-8');
|
|
99
|
+
|
|
100
|
+
// Extract route-like patterns from ARCHITECTURE.md
|
|
101
|
+
const docRoutes = new Set();
|
|
102
|
+
const routeRegex = /(?:\/api\/\S+|GET|POST|PUT|DELETE|PATCH)\s+(\S+)/gi;
|
|
103
|
+
let match;
|
|
104
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
105
|
+
docRoutes.add(match[1] || match[0]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Also check for paths in tables
|
|
109
|
+
const pathRegex = /`(\/api\/[^`]+)`/g;
|
|
110
|
+
while ((match = pathRegex.exec(content)) !== null) {
|
|
111
|
+
docRoutes.add(match[1]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Find route files in code
|
|
115
|
+
const codeRoutes = new Set();
|
|
116
|
+
const routeDirs = ['src/routes', 'src/app/api', 'routes', 'api'];
|
|
117
|
+
for (const rd of routeDirs) {
|
|
118
|
+
const routeDir = resolve(dir, rd);
|
|
119
|
+
if (!existsSync(routeDir)) continue;
|
|
120
|
+
|
|
121
|
+
const files = getFilesRecursive(routeDir);
|
|
122
|
+
for (const f of files) {
|
|
123
|
+
const rel = f.replace(dir + '/', '');
|
|
124
|
+
codeRoutes.add(rel);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
title: 'API Routes',
|
|
130
|
+
icon: '🛣️',
|
|
131
|
+
onlyInDocs: [...docRoutes].filter(r => ![...codeRoutes].some(cr => cr.includes(r.replace(/\//g, '/')))),
|
|
132
|
+
onlyInCode: [...codeRoutes].filter(cr => {
|
|
133
|
+
const name = basename(cr, extname(cr));
|
|
134
|
+
return ![...docRoutes].some(dr => dr.includes(name));
|
|
135
|
+
}),
|
|
136
|
+
matched: [...codeRoutes].filter(cr => {
|
|
137
|
+
const name = basename(cr, extname(cr));
|
|
138
|
+
return [...docRoutes].some(dr => dr.includes(name));
|
|
139
|
+
}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function diffEntities(dir) {
|
|
144
|
+
const dataModelPath = resolve(dir, 'docs-canonical/DATA-MODEL.md');
|
|
145
|
+
if (!existsSync(dataModelPath)) return null;
|
|
146
|
+
|
|
147
|
+
const content = readFileSync(dataModelPath, 'utf-8');
|
|
148
|
+
|
|
149
|
+
// Extract entity names from DATA-MODEL.md (look for ### headers or table rows)
|
|
150
|
+
const docEntities = new Set();
|
|
151
|
+
|
|
152
|
+
// Filter out template placeholders and common header noise
|
|
153
|
+
const HEADER_NOISE = new Set([
|
|
154
|
+
'EntityName', 'Entity', 'metadata', 'tbd', 'cascade', 'fields',
|
|
155
|
+
'purpose', 'version', 'author', 'example', 'TODO', 'Overview',
|
|
156
|
+
'Revision', 'History', 'Entities', 'Relationships', 'Indexes',
|
|
157
|
+
'Migration', 'Strategy',
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
const headerRegex = /^### (\S+)/gm;
|
|
161
|
+
let match;
|
|
162
|
+
while ((match = headerRegex.exec(content)) !== null) {
|
|
163
|
+
const name = match[1].replace(/[`*]/g, '');
|
|
164
|
+
// Skip template placeholders (<!-- ... -->) and noise words
|
|
165
|
+
if (name.startsWith('<!--') || name.length <= 1 || HEADER_NOISE.has(name) || HEADER_NOISE.has(name.toLowerCase())) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
docEntities.add(name.toLowerCase());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Also check tables for entity references
|
|
172
|
+
const tableRegex = /\|\s*(?:`)?(\w+)(?:`)?\s*\|/g;
|
|
173
|
+
// Filter out common table headers, template placeholders, and markdown noise
|
|
174
|
+
const TABLE_NOISE = new Set([
|
|
175
|
+
'entity', 'field', 'type', 'from', 'to', 'table', 'index', 'storage',
|
|
176
|
+
'required', 'default', 'constraints', 'description', 'name', 'value',
|
|
177
|
+
'status', 'version', 'category', 'technology', 'license', 'purpose',
|
|
178
|
+
'cascade', 'relationship', 'notes', 'date', 'author', 'changes',
|
|
179
|
+
'metadata', 'tbd', 'fields', 'todo', 'example', 'primary', 'key',
|
|
180
|
+
'none', 'see', 'detected', 'yes', 'no', 'all', 'the', 'for', 'not',
|
|
181
|
+
'add', 'database', 'orm', 'source', 'unit', 'test', 'integration',
|
|
182
|
+
'metric', 'target', 'current', 'journey', 'file', 'score', 'weight',
|
|
183
|
+
'weighted', 'method', 'provider', 'token', 'expiry', 'role',
|
|
184
|
+
'permissions', 'secret', 'rotation', 'access', 'variable', 'tool',
|
|
185
|
+
'command', 'run', 'component', 'responsibility', 'location', 'tests',
|
|
186
|
+
]);
|
|
187
|
+
while ((match = tableRegex.exec(content)) !== null) {
|
|
188
|
+
const name = match[1];
|
|
189
|
+
if (name.length > 2 && !TABLE_NOISE.has(name.toLowerCase())) {
|
|
190
|
+
docEntities.add(name.toLowerCase());
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Find model/entity files in code
|
|
195
|
+
const codeEntities = new Set();
|
|
196
|
+
const modelDirs = ['src/models', 'models', 'src/entities', 'entities', 'src/schema', 'schema', 'prisma'];
|
|
197
|
+
for (const md of modelDirs) {
|
|
198
|
+
const modelDir = resolve(dir, md);
|
|
199
|
+
if (!existsSync(modelDir)) continue;
|
|
200
|
+
|
|
201
|
+
const files = getFilesRecursive(modelDir);
|
|
202
|
+
for (const f of files) {
|
|
203
|
+
const name = basename(f, extname(f)).toLowerCase();
|
|
204
|
+
if (name !== 'index') {
|
|
205
|
+
codeEntities.add(name);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
title: 'Data Entities',
|
|
212
|
+
icon: '🗃️',
|
|
213
|
+
onlyInDocs: [...docEntities].filter(d => ![...codeEntities].some(ce => ce.includes(d) || d.includes(ce))),
|
|
214
|
+
onlyInCode: [...codeEntities].filter(ce => ![...docEntities].some(d => d.includes(ce) || ce.includes(d))),
|
|
215
|
+
matched: [...codeEntities].filter(ce => [...docEntities].some(d => d.includes(ce) || ce.includes(d))),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function diffEnvVars(dir) {
|
|
220
|
+
const envDocPath = resolve(dir, 'docs-canonical/ENVIRONMENT.md');
|
|
221
|
+
if (!existsSync(envDocPath)) return null;
|
|
222
|
+
|
|
223
|
+
const content = readFileSync(envDocPath, 'utf-8');
|
|
224
|
+
|
|
225
|
+
// Extract env var names from ENVIRONMENT.md
|
|
226
|
+
const docVars = new Set();
|
|
227
|
+
const varRegex = /`([A-Z][A-Z0-9_]{2,})`/g;
|
|
228
|
+
let match;
|
|
229
|
+
while ((match = varRegex.exec(content)) !== null) {
|
|
230
|
+
docVars.add(match[1]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Read .env.example
|
|
234
|
+
const codeVars = new Set();
|
|
235
|
+
const envExamplePath = resolve(dir, '.env.example');
|
|
236
|
+
if (existsSync(envExamplePath)) {
|
|
237
|
+
const envContent = readFileSync(envExamplePath, 'utf-8');
|
|
238
|
+
const envRegex = /^([A-Z][A-Z0-9_]+)\s*=/gm;
|
|
239
|
+
while ((match = envRegex.exec(envContent)) !== null) {
|
|
240
|
+
codeVars.add(match[1]);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (docVars.size === 0 && codeVars.size === 0) return null;
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
title: 'Environment Variables',
|
|
248
|
+
icon: '🔧',
|
|
249
|
+
onlyInDocs: [...docVars].filter(v => !codeVars.has(v)),
|
|
250
|
+
onlyInCode: [...codeVars].filter(v => !docVars.has(v)),
|
|
251
|
+
matched: [...docVars].filter(v => codeVars.has(v)),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function diffTechStack(dir) {
|
|
256
|
+
const archPath = resolve(dir, 'docs-canonical/ARCHITECTURE.md');
|
|
257
|
+
const pkgPath = resolve(dir, 'package.json');
|
|
258
|
+
if (!existsSync(archPath) || !existsSync(pkgPath)) return null;
|
|
259
|
+
|
|
260
|
+
const archContent = readFileSync(archPath, 'utf-8');
|
|
261
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
262
|
+
|
|
263
|
+
// Extract tech from ARCHITECTURE.md
|
|
264
|
+
const docTech = new Set();
|
|
265
|
+
const techPatterns = ['React', 'Next.js', 'Vue', 'Angular', 'Svelte', 'Express', 'Fastify', 'Hono',
|
|
266
|
+
'PostgreSQL', 'MySQL', 'MongoDB', 'DynamoDB', 'Redis', 'Prisma', 'Drizzle',
|
|
267
|
+
'TypeScript', 'Tailwind', 'Docker', 'Terraform'];
|
|
268
|
+
|
|
269
|
+
for (const tech of techPatterns) {
|
|
270
|
+
if (archContent.toLowerCase().includes(tech.toLowerCase())) {
|
|
271
|
+
docTech.add(tech);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Extract from package.json
|
|
276
|
+
const codeTech = new Set();
|
|
277
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
278
|
+
const depMap = {
|
|
279
|
+
'react': 'React', 'next': 'Next.js', 'vue': 'Vue', 'express': 'Express',
|
|
280
|
+
'fastify': 'Fastify', 'hono': 'Hono', 'prisma': 'Prisma', '@prisma/client': 'Prisma',
|
|
281
|
+
'drizzle-orm': 'Drizzle', 'typescript': 'TypeScript', 'tailwindcss': 'Tailwind',
|
|
282
|
+
'redis': 'Redis', 'ioredis': 'Redis', 'pg': 'PostgreSQL', 'mysql2': 'MySQL',
|
|
283
|
+
'mongoose': 'MongoDB', '@aws-sdk/client-dynamodb': 'DynamoDB',
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
for (const [dep, tech] of Object.entries(depMap)) {
|
|
287
|
+
if (allDeps[dep]) codeTech.add(tech);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (docTech.size === 0 && codeTech.size === 0) return null;
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
title: 'Tech Stack',
|
|
294
|
+
icon: '⚙️',
|
|
295
|
+
onlyInDocs: [...docTech].filter(t => !codeTech.has(t)),
|
|
296
|
+
onlyInCode: [...codeTech].filter(t => !docTech.has(t)),
|
|
297
|
+
matched: [...docTech].filter(t => codeTech.has(t)),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function diffTests(dir) {
|
|
302
|
+
const testSpecPath = resolve(dir, 'docs-canonical/TEST-SPEC.md');
|
|
303
|
+
if (!existsSync(testSpecPath)) return null;
|
|
304
|
+
|
|
305
|
+
const content = readFileSync(testSpecPath, 'utf-8');
|
|
306
|
+
|
|
307
|
+
// Extract test file references from TEST-SPEC.md
|
|
308
|
+
const docTests = new Set();
|
|
309
|
+
const testFileRegex = /`([^`]*\.(?:test|spec)\.[^`]+)`/g;
|
|
310
|
+
let match;
|
|
311
|
+
while ((match = testFileRegex.exec(content)) !== null) {
|
|
312
|
+
docTests.add(match[1]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Find actual test files
|
|
316
|
+
const codeTests = new Set();
|
|
317
|
+
const testDirs = ['tests', 'test', '__tests__', 'spec', 'e2e'];
|
|
318
|
+
for (const td of testDirs) {
|
|
319
|
+
const testDir = resolve(dir, td);
|
|
320
|
+
if (!existsSync(testDir)) continue;
|
|
321
|
+
|
|
322
|
+
const files = getFilesRecursive(testDir);
|
|
323
|
+
for (const f of files) {
|
|
324
|
+
const rel = f.replace(dir + '/', '');
|
|
325
|
+
codeTests.add(rel);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (docTests.size === 0 && codeTests.size === 0) return null;
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
title: 'Test Files',
|
|
333
|
+
icon: '🧪',
|
|
334
|
+
onlyInDocs: [...docTests].filter(t => !codeTests.has(t)),
|
|
335
|
+
onlyInCode: [...codeTests].filter(t => !docTests.has(t)),
|
|
336
|
+
matched: [...docTests].filter(t => codeTests.has(t)),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Utilities ──────────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
function getFilesRecursive(dir) {
|
|
343
|
+
const results = [];
|
|
344
|
+
if (!existsSync(dir)) return results;
|
|
345
|
+
|
|
346
|
+
const entries = readdirSync(dir);
|
|
347
|
+
for (const entry of entries) {
|
|
348
|
+
if (IGNORE_DIRS.has(entry) || entry.startsWith('.')) continue;
|
|
349
|
+
const fullPath = join(dir, entry);
|
|
350
|
+
try {
|
|
351
|
+
const stat = statSync(fullPath);
|
|
352
|
+
if (stat.isDirectory()) {
|
|
353
|
+
results.push(...getFilesRecursive(fullPath));
|
|
354
|
+
} else if (stat.isFile() && CODE_EXTENSIONS.has(extname(fullPath))) {
|
|
355
|
+
results.push(fullPath);
|
|
356
|
+
}
|
|
357
|
+
} catch { /* skip */ }
|
|
358
|
+
}
|
|
359
|
+
return results;
|
|
360
|
+
}
|