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/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,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
|
+
}
|