docguard-cli 0.7.3 → 0.8.2
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/README.md +31 -3
- 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 +16 -9
- package/cli/commands/diff.mjs +14 -3
- package/cli/commands/fix.mjs +4 -3
- package/cli/commands/generate.mjs +60 -2
- package/cli/commands/guard.mjs +30 -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 +106 -0
- package/cli/validators/docs-coverage.mjs +387 -0
- package/cli/validators/docs-diff.mjs +185 -0
- package/cli/validators/metadata-sync.mjs +179 -0
- package/cli/validators/metrics-consistency.mjs +166 -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/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;
|