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.
@@ -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, join } from 'node:path';
7
+ import { resolve, dirname } from 'node:path';
7
8
  import { fileURLToPath } from 'node:url';
8
- import { c, PROFILES } from '../docguard.mjs';
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
- export function runInit(projectDir, config, flags) {
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
- // Map template files to their destinations
50
- const allMappings = [
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
- // Filter based on profile — starter only gets required files
62
- const profileRequiredFiles = profile.requiredFiles
63
- ? new Set([...profile.requiredFiles.canonical, profile.requiredFiles.changelog, profile.requiredFiles.driftLog, ...profile.requiredFiles.agentFile])
64
- : null;
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 if it doesn't exist — auto-detect project type
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}(auto-detected: ${detectedType})${c.reset}`);
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
- // Install slash commands for AI agents — detect which agents are in use
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])) // check parent dir exists
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
- if (flags.skipPrompts) {
214
- // Simple instructions, no AI prompts
215
- console.log(`\n ${c.bold}Next steps:${c.reset}`);
216
- console.log(` ${c.dim}Run${c.reset} ${c.cyan}docguard diagnose${c.reset} ${c.dim}to get AI prompts for filling docs.${c.reset}`);
217
- console.log(` ${c.dim}Then verify:${c.reset} ${c.cyan}docguard guard${c.reset}\n`);
218
- } else {
219
- // Auto-populate: output AI research prompts for each created canonical doc
220
- const createdDocs = created.filter(f => f.startsWith('docs-canonical/'));
221
-
222
- if (createdDocs.length > 0) {
223
- console.log(`\n ${c.bold}🤖 AI Auto-Populate${c.reset}`);
224
- console.log(` ${c.dim}The files above are skeleton templates. Your AI agent should fill them.${c.reset}`);
225
- console.log(` ${c.dim}Run this single command to get a full remediation plan:${c.reset}\n`);
226
- console.log(` ${c.cyan}${c.bold}docguard diagnose${c.reset}\n`);
227
- console.log(` ${c.dim}Or generate prompts for individual docs:${c.reset}`);
228
-
229
- const docNameMap = {
230
- 'docs-canonical/ARCHITECTURE.md': 'architecture',
231
- 'docs-canonical/DATA-MODEL.md': 'data-model',
232
- 'docs-canonical/SECURITY.md': 'security',
233
- 'docs-canonical/TEST-SPEC.md': 'test-spec',
234
- 'docs-canonical/ENVIRONMENT.md': 'environment',
235
- };
236
-
237
- for (const doc of createdDocs) {
238
- const target = docNameMap[doc];
239
- if (target) {
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
-
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
10
10
  import { resolve, basename } from 'node:path';
11
- import { c } from '../docguard.mjs';
11
+ import { c } from '../shared.mjs';
12
12
 
13
13
  const SUPPORTED_PLATFORMS = ['mintlify'];
14
14
 
@@ -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 '../docguard.mjs';
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: ![CDD Score](${badgeUrl})${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
- scores.docQuality = calcDocQualityScore(projectDir, config);
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)) continue;
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)) found++;
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
- // Bonus: check if doc has docguard metadata
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) found++;
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
- return total === 0 ? 0 : Math.round((found / total) * 100);
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
- if (hasTestConfig) score += 15;
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 (existsSync(resolve(dir, '.env.example'))) score += 15;
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
- if (existsSync(resolve(dir, '.env.example'))) score += 30;
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: 'Fill in template sections replace placeholders with real content',
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 to ARCHITECTURE.md',
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
  }
@@ -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 '../docguard.mjs';
11
+ import { c } from '../shared.mjs';
12
12
 
13
13
  const IGNORE_DIRS = new Set([
14
14
  'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
@@ -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 '../docguard.mjs';
12
+ import { c } from '../shared.mjs';
13
13
  import { runGuardInternal } from './guard.mjs';
14
14
 
15
15
  const DEBOUNCE_MS = 500;