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.
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Docs-Diff Validator — Checks alignment between canonical docs and code.
3
+ *
4
+ * Runs as part of `docguard guard` on every invocation.
5
+ * Detects undocumented code artifacts and documented items not found in code.
6
+ * Returns warnings (not errors) since drift is a soft signal.
7
+ */
8
+
9
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
10
+ import { resolve, join, extname, basename } from 'node:path';
11
+
12
+ const IGNORE_DIRS = new Set([
13
+ 'node_modules', '.git', '.next', 'dist', 'build',
14
+ 'coverage', '.cache', '__pycache__', '.venv', 'vendor',
15
+ 'docs-canonical', 'docs-implementation', 'templates',
16
+ ]);
17
+
18
+ const CODE_EXTENSIONS = new Set([
19
+ '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
20
+ '.py', '.java', '.go', '.rs', '.rb', '.php',
21
+ ]);
22
+
23
+ /**
24
+ * Validate doc-code alignment — compares canonical docs vs source code.
25
+ * @returns {{ errors: string[], warnings: string[], passed: number, total: number }}
26
+ */
27
+ export function validateDocsDiff(projectDir, config) {
28
+ const warnings = [];
29
+ let passed = 0;
30
+ let total = 0;
31
+
32
+ const checks = [
33
+ diffTechStack(projectDir),
34
+ diffEnvVars(projectDir),
35
+ diffTests(projectDir),
36
+ ];
37
+
38
+ for (const result of checks) {
39
+ if (!result) continue;
40
+
41
+ total++;
42
+ const undocumented = result.onlyInCode.length;
43
+ const stale = result.onlyInDocs.length;
44
+
45
+ if (undocumented === 0 && stale === 0) {
46
+ passed++;
47
+ } else {
48
+ const parts = [];
49
+ if (undocumented > 0) parts.push(`${undocumented} in code but not documented`);
50
+ if (stale > 0) parts.push(`${stale} documented but not found in code`);
51
+ warnings.push(`${result.title} drift: ${parts.join(', ')}`);
52
+ }
53
+ }
54
+
55
+ return { errors: [], warnings, passed, total };
56
+ }
57
+
58
+ // ── Diff Functions (lightweight versions for validator) ──────────────────
59
+
60
+ function diffTechStack(dir) {
61
+ const archPath = resolve(dir, 'docs-canonical/ARCHITECTURE.md');
62
+ const pkgPath = resolve(dir, 'package.json');
63
+ if (!existsSync(archPath) || !existsSync(pkgPath)) return null;
64
+
65
+ const archContent = readFileSync(archPath, 'utf-8');
66
+ let pkg;
67
+ try { pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); } catch { return null; }
68
+
69
+ const docTech = new Set();
70
+ const techPatterns = ['React', 'Next.js', 'Vue', 'Angular', 'Svelte', 'Express', 'Fastify', 'Hono',
71
+ 'PostgreSQL', 'MySQL', 'MongoDB', 'DynamoDB', 'Redis', 'Prisma', 'Drizzle',
72
+ 'TypeScript', 'Tailwind', 'Docker', 'Terraform'];
73
+
74
+ for (const tech of techPatterns) {
75
+ if (archContent.toLowerCase().includes(tech.toLowerCase())) {
76
+ docTech.add(tech);
77
+ }
78
+ }
79
+
80
+ const codeTech = new Set();
81
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
82
+ const depMap = {
83
+ 'react': 'React', 'next': 'Next.js', 'vue': 'Vue', 'express': 'Express',
84
+ 'fastify': 'Fastify', 'hono': 'Hono', 'prisma': 'Prisma', '@prisma/client': 'Prisma',
85
+ 'drizzle-orm': 'Drizzle', 'typescript': 'TypeScript', 'tailwindcss': 'Tailwind',
86
+ 'redis': 'Redis', 'ioredis': 'Redis', 'pg': 'PostgreSQL', 'mysql2': 'MySQL',
87
+ 'mongoose': 'MongoDB', '@aws-sdk/client-dynamodb': 'DynamoDB',
88
+ };
89
+
90
+ for (const [dep, tech] of Object.entries(depMap)) {
91
+ if (allDeps[dep]) codeTech.add(tech);
92
+ }
93
+
94
+ if (docTech.size === 0 && codeTech.size === 0) return null;
95
+
96
+ return {
97
+ title: 'Tech Stack',
98
+ onlyInDocs: [...docTech].filter(t => !codeTech.has(t)),
99
+ onlyInCode: [...codeTech].filter(t => !docTech.has(t)),
100
+ };
101
+ }
102
+
103
+ function diffEnvVars(dir) {
104
+ const envDocPath = resolve(dir, 'docs-canonical/ENVIRONMENT.md');
105
+ if (!existsSync(envDocPath)) return null;
106
+
107
+ const content = readFileSync(envDocPath, 'utf-8');
108
+ const docVars = new Set();
109
+ const varRegex = /`([A-Z][A-Z0-9_]{2,})`/g;
110
+ let match;
111
+ while ((match = varRegex.exec(content)) !== null) {
112
+ docVars.add(match[1]);
113
+ }
114
+
115
+ const codeVars = new Set();
116
+ const envExamplePath = resolve(dir, '.env.example');
117
+ if (existsSync(envExamplePath)) {
118
+ const envContent = readFileSync(envExamplePath, 'utf-8');
119
+ const envRegex = /^([A-Z][A-Z0-9_]+)\s*=/gm;
120
+ while ((match = envRegex.exec(envContent)) !== null) {
121
+ codeVars.add(match[1]);
122
+ }
123
+ }
124
+
125
+ if (docVars.size === 0 && codeVars.size === 0) return null;
126
+
127
+ return {
128
+ title: 'Environment Variables',
129
+ onlyInDocs: [...docVars].filter(v => !codeVars.has(v)),
130
+ onlyInCode: [...codeVars].filter(v => !docVars.has(v)),
131
+ };
132
+ }
133
+
134
+ function diffTests(dir) {
135
+ const testSpecPath = resolve(dir, 'docs-canonical/TEST-SPEC.md');
136
+ if (!existsSync(testSpecPath)) return null;
137
+
138
+ const content = readFileSync(testSpecPath, 'utf-8');
139
+ const docTests = new Set();
140
+ const testFileRegex = /`([^`]*\.(test|spec)\.[^`]+)`/g;
141
+ let match;
142
+ while ((match = testFileRegex.exec(content)) !== null) {
143
+ docTests.add(match[1]);
144
+ }
145
+
146
+ const codeTests = new Set();
147
+ const testDirs = ['tests', 'test', '__tests__', 'spec', 'e2e'];
148
+ for (const td of testDirs) {
149
+ const testDir = resolve(dir, td);
150
+ if (!existsSync(testDir)) continue;
151
+ const files = getFilesRecursive(testDir);
152
+ for (const f of files) {
153
+ codeTests.add(f.replace(dir + '/', ''));
154
+ }
155
+ }
156
+
157
+ if (docTests.size === 0 && codeTests.size === 0) return null;
158
+
159
+ return {
160
+ title: 'Test Files',
161
+ onlyInDocs: [...docTests].filter(t => !codeTests.has(t)),
162
+ onlyInCode: [...codeTests].filter(t => !docTests.has(t)),
163
+ };
164
+ }
165
+
166
+ function getFilesRecursive(dir) {
167
+ const results = [];
168
+ if (!existsSync(dir)) return results;
169
+ let entries;
170
+ try { entries = readdirSync(dir); } catch { return results; }
171
+
172
+ for (const entry of entries) {
173
+ if (IGNORE_DIRS.has(entry) || entry.startsWith('.')) continue;
174
+ const fullPath = join(dir, entry);
175
+ try {
176
+ const stat = statSync(fullPath);
177
+ if (stat.isDirectory()) {
178
+ results.push(...getFilesRecursive(fullPath));
179
+ } else if (stat.isFile() && CODE_EXTENSIONS.has(extname(fullPath))) {
180
+ results.push(fullPath);
181
+ }
182
+ } catch { /* skip */ }
183
+ }
184
+ return results;
185
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Metadata Sync Validator — Detects stale version references across docs.
3
+ *
4
+ * Cross-checks package.json version against extension.yml and all .md files.
5
+ * Flags outdated version strings (e.g., README references v0.7.2 but package.json is 0.8.0).
6
+ */
7
+
8
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
9
+ import { resolve, join, relative, extname } from 'node:path';
10
+ import { loadIgnorePatterns } from '../shared.mjs';
11
+
12
+ const IGNORE_DIRS = new Set([
13
+ 'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
14
+ '.cache', '__pycache__', '.venv', 'vendor', '.turbo', '.vercel',
15
+ ]);
16
+
17
+ /**
18
+ * Validate version/metadata consistency across project files.
19
+ * @param {string} projectDir - Project root directory
20
+ * @param {object} config - DocGuard config
21
+ * @returns {{ errors: string[], warnings: string[], passed: number, total: number }}
22
+ */
23
+ export function validateMetadataSync(projectDir, config) {
24
+ const warnings = [];
25
+ let passed = 0;
26
+ let total = 0;
27
+
28
+ // ── Get source of truth: package.json version ──
29
+ const pkgPath = resolve(projectDir, 'package.json');
30
+ if (!existsSync(pkgPath)) {
31
+ return { errors: [], warnings, passed: 0, total: 0 };
32
+ }
33
+
34
+ let pkg;
35
+ try { pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); } catch { return { errors: [], warnings, passed: 0, total: 0 }; }
36
+ const currentVersion = pkg.version;
37
+ if (!currentVersion) return { errors: [], warnings, passed: 0, total: 0 };
38
+
39
+ // Parse into components for smart comparison
40
+ const vParts = currentVersion.split('.');
41
+ const major = parseInt(vParts[0], 10);
42
+ const minor = parseInt(vParts[1], 10);
43
+
44
+ // ── Check 1: extension.yml version sync ──
45
+ const extFiles = findExtensionYmls(projectDir);
46
+ for (const extFile of extFiles) {
47
+ total++;
48
+ const relPath = relative(projectDir, extFile);
49
+ try {
50
+ const content = readFileSync(extFile, 'utf-8');
51
+ const versionMatch = content.match(/version:\s*["']?(\d+\.\d+\.\d+)["']?/);
52
+ if (versionMatch) {
53
+ if (versionMatch[1] !== currentVersion) {
54
+ warnings.push(
55
+ `${relPath} has version "${versionMatch[1]}" but package.json is "${currentVersion}"`
56
+ );
57
+ } else {
58
+ passed++;
59
+ }
60
+ }
61
+ } catch { /* skip unreadable */ }
62
+ }
63
+
64
+ // ── Check 2: Version references in markdown files ──
65
+ const isIgnored = loadIgnorePatterns(projectDir);
66
+ const mdFiles = findMarkdownFiles(projectDir);
67
+ // Version patterns to find: v0.7.2, @0.7.2, /v0.7.2/, docguard-cli@0.7.2
68
+ const versionRegex = /(?:v|@|\/v?)(\d+\.\d+\.\d+)/g;
69
+
70
+ for (const mdFile of mdFiles) {
71
+ const relPath = relative(projectDir, mdFile);
72
+ // Skip CHANGELOG.md and DRIFT-LOG.md — these are historical by definition
73
+ const baseName = relPath.toLowerCase();
74
+ if (baseName.includes('changelog') || baseName.includes('drift-log')) continue;
75
+ // Skip files matched by .docguardignore
76
+ if (isIgnored(relPath)) continue;
77
+
78
+ let content;
79
+ try { content = readFileSync(mdFile, 'utf-8'); } catch { continue; }
80
+
81
+ // Only flag version references in actionable contexts:
82
+ // - URLs (download, install, archive links)
83
+ // - version: declarations (YAML-style)
84
+ // - npm install / npx commands
85
+ // - Badge URLs
86
+ // NOT in prose text like "In v0.2.0 we added..." or roadmap discussions
87
+ const actionablePatterns = [
88
+ // URLs with version: /v0.7.2/, /tags/v0.7.2, @0.7.2
89
+ /(?:archive|tags|releases|download)\/v?(\d+\.\d+\.\d+)/g,
90
+ // npm install/npx commands: docguard-cli@0.7.2
91
+ /@(\d+\.\d+\.\d+)/g,
92
+ // YAML-style: version: "0.7.2" or version: 0.7.2
93
+ /version:\s*["']?(\d+\.\d+\.\d+)["']?/g,
94
+ ];
95
+
96
+ for (const pattern of actionablePatterns) {
97
+ pattern.lastIndex = 0;
98
+ let match;
99
+ while ((match = pattern.exec(content)) !== null) {
100
+ const foundVersion = match[1];
101
+ const fParts = foundVersion.split('.');
102
+ const fMajor = parseInt(fParts[0], 10);
103
+ const fMinor = parseInt(fParts[1], 10);
104
+
105
+ // Only flag if same major but older minor (same package, stale ref)
106
+ if (fMajor === major && fMinor < minor && foundVersion !== currentVersion) {
107
+ total++;
108
+ warnings.push(
109
+ `${relPath} references "v${foundVersion}" in an actionable context (URL/install/declaration) but current version is "${currentVersion}"`
110
+ );
111
+ } else if (fMajor === major && fMinor === minor && foundVersion === currentVersion) {
112
+ total++;
113
+ passed++;
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ return { errors: [], warnings, passed, total };
120
+ }
121
+
122
+ // ── Helpers ──────────────────────────────────────────────────────────────────
123
+
124
+ function findExtensionYmls(dir) {
125
+ const results = [];
126
+ const extDir = resolve(dir, 'extensions');
127
+ if (existsSync(extDir)) {
128
+ walkFiles(extDir, (f) => {
129
+ if (f.endsWith('extension.yml') || f.endsWith('extension.yaml')) {
130
+ results.push(f);
131
+ }
132
+ });
133
+ }
134
+ // Also check root
135
+ const rootExt = resolve(dir, 'extension.yml');
136
+ if (existsSync(rootExt)) results.push(rootExt);
137
+ return results;
138
+ }
139
+
140
+ function findMarkdownFiles(dir) {
141
+ const seen = new Set();
142
+ const mdFiles = [];
143
+ const searchDirs = [
144
+ dir,
145
+ resolve(dir, 'docs-canonical'),
146
+ resolve(dir, 'extensions'),
147
+ ];
148
+
149
+ for (const searchDir of searchDirs) {
150
+ if (!existsSync(searchDir)) continue;
151
+ walkFiles(searchDir, (f) => {
152
+ if (f.endsWith('.md') && !seen.has(f)) {
153
+ seen.add(f);
154
+ mdFiles.push(f);
155
+ }
156
+ });
157
+ }
158
+
159
+ return mdFiles;
160
+ }
161
+
162
+ function walkFiles(dir, callback) {
163
+ if (!existsSync(dir)) return;
164
+ let entries;
165
+ try { entries = readdirSync(dir); } catch { return; }
166
+
167
+ for (const entry of entries) {
168
+ if (IGNORE_DIRS.has(entry) || entry.startsWith('.')) continue;
169
+ const fullPath = join(dir, entry);
170
+ try {
171
+ const stat = statSync(fullPath);
172
+ if (stat.isDirectory()) {
173
+ walkFiles(fullPath, callback);
174
+ } else if (stat.isFile()) {
175
+ callback(fullPath);
176
+ }
177
+ } catch { /* skip */ }
178
+ }
179
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Metrics Consistency Validator — Detects stale hardcoded numbers in docs.
3
+ *
4
+ * Scans all .md files for patterns like "N checks", "N validators", "N tests"
5
+ * and compares against actual values from guard results and package.json.
6
+ * Returns warnings for mismatches.
7
+ */
8
+
9
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
10
+ import { resolve, join, relative } from 'node:path';
11
+ import { loadIgnorePatterns } from '../shared.mjs';
12
+
13
+ const IGNORE_DIRS = new Set([
14
+ 'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
15
+ '.cache', '__pycache__', '.venv', 'vendor', '.turbo', '.vercel',
16
+ ]);
17
+
18
+ /**
19
+ * Validate metrics consistency across documentation.
20
+ * @param {string} projectDir - Project root directory
21
+ * @param {object} config - DocGuard config
22
+ * @param {object} [guardResults] - Results from runGuardInternal (optional)
23
+ * @returns {{ errors: string[], warnings: string[], passed: number, total: number }}
24
+ */
25
+ export function validateMetricsConsistency(projectDir, config, guardResults) {
26
+ const warnings = [];
27
+ let passed = 0;
28
+ let total = 0;
29
+
30
+ // ── Collect actual metrics ──
31
+ const actuals = {};
32
+
33
+ // Guard check count (from guard results if available)
34
+ if (guardResults && Array.isArray(guardResults)) {
35
+ const totalChecks = guardResults.reduce((sum, r) => {
36
+ if (r.status === 'skipped') return sum;
37
+ return sum + (r.total || 0);
38
+ }, 0);
39
+ const validatorCount = guardResults.filter(r => r.status !== 'skipped').length;
40
+
41
+ actuals.checks = totalChecks;
42
+ actuals.validators = validatorCount;
43
+ }
44
+
45
+ // Test count — count test files on disk
46
+ const testFiles = findTestFiles(projectDir);
47
+ if (testFiles.length > 0) {
48
+ actuals.tests = testFiles.length;
49
+ }
50
+
51
+ // If no actuals to compare, skip
52
+ if (Object.keys(actuals).length === 0) {
53
+ return { errors: [], warnings, passed: 0, total: 0 };
54
+ }
55
+
56
+ // ── Scan markdown files for hardcoded numbers ──
57
+ const isIgnored = loadIgnorePatterns(projectDir);
58
+ const mdFiles = findMarkdownFiles(projectDir);
59
+ // Patterns must match standalone number references, not ratio-style "8/8 checks"
60
+ const patterns = [
61
+ { key: 'checks', regex: /(?<!\d\/)\b(\d{2,})\s+(?:automated\s+)?checks?\b/gi, label: 'checks' },
62
+ { key: 'validators', regex: /(?<!\d\/)\b(\d{2,})\s+validators?\b/gi, label: 'validators' },
63
+ ];
64
+
65
+ for (const mdFile of mdFiles) {
66
+ const relPath = relative(projectDir, mdFile);
67
+ // Skip changelog (historical numbers are fine by definition)
68
+ if (relPath.toLowerCase().includes('changelog')) continue;
69
+ // Skip files matched by .docguardignore
70
+ if (isIgnored(relPath)) continue;
71
+
72
+ let content;
73
+ try { content = readFileSync(mdFile, 'utf-8'); } catch { continue; }
74
+
75
+ for (const { key, regex, label } of patterns) {
76
+ if (actuals[key] === undefined) continue;
77
+
78
+ regex.lastIndex = 0;
79
+ let match;
80
+ while ((match = regex.exec(content)) !== null) {
81
+ total++;
82
+ const found = parseInt(match[1], 10);
83
+ if (found !== actuals[key] && found > 0) {
84
+ warnings.push(
85
+ `${relPath} says "${found} ${label}" but actual count is ${actuals[key]}. Update the doc or run \`docguard generate --force\``
86
+ );
87
+ } else {
88
+ passed++;
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ return { errors: [], warnings, passed, total };
95
+ }
96
+
97
+ // ── Helpers ──────────────────────────────────────────────────────────────────
98
+
99
+ function findTestFiles(dir) {
100
+ const tests = [];
101
+ const testDirs = ['tests', 'test', '__tests__', 'spec', 'e2e'];
102
+
103
+ // Top-level test dirs
104
+ for (const td of testDirs) {
105
+ const fullDir = resolve(dir, td);
106
+ if (existsSync(fullDir)) {
107
+ walkFiles(fullDir, (f) => {
108
+ if (/\.(test|spec)\.[^.]+$/.test(f)) tests.push(f);
109
+ });
110
+ }
111
+ }
112
+
113
+ // Co-located tests in src/
114
+ const srcDir = resolve(dir, 'src');
115
+ if (existsSync(srcDir)) {
116
+ walkFiles(srcDir, (f) => {
117
+ if (/\.(test|spec)\.[^.]+$/.test(f) || f.includes('__tests__')) {
118
+ if (!tests.includes(f)) tests.push(f);
119
+ }
120
+ });
121
+ }
122
+
123
+ return tests;
124
+ }
125
+
126
+ function findMarkdownFiles(dir) {
127
+ const seen = new Set();
128
+ const mdFiles = [];
129
+ // Check root, docs-canonical, and extensions
130
+ const searchDirs = [
131
+ dir,
132
+ resolve(dir, 'docs-canonical'),
133
+ resolve(dir, 'extensions'),
134
+ ];
135
+
136
+ for (const searchDir of searchDirs) {
137
+ if (!existsSync(searchDir)) continue;
138
+ walkFiles(searchDir, (f) => {
139
+ if (f.endsWith('.md') && !seen.has(f)) {
140
+ seen.add(f);
141
+ mdFiles.push(f);
142
+ }
143
+ });
144
+ }
145
+
146
+ return mdFiles;
147
+ }
148
+
149
+ function walkFiles(dir, callback) {
150
+ if (!existsSync(dir)) return;
151
+ let entries;
152
+ try { entries = readdirSync(dir); } catch { return; }
153
+
154
+ for (const entry of entries) {
155
+ if (IGNORE_DIRS.has(entry) || entry.startsWith('.')) continue;
156
+ const fullPath = join(dir, entry);
157
+ try {
158
+ const stat = statSync(fullPath);
159
+ if (stat.isDirectory()) {
160
+ walkFiles(fullPath, callback);
161
+ } else if (stat.isFile()) {
162
+ callback(fullPath);
163
+ }
164
+ } catch { /* skip */ }
165
+ }
166
+ }
@@ -37,6 +37,7 @@ export function validateTestSpec(projectDir, config) {
37
37
  if (cells.length < 3) continue;
38
38
 
39
39
  const sourceFile = cells[0];
40
+ const testFile = cells[1];
40
41
  const status = cells[cells.length - 1]; // Last column is always status
41
42
 
42
43
  // Skip template/example rows and italic placeholder rows
@@ -56,6 +57,38 @@ export function validateTestSpec(projectDir, config) {
56
57
  results.total++;
57
58
  results.passed++;
58
59
  }
60
+
61
+ // ── File existence checks ───────────────────────────────────────
62
+ // Verify source file still exists (catch stale map entries)
63
+ const cleanSource = sourceFile.replace(/`/g, '').trim();
64
+ if (cleanSource && cleanSource !== '—' && cleanSource !== 'Source File') {
65
+ const sourcePath = resolve(projectDir, cleanSource);
66
+ if (!existsSync(sourcePath)) {
67
+ results.total++;
68
+ results.warnings.push(
69
+ `Source-to-Test Map: source file \`${cleanSource}\` not found on disk — stale entry?`
70
+ );
71
+ } else {
72
+ results.total++;
73
+ results.passed++;
74
+ }
75
+ }
76
+
77
+ // Verify test file exists (catch wrong/stale test references)
78
+ const cleanTest = testFile ? testFile.replace(/`/g, '').trim() : '';
79
+ if (cleanTest && cleanTest !== '—' && cleanTest !== 'Test File' &&
80
+ cleanTest !== 'Unit Test' && !cleanTest.includes('N/A')) {
81
+ const testPath = resolve(projectDir, cleanTest);
82
+ if (!existsSync(testPath)) {
83
+ results.total++;
84
+ results.warnings.push(
85
+ `Source-to-Test Map: test file \`${cleanTest}\` not found — referenced by ${cleanSource}`
86
+ );
87
+ } else {
88
+ results.total++;
89
+ results.passed++;
90
+ }
91
+ }
59
92
  }
60
93
  }
61
94
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.7.3",
3
+ "version": "0.8.2",
4
4
  "description": "The enforcement tool for Canonical-Driven Development (CDD). Audit, generate, and guard your project documentation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -53,3 +53,16 @@
53
53
  |--------|---------------|------|
54
54
  | Health | <!-- e.g. /health returns 200 --> | |
55
55
  | Auth | <!-- e.g. Login flow completes --> | |
56
+
57
+ ## Recommended Test Patterns
58
+
59
+ <!-- DocGuard validates that files listed in the Source-to-Test Map exist on disk.
60
+ Keep the map up to date — stale entries will trigger warnings. -->
61
+
62
+ | Pattern | Description | Priority |
63
+ |---------|-------------|----------|
64
+ | Config-awareness | Test behavior changes per `.docguard.json` / env config | ⚠️ High |
65
+ | Individual functions | Test each module/function directly, not just via CLI | ⚠️ High |
66
+ | Edge cases | Empty inputs, missing files, invalid config | ✅ Medium |
67
+ | Error paths | Verify graceful failure, not just happy path | ✅ Medium |
68
+ | Regression guards | Pin specific bug fixes with dedicated tests | ✅ Medium |
@@ -1,92 +0,0 @@
1
- /**
2
- * Audit Command — Scan project, report what CDD docs exist or are missing
3
- * Now uses the full documentTypes config to show ALL docs with categories.
4
- */
5
-
6
- import { existsSync } from 'node:fs';
7
- import { resolve } from 'node:path';
8
- import { c } from '../docguard.mjs';
9
-
10
- export function runAudit(projectDir, config, flags) {
11
- console.log(`${c.bold}📋 DocGuard Audit — ${config.projectName}${c.reset}`);
12
- console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
13
-
14
- const results = { found: 0, missing: 0, optional: 0, total: 0, details: [] };
15
-
16
- // Use documentTypes from config (all 16 doc types with required/optional)
17
- const docTypes = config.documentTypes || {};
18
-
19
- // Group by category
20
- const categories = {};
21
- for (const [filePath, meta] of Object.entries(docTypes)) {
22
- const cat = meta.category || 'other';
23
- if (!categories[cat]) categories[cat] = [];
24
- categories[cat].push({ filePath, ...meta });
25
- }
26
-
27
- // Category display names and order
28
- const categoryLabels = {
29
- canonical: '📘 Canonical Documentation (Design Intent)',
30
- implementation: '📗 Implementation Documentation (Current State)',
31
- agent: '🤖 Agent Instructions',
32
- tracking: '📑 Change Tracking',
33
- };
34
-
35
- const categoryOrder = ['canonical', 'implementation', 'agent', 'tracking'];
36
-
37
- for (const cat of categoryOrder) {
38
- const docs = categories[cat];
39
- if (!docs || docs.length === 0) continue;
40
-
41
- console.log(`${c.bold} ${categoryLabels[cat] || cat}${c.reset}`);
42
-
43
- for (const doc of docs) {
44
- const fullPath = resolve(projectDir, doc.filePath);
45
- const exists = existsSync(fullPath);
46
-
47
- if (exists) {
48
- results.found++;
49
- results.total++;
50
- results.details.push({ file: doc.filePath, status: 'found', required: doc.required });
51
- console.log(` ${c.green}✅${c.reset} ${doc.filePath} ${c.dim}— ${doc.description}${c.reset}`);
52
- } else if (doc.required) {
53
- results.missing++;
54
- results.total++;
55
- results.details.push({ file: doc.filePath, status: 'missing', required: true });
56
- console.log(` ${c.red}❌${c.reset} ${doc.filePath} ${c.dim}— ${doc.description}${c.reset} ${c.red}(required)${c.reset}`);
57
- } else {
58
- results.optional++;
59
- results.details.push({ file: doc.filePath, status: 'optional', required: false });
60
- if (flags.verbose) {
61
- console.log(` ${c.dim}○ ${doc.filePath} — ${doc.description} (optional)${c.reset}`);
62
- }
63
- }
64
- }
65
- console.log('');
66
- }
67
-
68
- // Score (only required files)
69
- const requiredTotal = results.found + results.missing;
70
- const pct = requiredTotal === 0 ? 100 : Math.round((results.found / requiredTotal) * 100);
71
- const scoreColor = pct >= 80 ? c.green : pct >= 50 ? c.yellow : c.red;
72
-
73
- console.log(`${c.bold} ─────────────────────────────────────${c.reset}`);
74
- console.log(` ${c.bold}Required:${c.reset} ${scoreColor}${results.found - (results.found - requiredTotal + results.missing)}/${requiredTotal} files (${pct}%)${c.reset}`);
75
-
76
- // Show optional count
77
- if (!flags.verbose && results.optional > 0) {
78
- console.log(` ${c.dim}Optional: ${results.optional} not present (use --verbose to see all)${c.reset}`);
79
- }
80
-
81
- if (results.missing > 0) {
82
- console.log(`\n ${c.yellow}💡 Run ${c.cyan}docguard init${c.yellow} to create missing docs from templates.${c.reset}`);
83
- console.log(` ${c.yellow}💡 Run ${c.cyan}docguard generate${c.yellow} to auto-fill docs from your codebase.${c.reset}`);
84
- } else {
85
- console.log(`\n ${c.green}🎉 All required CDD documentation present!${c.reset}`);
86
- console.log(` ${c.dim}Run ${c.cyan}docguard guard${c.dim} to validate content alignment.${c.reset}`);
87
- console.log(` ${c.dim}Run ${c.cyan}docguard score${c.dim} to check your CDD maturity.${c.reset}`);
88
- }
89
-
90
- console.log('');
91
- return results;
92
- }