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/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
- import { runAudit } from './commands/audit.mjs';
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
- // ── Colors (ANSI escape codes, zero deps) ──────────────────────────────────
42
- export const c = {
43
- reset: '\x1b[0m',
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
- // Export profiles for use by other commands (init --profile)
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}Commands:${c.reset}
277
- ${c.green}audit${c.reset} Scan project, report what CDD docs exist or are missing
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
- ${c.green}hooks${c.reset} Install git hooks (pre-commit, pre-push, commit-msg)
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
- ${c.green}ci${c.reset} Single command for CI/CD pipelines (guard + score)
288
- ${c.green}fix${c.reset} Find issues and generate AI fix instructions
289
- ${c.green}watch${c.reset} Watch for file changes and re-run guard automatically
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}# Quick start for a side project${c.reset}
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
- runAudit(projectDir, config, flags);
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,106 @@
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
+ };
66
+
67
+ // ── .docguardignore Support ───────────────────────────────────────────────
68
+ import { existsSync, readFileSync } from 'node:fs';
69
+ import { resolve, relative } from 'node:path';
70
+
71
+ /**
72
+ * Load ignore patterns from .docguardignore (like .gitignore).
73
+ * Returns a function that checks if a relative path should be ignored.
74
+ *
75
+ * Format: one pattern per line, # comments, blank lines skipped.
76
+ * Supports simple glob: * (any chars), ** (any path segments).
77
+ *
78
+ * @param {string} projectDir - Project root
79
+ * @returns {(relPath: string) => boolean} - Returns true if file should be ignored
80
+ */
81
+ export function loadIgnorePatterns(projectDir) {
82
+ const ignorePath = resolve(projectDir, '.docguardignore');
83
+ if (!existsSync(ignorePath)) return () => false;
84
+
85
+ let content;
86
+ try { content = readFileSync(ignorePath, 'utf-8'); } catch { return () => false; }
87
+
88
+ const patterns = content
89
+ .split('\n')
90
+ .map(line => line.trim())
91
+ .filter(line => line && !line.startsWith('#'))
92
+ .map(pattern => {
93
+ // Convert glob to regex:
94
+ // ** → match any path segments
95
+ // * → match any chars except /
96
+ // . → literal dot
97
+ const escaped = pattern
98
+ .replace(/\./g, '\\.')
99
+ .replace(/\*\*/g, '§§') // temp placeholder
100
+ .replace(/\*/g, '[^/]*')
101
+ .replace(/§§/g, '.*');
102
+ return new RegExp(`^${escaped}$|/${escaped}$|^${escaped}/|/${escaped}/`);
103
+ });
104
+
105
+ return (relPath) => patterns.some(regex => regex.test(relPath));
106
+ }
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Docs-Coverage Validator — Detects code features not referenced in docs.
3
+ *
4
+ * Generic validator for ANY project type. Scans the project for
5
+ * "documentable artifacts" and checks if at least one canonical doc
6
+ * or README references them.
7
+ *
8
+ * What it catches:
9
+ * - Config/dotfiles at root not mentioned in docs
10
+ * - Config filenames referenced in source code (resolve/readFile calls) but not documented
11
+ * - package.json bin entries not documented
12
+ * - Source directories not referenced in ARCHITECTURE.md
13
+ * - README.md missing standard sections (inspired by Standard README spec)
14
+ */
15
+
16
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
17
+ import { resolve, join, relative, basename, extname } from 'node:path';
18
+
19
+ const IGNORE_DIRS = new Set([
20
+ 'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
21
+ '.cache', '__pycache__', '.venv', 'vendor', '.turbo', '.vercel',
22
+ ]);
23
+
24
+ // Dotfiles that are universally common and don't need documentation
25
+ const COMMON_DOTFILES = new Set([
26
+ '.gitignore', '.gitattributes', '.git', '.DS_Store',
27
+ '.editorconfig', '.prettierrc', '.prettierignore',
28
+ '.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.cjs',
29
+ '.eslintignore', '.nvmrc', '.node-version', '.npmrc', '.npmignore',
30
+ '.env', '.env.local', '.env.development', '.env.production',
31
+ '.vscode', '.idea', '.github', '.husky',
32
+ '.babelrc', '.browserslistrc', '.stylelintrc',
33
+ ]);
34
+
35
+ /**
36
+ * Validate that code artifacts are referenced in documentation.
37
+ * @param {string} projectDir - Project root directory
38
+ * @param {object} config - DocGuard config
39
+ * @returns {{ errors: string[], warnings: string[], passed: number, total: number }}
40
+ */
41
+ export function validateDocsCoverage(projectDir, config) {
42
+ const warnings = [];
43
+ let passed = 0;
44
+ let total = 0;
45
+
46
+ // Collect all doc content for searching
47
+ const allDocContent = collectDocContent(projectDir);
48
+ if (!allDocContent) {
49
+ return { errors: [], warnings, passed: 0, total: 0 };
50
+ }
51
+
52
+ // ── Check 1: Project-specific config/dotfiles referenced in docs ──
53
+ const configChecks = checkConfigFiles(projectDir, allDocContent);
54
+ total += configChecks.total;
55
+ passed += configChecks.passed;
56
+ warnings.push(...configChecks.warnings);
57
+
58
+ // ── Check 2: package.json bin entries documented ──
59
+ const binChecks = checkPackageBins(projectDir, allDocContent);
60
+ total += binChecks.total;
61
+ passed += binChecks.passed;
62
+ warnings.push(...binChecks.warnings);
63
+
64
+ // ── Check 3: Source directory structure matches ARCHITECTURE.md ──
65
+ const dirChecks = checkSourceDirs(projectDir, allDocContent);
66
+ total += dirChecks.total;
67
+ passed += dirChecks.passed;
68
+ warnings.push(...dirChecks.warnings);
69
+
70
+ // ── Check 4: Config filenames referenced in source code but not documented ──
71
+ const codeConfigChecks = checkCodeReferencedConfigs(projectDir, allDocContent);
72
+ total += codeConfigChecks.total;
73
+ passed += codeConfigChecks.passed;
74
+ warnings.push(...codeConfigChecks.warnings);
75
+
76
+ // ── Check 5: README section completeness (Standard README spec) ──
77
+ const readmeChecks = checkReadmeSections(projectDir);
78
+ total += readmeChecks.total;
79
+ passed += readmeChecks.passed;
80
+ warnings.push(...readmeChecks.warnings);
81
+
82
+ return { errors: [], warnings, passed, total };
83
+ }
84
+
85
+ // ── Check Functions ─────────────────────────────────────────────────────────
86
+
87
+ /**
88
+ * Check 1: Project-specific config/dotfiles are mentioned in docs.
89
+ * Skips universally common files (.gitignore, .eslintrc, etc.).
90
+ */
91
+ function checkConfigFiles(projectDir, allDocContent) {
92
+ const warnings = [];
93
+ let passed = 0;
94
+ let total = 0;
95
+
96
+ let entries;
97
+ try { entries = readdirSync(projectDir); } catch { return { warnings, passed, total }; }
98
+
99
+ const lowerDocContent = allDocContent.toLowerCase();
100
+
101
+ for (const entry of entries) {
102
+ const isDotFile = entry.startsWith('.');
103
+ const isProjectConfig = entry.endsWith('.config.js') ||
104
+ entry.endsWith('.config.ts') ||
105
+ entry.endsWith('.config.mjs') ||
106
+ entry.endsWith('.config.cjs') ||
107
+ entry.endsWith('.json') && !['package.json', 'package-lock.json', 'tsconfig.json'].includes(entry);
108
+
109
+ if (!isDotFile && !isProjectConfig) continue;
110
+ if (COMMON_DOTFILES.has(entry)) continue;
111
+ if (entry === 'tsconfig.json' || entry === 'package-lock.json') continue;
112
+
113
+ total++;
114
+ if (lowerDocContent.includes(entry.toLowerCase())) {
115
+ passed++;
116
+ } else {
117
+ warnings.push(
118
+ `Config file "${entry}" exists but is not mentioned in any documentation. Document its purpose in ARCHITECTURE.md or README.md`
119
+ );
120
+ }
121
+ }
122
+
123
+ return { warnings, passed, total };
124
+ }
125
+
126
+ /**
127
+ * Check 2: package.json bin entries (CLI commands users run) are documented.
128
+ */
129
+ function checkPackageBins(projectDir, allDocContent) {
130
+ const warnings = [];
131
+ let passed = 0;
132
+ let total = 0;
133
+
134
+ const pkgPath = resolve(projectDir, 'package.json');
135
+ if (!existsSync(pkgPath)) return { warnings, passed, total };
136
+
137
+ let pkg;
138
+ try { pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); } catch { return { warnings, passed, total }; }
139
+
140
+ const bins = typeof pkg.bin === 'string'
141
+ ? { [pkg.name]: pkg.bin }
142
+ : (pkg.bin || {});
143
+
144
+ const lowerDocContent = allDocContent.toLowerCase();
145
+
146
+ for (const [binName] of Object.entries(bins)) {
147
+ total++;
148
+ if (lowerDocContent.includes(binName.toLowerCase())) {
149
+ passed++;
150
+ } else {
151
+ warnings.push(
152
+ `package.json defines CLI command "${binName}" but it's not mentioned in any documentation`
153
+ );
154
+ }
155
+ }
156
+
157
+ return { warnings, passed, total };
158
+ }
159
+
160
+ /**
161
+ * Check 3: Source directories are referenced in ARCHITECTURE.md.
162
+ */
163
+ function checkSourceDirs(projectDir, allDocContent) {
164
+ const warnings = [];
165
+ let passed = 0;
166
+ let total = 0;
167
+
168
+ const archPath = resolve(projectDir, 'docs-canonical/ARCHITECTURE.md');
169
+ if (!existsSync(archPath)) return { warnings, passed, total };
170
+
171
+ let archContent;
172
+ try { archContent = readFileSync(archPath, 'utf-8'); } catch { return { warnings, passed, total }; }
173
+
174
+ const lowerArchContent = archContent.toLowerCase();
175
+ const sourceRoots = ['src', 'lib', 'app', 'cli', 'server', 'api'];
176
+
177
+ for (const root of sourceRoots) {
178
+ const rootDir = resolve(projectDir, root);
179
+ if (!existsSync(rootDir)) continue;
180
+
181
+ let entries;
182
+ try { entries = readdirSync(rootDir); } catch { continue; }
183
+
184
+ for (const entry of entries) {
185
+ const fullPath = join(rootDir, entry);
186
+ try {
187
+ const stat = statSync(fullPath);
188
+ if (!stat.isDirectory()) continue;
189
+ } catch { continue; }
190
+
191
+ if (IGNORE_DIRS.has(entry) || entry.startsWith('.') || entry === '__tests__' || entry === '__test__') continue;
192
+
193
+ total++;
194
+ const searchName = entry.toLowerCase();
195
+ if (lowerArchContent.includes(searchName) || lowerArchContent.includes(root + '/' + entry)) {
196
+ passed++;
197
+ } else {
198
+ warnings.push(
199
+ `Source directory "${root}/${entry}/" is not referenced in ARCHITECTURE.md`
200
+ );
201
+ }
202
+ }
203
+ }
204
+
205
+ return { warnings, passed, total };
206
+ }
207
+
208
+ /**
209
+ * Check 4: Config files that code actually READS are documented.
210
+ *
211
+ * Scans source code for resolve(dir, '.configname') and existsSync('.configname')
212
+ * patterns — these are configs the project USES. Avoids matching config names
213
+ * sitting in arrays (scan patterns for detecting other projects' configs).
214
+ */
215
+ function checkCodeReferencedConfigs(projectDir, allDocContent) {
216
+ const warnings = [];
217
+ let passed = 0;
218
+ let total = 0;
219
+
220
+ const lowerDocContent = allDocContent.toLowerCase();
221
+ const foundConfigs = new Set();
222
+
223
+ // Only match config filenames inside function calls that actually USE the file:
224
+ // resolve(dir, '.docguardignore'), existsSync('.env.example'), readFileSync('vitest.config.ts')
225
+ const usageRegex = /(?:resolve|join|existsSync|readFileSync|accessSync|writeFileSync)\s*\([^)]*['"`]([^'"`\n]{2,})['"`]/g;
226
+
227
+ const sourceRoots = ['src', 'lib', 'cli', 'bin', 'server', 'api', 'app'];
228
+
229
+ const scanFile = (filePath) => {
230
+ const ext = extname(filePath);
231
+ if (!['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'].includes(ext)) return;
232
+ let content;
233
+ try { content = readFileSync(filePath, 'utf-8'); } catch { return; }
234
+
235
+ usageRegex.lastIndex = 0;
236
+ let match;
237
+ while ((match = usageRegex.exec(content)) !== null) {
238
+ const name = match[1];
239
+ // Must be a dotfile (.something) or *.config.* — not a path
240
+ if (name.includes('/') || name.startsWith('..')) continue;
241
+ const isDotConfig = name.startsWith('.') && name.length > 2;
242
+ const isNamedConfig = /^[\w-]+\.config\.\w+$/.test(name);
243
+ if (!isDotConfig && !isNamedConfig) continue;
244
+ // Skip bare extensions
245
+ if (/^\.[a-z]{1,4}$/i.test(name)) continue;
246
+ foundConfigs.add(name);
247
+ }
248
+ };
249
+
250
+ for (const root of sourceRoots) {
251
+ const rootDir = resolve(projectDir, root);
252
+ if (!existsSync(rootDir)) continue;
253
+ walkFiles(rootDir, scanFile);
254
+ }
255
+
256
+ for (const configName of foundConfigs) {
257
+ if (COMMON_DOTFILES.has(configName)) continue;
258
+ total++;
259
+ if (lowerDocContent.includes(configName.toLowerCase())) {
260
+ passed++;
261
+ } else {
262
+ warnings.push(
263
+ `Code references config file "${configName}" but no documentation mentions it. Add it to README.md or ARCHITECTURE.md`
264
+ );
265
+ }
266
+ }
267
+
268
+ return { warnings, passed, total };
269
+ }
270
+
271
+ /**
272
+ * Check 5: README section completeness.
273
+ * Inspired by Standard README (https://github.com/RichardLitt/standard-readme)
274
+ * and Make a README (https://www.makeareadme.com/).
275
+ */
276
+ function checkReadmeSections(projectDir) {
277
+ const warnings = [];
278
+ let passed = 0;
279
+ let total = 0;
280
+
281
+ const readmePath = resolve(projectDir, 'README.md');
282
+ if (!existsSync(readmePath)) return { warnings, passed, total };
283
+
284
+ let content;
285
+ try { content = readFileSync(readmePath, 'utf-8'); } catch { return { warnings, passed, total }; }
286
+
287
+ const lowerContent = content.toLowerCase();
288
+
289
+ // Required sections — every well-documented project should have these
290
+ const requiredSections = [
291
+ { name: 'Installation', patterns: ['install', 'getting started', 'setup', 'quickstart', 'quick start'] },
292
+ { name: 'Usage', patterns: ['usage', 'how to use', 'examples', 'getting started'] },
293
+ { name: 'License', patterns: ['license', 'licence'] },
294
+ ];
295
+
296
+ // Recommended — count toward score but don't warn
297
+ const recommendedSections = [
298
+ { name: 'Contributing', patterns: ['contributing', 'contribution', 'how to contribute'] },
299
+ { name: 'Description', patterns: ['## what', '## about', '## description', '## overview'] },
300
+ ];
301
+
302
+ for (const section of requiredSections) {
303
+ total++;
304
+ if (section.patterns.some(p => lowerContent.includes(p))) {
305
+ passed++;
306
+ } else {
307
+ warnings.push(`README.md is missing a "${section.name}" section (Standard README spec)`);
308
+ }
309
+ }
310
+
311
+ for (const section of recommendedSections) {
312
+ total++;
313
+ if (section.patterns.some(p => lowerContent.includes(p))) {
314
+ passed++;
315
+ }
316
+ }
317
+
318
+ return { warnings, passed, total };
319
+ }
320
+
321
+ // ── Helpers ──────────────────────────────────────────────────────────────────
322
+
323
+ /**
324
+ * Collect all documentation content into a single searchable string.
325
+ */
326
+ function collectDocContent(projectDir) {
327
+ const docPaths = [];
328
+
329
+ const rootDocs = ['README.md', 'AGENTS.md', 'CLAUDE.md', 'CONTRIBUTING.md', 'STANDARD.md'];
330
+ for (const doc of rootDocs) {
331
+ const p = resolve(projectDir, doc);
332
+ if (existsSync(p)) docPaths.push(p);
333
+ }
334
+
335
+ const canonDir = resolve(projectDir, 'docs-canonical');
336
+ if (existsSync(canonDir)) {
337
+ try {
338
+ for (const entry of readdirSync(canonDir)) {
339
+ if (entry.endsWith('.md')) docPaths.push(resolve(canonDir, entry));
340
+ }
341
+ } catch { /* skip */ }
342
+ }
343
+
344
+ const extDir = resolve(projectDir, 'extensions');
345
+ if (existsSync(extDir)) {
346
+ walkFiles(extDir, (f) => {
347
+ if (f.endsWith('.md') || f.endsWith('.yml') || f.endsWith('.yaml')) {
348
+ docPaths.push(f);
349
+ }
350
+ });
351
+ }
352
+
353
+ for (const docsDir of ['docs', 'docs-implementation']) {
354
+ const d = resolve(projectDir, docsDir);
355
+ if (existsSync(d)) {
356
+ walkFiles(d, (f) => {
357
+ if (f.endsWith('.md')) docPaths.push(f);
358
+ });
359
+ }
360
+ }
361
+
362
+ if (docPaths.length === 0) return null;
363
+ const parts = [];
364
+ for (const p of docPaths) {
365
+ try { parts.push(readFileSync(p, 'utf-8')); } catch { /* skip */ }
366
+ }
367
+ return parts.join('\n');
368
+ }
369
+
370
+ function walkFiles(dir, callback) {
371
+ if (!existsSync(dir)) return;
372
+ let entries;
373
+ try { entries = readdirSync(dir); } catch { return; }
374
+
375
+ for (const entry of entries) {
376
+ if (IGNORE_DIRS.has(entry) || entry.startsWith('.')) continue;
377
+ const fullPath = join(dir, entry);
378
+ try {
379
+ const stat = statSync(fullPath);
380
+ if (stat.isDirectory()) {
381
+ walkFiles(fullPath, callback);
382
+ } else if (stat.isFile()) {
383
+ callback(fullPath);
384
+ }
385
+ } catch { /* skip */ }
386
+ }
387
+ }