docguard-cli 0.9.8 → 0.9.10
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/commands/diagnose.mjs +64 -24
- package/cli/commands/fix.mjs +1 -1
- package/cli/commands/guard.mjs +12 -1
- package/cli/commands/hooks.mjs +2 -2
- package/cli/commands/init.mjs +94 -73
- package/cli/commands/score.mjs +58 -16
- package/cli/commands/setup.mjs +60 -30
- package/cli/docguard.mjs +14 -5
- package/cli/ensure-skills.mjs +231 -13
- package/cli/scanners/speckit.mjs +1 -1
- package/cli/shared-ignore.mjs +76 -0
- package/cli/validators/architecture.mjs +21 -6
- package/cli/validators/doc-quality.mjs +1 -1
- package/cli/validators/docs-diff.mjs +79 -12
- package/cli/validators/schema-sync.mjs +1 -1
- package/cli/validators/security.mjs +49 -1
- package/cli/validators/todo-tracking.mjs +41 -15
- package/extensions/spec-kit-docguard/extension.yml +6 -2
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -1
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +9 -4
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +5 -1
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Ignore Utility — Unified file filtering for all validators.
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent glob matching for config ignore arrays:
|
|
5
|
+
* - config.ignore (global — all validators)
|
|
6
|
+
* - config.securityIgnore (security validator only)
|
|
7
|
+
* - config.todoIgnore (TODO-tracking validator only)
|
|
8
|
+
*
|
|
9
|
+
* Supports exact paths AND glob patterns:
|
|
10
|
+
* - "src/foo.ts" → exact match
|
|
11
|
+
* - "packages/cdk/**" → match any file under packages/cdk/
|
|
12
|
+
* - "backend/src/__tests__/**" → match any file under that path
|
|
13
|
+
* - "*.test.ts" → match files ending in .test.ts
|
|
14
|
+
*
|
|
15
|
+
* Zero NPM dependencies — pure Node.js built-ins only.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Convert a glob pattern to a RegExp.
|
|
20
|
+
* Supports: * (any chars except /), ** (any path segments), . (literal dot).
|
|
21
|
+
*
|
|
22
|
+
* @param {string} pattern - Glob pattern
|
|
23
|
+
* @returns {RegExp}
|
|
24
|
+
*/
|
|
25
|
+
function globToRegex(pattern) {
|
|
26
|
+
const escaped = pattern
|
|
27
|
+
.replace(/\./g, '\\.')
|
|
28
|
+
.replace(/\*\*/g, '§§') // temp placeholder for **
|
|
29
|
+
.replace(/\*/g, '[^/]*')
|
|
30
|
+
.replace(/§§/g, '.*');
|
|
31
|
+
// Match if the relative path:
|
|
32
|
+
// - equals the pattern exactly
|
|
33
|
+
// - ends with /pattern
|
|
34
|
+
// - starts with pattern/
|
|
35
|
+
// - contains /pattern/
|
|
36
|
+
return new RegExp(`^${escaped}$|/${escaped}$|^${escaped}/|/${escaped}/`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build a filter function from an array of glob patterns.
|
|
41
|
+
* Returns a function that returns true if a relative path should be SKIPPED.
|
|
42
|
+
*
|
|
43
|
+
* @param {string[]} patterns - Glob patterns (from config.ignore, config.securityIgnore, etc.)
|
|
44
|
+
* @returns {(relPath: string) => boolean} - true if file should be ignored
|
|
45
|
+
*/
|
|
46
|
+
export function buildIgnoreFilter(patterns = []) {
|
|
47
|
+
if (!patterns || patterns.length === 0) return () => false;
|
|
48
|
+
|
|
49
|
+
const regexes = patterns.map(p => globToRegex(p));
|
|
50
|
+
return (relPath) => regexes.some(regex => regex.test(relPath));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a relative path should be ignored by BOTH
|
|
55
|
+
* global ignore + validator-specific ignore.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} relPath - Relative file path (e.g., "backend/src/__tests__/foo.test.ts")
|
|
58
|
+
* @param {object} config - DocGuard config object
|
|
59
|
+
* @param {string} [validatorKey] - Optional validator-specific key (e.g., 'securityIgnore', 'todoIgnore')
|
|
60
|
+
* @returns {boolean} - true if file should be skipped
|
|
61
|
+
*/
|
|
62
|
+
export function shouldIgnore(relPath, config, validatorKey) {
|
|
63
|
+
// Check global ignore
|
|
64
|
+
if (config.ignore && config.ignore.length > 0) {
|
|
65
|
+
const globalFilter = buildIgnoreFilter(config.ignore);
|
|
66
|
+
if (globalFilter(relPath)) return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check validator-specific ignore
|
|
70
|
+
if (validatorKey && config[validatorKey] && config[validatorKey].length > 0) {
|
|
71
|
+
const validatorFilter = buildIgnoreFilter(config[validatorKey]);
|
|
72
|
+
if (validatorFilter(relPath)) return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
* - Circular dependencies (A → B → A)
|
|
11
11
|
* - Layer boundary violations (routes importing from routes, etc.)
|
|
12
12
|
* - Orphan modules (code files with 0 inbound imports)
|
|
13
|
+
*
|
|
14
|
+
* Respects config.ignore (global) for file filtering.
|
|
15
|
+
* Uses shared-ignore.mjs for consistent filtering (Constitution IV, v1.1.0).
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
16
19
|
import { resolve, join, extname, relative, dirname, basename } from 'node:path';
|
|
20
|
+
import { shouldIgnore } from '../shared-ignore.mjs';
|
|
17
21
|
|
|
18
22
|
const IGNORE_DIRS = new Set([
|
|
19
23
|
'node_modules', '.git', '.next', 'dist', 'build',
|
|
@@ -33,7 +37,7 @@ export function validateArchitecture(projectDir, config) {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
// ── 2. Auto-detect import graph ──
|
|
36
|
-
const importGraph = buildImportGraph(projectDir);
|
|
40
|
+
const importGraph = buildImportGraph(projectDir, config);
|
|
37
41
|
if (importGraph.files.length === 0) return results;
|
|
38
42
|
|
|
39
43
|
// ── 3. Detect circular dependencies ──
|
|
@@ -84,7 +88,7 @@ function validateConfigLayers(projectDir, config, layers, results) {
|
|
|
84
88
|
const layerDir = resolve(projectDir, dir);
|
|
85
89
|
if (!existsSync(layerDir)) continue;
|
|
86
90
|
|
|
87
|
-
const files = getFilesRecursive(layerDir);
|
|
91
|
+
const files = getFilesRecursive(layerDir, config, projectDir);
|
|
88
92
|
for (const file of files) {
|
|
89
93
|
if (!CODE_EXTENSIONS.has(extname(file))) continue;
|
|
90
94
|
|
|
@@ -110,14 +114,18 @@ function validateConfigLayers(projectDir, config, layers, results) {
|
|
|
110
114
|
|
|
111
115
|
// ── Import Graph Builder ────────────────────────────────────────────────────
|
|
112
116
|
|
|
113
|
-
function buildImportGraph(projectDir) {
|
|
117
|
+
function buildImportGraph(projectDir, config) {
|
|
114
118
|
const graph = { files: [], edges: [], fileMap: new Map() };
|
|
115
119
|
|
|
116
|
-
const allFiles = getFilesRecursive(projectDir);
|
|
120
|
+
const allFiles = getFilesRecursive(projectDir, config, projectDir);
|
|
117
121
|
const codeFiles = allFiles.filter(f => CODE_EXTENSIONS.has(extname(f)));
|
|
118
122
|
|
|
119
123
|
for (const file of codeFiles) {
|
|
120
124
|
const relPath = relative(projectDir, file);
|
|
125
|
+
|
|
126
|
+
// Skip files in ignored directories (config.ignore)
|
|
127
|
+
if (config && shouldIgnore(relPath, config)) continue;
|
|
128
|
+
|
|
121
129
|
graph.files.push(relPath);
|
|
122
130
|
|
|
123
131
|
try {
|
|
@@ -355,7 +363,7 @@ function getFileLayer(filePath, layerDirMap) {
|
|
|
355
363
|
|
|
356
364
|
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
357
365
|
|
|
358
|
-
function getFilesRecursive(dir) {
|
|
366
|
+
function getFilesRecursive(dir, config, projectDir) {
|
|
359
367
|
const results = [];
|
|
360
368
|
if (!existsSync(dir)) return results;
|
|
361
369
|
|
|
@@ -366,11 +374,18 @@ function getFilesRecursive(dir) {
|
|
|
366
374
|
|
|
367
375
|
for (const entry of entries) {
|
|
368
376
|
if (IGNORE_DIRS.has(entry) || entry.startsWith('.')) continue;
|
|
377
|
+
|
|
378
|
+
// Check config.ignore for this directory
|
|
379
|
+
if (config && projectDir) {
|
|
380
|
+
const relPath = relative(projectDir, join(dir, entry));
|
|
381
|
+
if (shouldIgnore(relPath, config)) continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
369
384
|
const fullPath = join(dir, entry);
|
|
370
385
|
try {
|
|
371
386
|
const stat = statSync(fullPath);
|
|
372
387
|
if (stat.isDirectory()) {
|
|
373
|
-
results.push(...getFilesRecursive(fullPath));
|
|
388
|
+
results.push(...getFilesRecursive(fullPath, config, projectDir));
|
|
374
389
|
} else {
|
|
375
390
|
results.push(fullPath);
|
|
376
391
|
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Optional: If `understanding` CLI is installed, runs a full 31-metric deep scan.
|
|
20
20
|
*
|
|
21
|
-
* Zero dependencies — pure Node.js built-ins only.
|
|
21
|
+
* Zero NPM runtime dependencies — pure Node.js built-ins only.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
* Runs as part of `docguard guard` on every invocation.
|
|
5
5
|
* Detects undocumented code artifacts and documented items not found in code.
|
|
6
6
|
* Returns warnings (not errors) since drift is a soft signal.
|
|
7
|
+
*
|
|
8
|
+
* Respects config.ignore and config.testPatterns for test file discovery.
|
|
9
|
+
* Uses shared-ignore.mjs for consistent filtering (Constitution IV, v1.1.0).
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
10
|
-
import { resolve, join, extname, basename } from 'node:path';
|
|
13
|
+
import { resolve, join, extname, basename, relative } from 'node:path';
|
|
14
|
+
import { shouldIgnore, buildIgnoreFilter } from '../shared-ignore.mjs';
|
|
11
15
|
|
|
12
16
|
const IGNORE_DIRS = new Set([
|
|
13
17
|
'node_modules', '.git', '.next', 'dist', 'build',
|
|
@@ -32,7 +36,7 @@ export function validateDocsDiff(projectDir, config) {
|
|
|
32
36
|
const checks = [
|
|
33
37
|
diffTechStack(projectDir),
|
|
34
38
|
diffEnvVars(projectDir),
|
|
35
|
-
diffTests(projectDir),
|
|
39
|
+
diffTests(projectDir, config),
|
|
36
40
|
];
|
|
37
41
|
|
|
38
42
|
for (const result of checks) {
|
|
@@ -131,7 +135,13 @@ function diffEnvVars(dir) {
|
|
|
131
135
|
};
|
|
132
136
|
}
|
|
133
137
|
|
|
134
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Diff test files between TEST-SPEC.md and actual code.
|
|
140
|
+
* Uses config.testPatterns if available, otherwise falls back to
|
|
141
|
+
* scanning standard test directories.
|
|
142
|
+
* Always ignores node_modules via shared ignore filter.
|
|
143
|
+
*/
|
|
144
|
+
function diffTests(dir, config) {
|
|
135
145
|
const testSpecPath = resolve(dir, 'docs-canonical/TEST-SPEC.md');
|
|
136
146
|
if (!existsSync(testSpecPath)) return null;
|
|
137
147
|
|
|
@@ -144,13 +154,30 @@ function diffTests(dir) {
|
|
|
144
154
|
}
|
|
145
155
|
|
|
146
156
|
const codeTests = new Set();
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
|
|
158
|
+
// Use testPatterns from config if available
|
|
159
|
+
const testPatterns = config?.testPatterns || [];
|
|
160
|
+
if (testPatterns.length > 0) {
|
|
161
|
+
// Use configured patterns to find test files
|
|
162
|
+
const patternFilter = buildIgnoreFilter(testPatterns.map(p => {
|
|
163
|
+
// Invert the pattern: we WANT files matching these patterns
|
|
164
|
+
return p;
|
|
165
|
+
}));
|
|
166
|
+
// Walk the project and collect matching test files
|
|
167
|
+
const allTestFiles = getTestFilesFromPatterns(dir, testPatterns, config);
|
|
168
|
+
for (const f of allTestFiles) {
|
|
169
|
+
codeTests.add(f);
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
// Fall back to standard test directories
|
|
173
|
+
const testDirs = ['tests', 'test', '__tests__', 'spec', 'e2e'];
|
|
174
|
+
for (const td of testDirs) {
|
|
175
|
+
const testDir = resolve(dir, td);
|
|
176
|
+
if (!existsSync(testDir)) continue;
|
|
177
|
+
const files = getFilesRecursive(testDir, config);
|
|
178
|
+
for (const f of files) {
|
|
179
|
+
codeTests.add(f.replace(dir + '/', ''));
|
|
180
|
+
}
|
|
154
181
|
}
|
|
155
182
|
}
|
|
156
183
|
|
|
@@ -163,7 +190,47 @@ function diffTests(dir) {
|
|
|
163
190
|
};
|
|
164
191
|
}
|
|
165
192
|
|
|
166
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Find test files matching configured testPatterns.
|
|
195
|
+
* Walks the project tree, skipping node_modules and ignored dirs.
|
|
196
|
+
*/
|
|
197
|
+
function getTestFilesFromPatterns(dir, patterns, config) {
|
|
198
|
+
const results = [];
|
|
199
|
+
const testFileRegex = /\.(test|spec)\.(mjs|cjs|[jt]sx?)$/;
|
|
200
|
+
|
|
201
|
+
function walk(currentDir) {
|
|
202
|
+
let entries;
|
|
203
|
+
try { entries = readdirSync(currentDir); } catch { return; }
|
|
204
|
+
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
if (IGNORE_DIRS.has(entry) || entry.startsWith('.')) continue;
|
|
207
|
+
const fullPath = join(currentDir, entry);
|
|
208
|
+
try {
|
|
209
|
+
const stat = statSync(fullPath);
|
|
210
|
+
if (stat.isDirectory()) {
|
|
211
|
+
walk(fullPath);
|
|
212
|
+
} else if (stat.isFile()) {
|
|
213
|
+
const relPath = relative(dir, fullPath);
|
|
214
|
+
// Skip files in ignored paths
|
|
215
|
+
if (config && shouldIgnore(relPath, config)) continue;
|
|
216
|
+
// Check if it matches test file naming patterns
|
|
217
|
+
if (testFileRegex.test(entry) || /__(tests|test)__/.test(relPath)) {
|
|
218
|
+
// Check if it matches any of the configured test patterns
|
|
219
|
+
const patternFilter = buildIgnoreFilter(patterns);
|
|
220
|
+
if (patternFilter(relPath)) {
|
|
221
|
+
results.push(relPath);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch { /* skip */ }
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
walk(dir);
|
|
230
|
+
return results;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getFilesRecursive(dir, config) {
|
|
167
234
|
const results = [];
|
|
168
235
|
if (!existsSync(dir)) return results;
|
|
169
236
|
let entries;
|
|
@@ -175,7 +242,7 @@ function getFilesRecursive(dir) {
|
|
|
175
242
|
try {
|
|
176
243
|
const stat = statSync(fullPath);
|
|
177
244
|
if (stat.isDirectory()) {
|
|
178
|
-
results.push(...getFilesRecursive(fullPath));
|
|
245
|
+
results.push(...getFilesRecursive(fullPath, config));
|
|
179
246
|
} else if (stat.isFile() && CODE_EXTENSIONS.has(extname(fullPath))) {
|
|
180
247
|
results.push(fullPath);
|
|
181
248
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Supported: Prisma, Drizzle, Sequelize, TypeORM, Knex, Django, Rails
|
|
8
8
|
*
|
|
9
|
-
* Zero dependencies — pure Node.js built-ins only.
|
|
9
|
+
* Zero NPM runtime dependencies — pure Node.js built-ins only.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Security Validator — Basic checks for secrets in code
|
|
3
|
+
*
|
|
4
|
+
* Respects config.securityIgnore (glob patterns) and config.ignore (global).
|
|
5
|
+
* Uses shared-ignore.mjs for consistent filtering (Constitution IV, v1.1.0).
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
6
9
|
import { resolve, join, extname } from 'node:path';
|
|
10
|
+
import { shouldIgnore } from '../shared-ignore.mjs';
|
|
7
11
|
|
|
8
12
|
const CODE_EXTENSIONS = new Set([
|
|
9
13
|
'.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
|
|
@@ -26,6 +30,30 @@ const SECRET_PATTERNS = [
|
|
|
26
30
|
{ pattern: /(?:sk-|sk_live_|sk_test_)[a-zA-Z0-9]{20,}/g, label: 'API secret key (Stripe/OpenAI pattern)' },
|
|
27
31
|
];
|
|
28
32
|
|
|
33
|
+
// Known-safe placeholder/example values that should never be flagged
|
|
34
|
+
const SAFE_PATTERNS = [
|
|
35
|
+
/EXAMPLE/i, // AWS docs example keys contain "EXAMPLE"
|
|
36
|
+
/placeholder\s*=\s*["']/i, // HTML placeholder attributes
|
|
37
|
+
/example\s*:/i, // OpenAPI example: blocks
|
|
38
|
+
/['"]password123['"]/, // Common test fixture value
|
|
39
|
+
/\/\/\s*example/i, // Code comments with "example"
|
|
40
|
+
/<!--.*-->/, // HTML comments
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a match line is a known-safe placeholder/example.
|
|
45
|
+
* @param {string} line - The full source line containing the match
|
|
46
|
+
* @param {string} matchStr - The matched string
|
|
47
|
+
* @returns {boolean} - true if this is a safe/placeholder value
|
|
48
|
+
*/
|
|
49
|
+
function isSafePlaceholder(line, matchStr) {
|
|
50
|
+
// Check if the matched string itself contains "EXAMPLE"
|
|
51
|
+
if (/EXAMPLE/i.test(matchStr)) return true;
|
|
52
|
+
|
|
53
|
+
// Check if the source line matches any safe pattern
|
|
54
|
+
return SAFE_PATTERNS.some(p => p.test(line));
|
|
55
|
+
}
|
|
56
|
+
|
|
29
57
|
export function validateSecurity(projectDir, config) {
|
|
30
58
|
const results = { name: 'security', errors: [], warnings: [], passed: 0, total: 0 };
|
|
31
59
|
|
|
@@ -40,13 +68,33 @@ export function validateSecurity(projectDir, config) {
|
|
|
40
68
|
// Skip .env.example — it should have placeholder values
|
|
41
69
|
if (filePath.endsWith('.env.example')) return;
|
|
42
70
|
|
|
43
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
44
71
|
const relPath = filePath.replace(projectDir + '/', '');
|
|
45
72
|
|
|
73
|
+
// Apply config ignore patterns (securityIgnore + global ignore)
|
|
74
|
+
if (shouldIgnore(relPath, config, 'securityIgnore')) return;
|
|
75
|
+
|
|
76
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
77
|
+
const lines = content.split('\n');
|
|
78
|
+
|
|
46
79
|
for (const { pattern, label } of SECRET_PATTERNS) {
|
|
47
80
|
pattern.lastIndex = 0;
|
|
48
81
|
const match = pattern.exec(content);
|
|
49
82
|
if (match) {
|
|
83
|
+
// Find the line containing this match for context-aware filtering
|
|
84
|
+
const matchPos = match.index;
|
|
85
|
+
let charCount = 0;
|
|
86
|
+
let matchLine = '';
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
charCount += line.length + 1; // +1 for newline
|
|
89
|
+
if (charCount > matchPos) {
|
|
90
|
+
matchLine = line;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Skip known-safe placeholder/example values
|
|
96
|
+
if (isSafePlaceholder(matchLine, match[0])) continue;
|
|
97
|
+
|
|
50
98
|
findings.push({ file: relPath, label, match: match[0].substring(0, 30) + '...' });
|
|
51
99
|
}
|
|
52
100
|
}
|
|
@@ -6,14 +6,18 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Also detects skipped tests without explanation.
|
|
8
8
|
*
|
|
9
|
+
* Respects config.todoIgnore (glob patterns) and config.ignore (global).
|
|
10
|
+
* Uses shared-ignore.mjs for consistent filtering (Constitution IV, v1.1.0).
|
|
11
|
+
*
|
|
9
12
|
* Inspired by spec-kit-cleanup (github.com/dsrednicki/spec-kit-cleanup)
|
|
10
13
|
* which uses tiered issue classification for code hygiene.
|
|
11
14
|
*
|
|
12
|
-
* Zero dependencies — pure Node.js built-ins only.
|
|
15
|
+
* Zero NPM runtime dependencies — pure Node.js built-ins only.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
16
19
|
import { resolve, join, relative, extname } from 'node:path';
|
|
20
|
+
import { shouldIgnore } from '../shared-ignore.mjs';
|
|
17
21
|
|
|
18
22
|
const IGNORE_DIRS = new Set([
|
|
19
23
|
'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
|
|
@@ -78,14 +82,14 @@ export function validateTodoTracking(projectDir, config) {
|
|
|
78
82
|
/**
|
|
79
83
|
* Scan test files for skip/todo patterns without adjacent explanation comments.
|
|
80
84
|
*/
|
|
81
|
-
function checkSkippedTests(projectDir) {
|
|
85
|
+
function checkSkippedTests(projectDir, config) {
|
|
82
86
|
const errors = [];
|
|
83
87
|
const warnings = [];
|
|
84
88
|
let passed = 0;
|
|
85
89
|
let total = 0;
|
|
86
90
|
|
|
87
91
|
const testFiles = [];
|
|
88
|
-
findTestFiles(projectDir, projectDir, testFiles);
|
|
92
|
+
findTestFiles(projectDir, projectDir, testFiles, config);
|
|
89
93
|
|
|
90
94
|
if (testFiles.length === 0) return { errors, warnings, passed, total };
|
|
91
95
|
|
|
@@ -161,7 +165,7 @@ function checkUntrackedTodos(projectDir, config) {
|
|
|
161
165
|
|
|
162
166
|
// Collect all TODO/FIXME items from source
|
|
163
167
|
const todos = [];
|
|
164
|
-
findTodos(projectDir, projectDir, todos);
|
|
168
|
+
findTodos(projectDir, projectDir, todos, config);
|
|
165
169
|
|
|
166
170
|
if (todos.length === 0) {
|
|
167
171
|
// No TODOs found — that's clean code
|
|
@@ -177,11 +181,26 @@ function checkUntrackedTodos(projectDir, config) {
|
|
|
177
181
|
let untrackedCount = 0;
|
|
178
182
|
|
|
179
183
|
for (const todo of todos) {
|
|
180
|
-
// Check if the TODO
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
doc.content
|
|
184
|
-
|
|
184
|
+
// Check if the TODO is tracked in documentation
|
|
185
|
+
// Improved matching: check full text AND file location context
|
|
186
|
+
const isTracked = trackingContent.some(doc => {
|
|
187
|
+
const content = doc.content;
|
|
188
|
+
const contentLower = content.toLowerCase();
|
|
189
|
+
const todoTextLower = todo.text.toLowerCase().trim();
|
|
190
|
+
|
|
191
|
+
// Match 1: Full TODO text appears in the doc (at least 20 chars or full text)
|
|
192
|
+
const searchText = todoTextLower.length > 20
|
|
193
|
+
? todoTextLower.substring(0, 40)
|
|
194
|
+
: todoTextLower;
|
|
195
|
+
const hasText = contentLower.includes(searchText);
|
|
196
|
+
|
|
197
|
+
// Match 2: File location appears nearby in the doc
|
|
198
|
+
const hasLocation = content.includes(todo.file) ||
|
|
199
|
+
content.includes(`${todo.file}:${todo.line}`);
|
|
200
|
+
|
|
201
|
+
// Either the full text matches, or the file location is referenced with partial text
|
|
202
|
+
return (hasText && hasLocation) || (hasText && todoTextLower.length > 30);
|
|
203
|
+
});
|
|
185
204
|
|
|
186
205
|
if (!isTracked) {
|
|
187
206
|
untrackedCount++;
|
|
@@ -231,7 +250,7 @@ function loadTrackingDocs(projectDir, config) {
|
|
|
231
250
|
|
|
232
251
|
// ──── File Scanners ────────────────────────────────────────────────────────
|
|
233
252
|
|
|
234
|
-
function findTestFiles(rootDir, dir, files) {
|
|
253
|
+
function findTestFiles(rootDir, dir, files, config) {
|
|
235
254
|
let entries;
|
|
236
255
|
try { entries = readdirSync(dir); } catch { return; }
|
|
237
256
|
|
|
@@ -244,7 +263,7 @@ function findTestFiles(rootDir, dir, files) {
|
|
|
244
263
|
try { stat = statSync(full); } catch { continue; }
|
|
245
264
|
|
|
246
265
|
if (stat.isDirectory()) {
|
|
247
|
-
findTestFiles(rootDir, full, files);
|
|
266
|
+
findTestFiles(rootDir, full, files, config);
|
|
248
267
|
} else {
|
|
249
268
|
const ext = extname(entry).toLowerCase();
|
|
250
269
|
if (!TEST_EXTENSIONS.has(ext)) continue;
|
|
@@ -252,13 +271,16 @@ function findTestFiles(rootDir, dir, files) {
|
|
|
252
271
|
// Match test file patterns
|
|
253
272
|
if (/\.(test|spec)\.(mjs|cjs|[jt]sx?)$/.test(entry) ||
|
|
254
273
|
/__(tests|test)__/.test(relative(rootDir, full))) {
|
|
255
|
-
|
|
274
|
+
const relPath = relative(rootDir, full);
|
|
275
|
+
// Apply config ignore patterns (todoIgnore + global ignore)
|
|
276
|
+
if (config && shouldIgnore(relPath, config, 'todoIgnore')) continue;
|
|
277
|
+
files.push(relPath);
|
|
256
278
|
}
|
|
257
279
|
}
|
|
258
280
|
}
|
|
259
281
|
}
|
|
260
282
|
|
|
261
|
-
function findTodos(rootDir, dir, todos) {
|
|
283
|
+
function findTodos(rootDir, dir, todos, config) {
|
|
262
284
|
let entries;
|
|
263
285
|
try { entries = readdirSync(dir); } catch { return; }
|
|
264
286
|
|
|
@@ -271,16 +293,20 @@ function findTodos(rootDir, dir, todos) {
|
|
|
271
293
|
try { stat = statSync(full); } catch { continue; }
|
|
272
294
|
|
|
273
295
|
if (stat.isDirectory()) {
|
|
274
|
-
findTodos(rootDir, full, todos);
|
|
296
|
+
findTodos(rootDir, full, todos, config);
|
|
275
297
|
} else {
|
|
276
298
|
const ext = extname(entry).toLowerCase();
|
|
277
299
|
if (!SOURCE_EXTENSIONS.has(ext)) continue;
|
|
278
300
|
|
|
301
|
+
const relPath = relative(rootDir, full);
|
|
302
|
+
|
|
303
|
+
// Apply config ignore patterns (todoIgnore + global ignore)
|
|
304
|
+
if (config && shouldIgnore(relPath, config, 'todoIgnore')) continue;
|
|
305
|
+
|
|
279
306
|
let content;
|
|
280
307
|
try { content = readFileSync(full, 'utf-8'); } catch { continue; }
|
|
281
308
|
|
|
282
309
|
const lines = content.split('\n');
|
|
283
|
-
const relPath = relative(rootDir, full);
|
|
284
310
|
|
|
285
311
|
for (let i = 0; i < lines.length; i++) {
|
|
286
312
|
if (TODO_PATTERN.test(lines[i])) {
|
|
@@ -3,8 +3,8 @@ schema_version: "1.0"
|
|
|
3
3
|
extension:
|
|
4
4
|
id: "docguard"
|
|
5
5
|
name: "DocGuard — CDD Enforcement"
|
|
6
|
-
version: "0.9.
|
|
7
|
-
description: "Canonical-Driven Development enforcement
|
|
6
|
+
version: "0.9.9"
|
|
7
|
+
description: "Canonical-Driven Development enforcement as a true spec-kit extension. LLM-first design with 19 automated validators, 4 AI behavior skills, spec-kit skill chaining, and workflow hooks. Zero NPM runtime dependencies."
|
|
8
8
|
author: "Ricardo Accioly"
|
|
9
9
|
repository: "https://github.com/raccioly/docguard"
|
|
10
10
|
license: "MIT"
|
|
@@ -12,12 +12,16 @@ extension:
|
|
|
12
12
|
|
|
13
13
|
requires:
|
|
14
14
|
speckit_version: ">=0.1.0"
|
|
15
|
+
framework: "spec-kit" # DocGuard builds on spec-kit conventions (.specify/, skills, constitution)
|
|
15
16
|
tools:
|
|
16
17
|
- name: "node"
|
|
17
18
|
required: true
|
|
18
19
|
version: ">=18.0.0"
|
|
19
20
|
- name: "npx"
|
|
20
21
|
required: true
|
|
22
|
+
- name: "specify"
|
|
23
|
+
required: false
|
|
24
|
+
description: "Spec Kit CLI — enables auto-initialization of SDD workflow during docguard init"
|
|
21
25
|
|
|
22
26
|
provides:
|
|
23
27
|
commands:
|
|
@@ -6,9 +6,10 @@ description: AI-driven documentation repair with structured research workflow, t
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.9.
|
|
9
|
+
version: 0.9.9
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-fix
|
|
11
11
|
---
|
|
12
|
+
<!-- docguard:version: 0.9.9 -->
|
|
12
13
|
|
|
13
14
|
# DocGuard Fix Skill
|
|
14
15
|
|
|
@@ -7,9 +7,10 @@ description: Run DocGuard guard validation against Canonical-Driven Development
|
|
|
7
7
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
8
8
|
metadata:
|
|
9
9
|
author: docguard
|
|
10
|
-
version: 0.9.
|
|
10
|
+
version: 0.9.9
|
|
11
11
|
source: extensions/spec-kit-docguard/skills/docguard-guard
|
|
12
12
|
---
|
|
13
|
+
<!-- docguard:version: 0.9.9 -->
|
|
13
14
|
|
|
14
15
|
# DocGuard Guard Skill
|
|
15
16
|
|
|
@@ -155,12 +156,16 @@ Present the user with options:
|
|
|
155
156
|
- **Track progress** — if user runs guard multiple times, compare before/after
|
|
156
157
|
- If user provides `$ARGUMENTS` like "just structure" or "only security", filter report to those validators
|
|
157
158
|
|
|
158
|
-
## Integration with Spec Kit
|
|
159
|
+
## Integration with Spec Kit (Extension-First)
|
|
159
160
|
|
|
160
|
-
|
|
161
|
+
DocGuard is a spec-kit extension. When this project has a `.specify/` directory:
|
|
162
|
+
- Read `.specify/memory/constitution.md` for project principles that constrain documentation
|
|
161
163
|
- Include Spec-Kit validator results in the triage
|
|
162
164
|
- Cross-reference spec quality issues with `specs/*/spec.md` file paths
|
|
163
|
-
-
|
|
165
|
+
- When specification issues found → suggest `/speckit.specify` or `/speckit.clarify`
|
|
166
|
+
- When architecture gaps found → suggest `/speckit.plan`
|
|
167
|
+
- When cross-artifact inconsistencies exceed 3 → suggest `/speckit.analyze`
|
|
168
|
+
- When no constitution exists → suggest `/speckit.constitution` as first step
|
|
164
169
|
|
|
165
170
|
## Context
|
|
166
171
|
|
|
@@ -6,9 +6,10 @@ description: Cross-document consistency analysis and quality assessment. Perform
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.9.
|
|
9
|
+
version: 0.9.9
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-review
|
|
11
11
|
---
|
|
12
|
+
<!-- docguard:version: 0.9.9 -->
|
|
12
13
|
|
|
13
14
|
# DocGuard Review Skill
|
|
14
15
|
|
|
@@ -163,7 +164,10 @@ Output a structured markdown report (do NOT write to disk):
|
|
|
163
164
|
|
|
164
165
|
Based on findings:
|
|
165
166
|
- **If CRITICAL issues**: "Run `/docguard.fix --doc [name]` to resolve blocking issues"
|
|
167
|
+
- **If spec-related gaps**: "Run `/speckit.specify` to update specifications" or "/speckit.clarify to resolve ambiguities"
|
|
168
|
+
- **If architecture drift**: "Run `/speckit.plan` to realign implementation plan with codebase"
|
|
166
169
|
- **If only LOW/MEDIUM**: "Documentation is healthy. Consider `/docguard.fix` for polish"
|
|
170
|
+
- **If constitution missing**: "Run `/speckit.constitution` to establish project principles"
|
|
167
171
|
- **If all clean**: "Documentation is excellent. No action needed."
|
|
168
172
|
|
|
169
173
|
Ask: "Would you like me to fix the top N issues? (I'll show you what I plan to change before applying)"
|
|
@@ -6,9 +6,10 @@ description: CDD maturity assessment with category-aware improvement roadmap. Ru
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.9.
|
|
9
|
+
version: 0.9.9
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-score
|
|
11
11
|
---
|
|
12
|
+
<!-- docguard:version: 0.9.9 -->
|
|
12
13
|
|
|
13
14
|
# DocGuard Score Skill
|
|
14
15
|
|
package/package.json
CHANGED