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