docguard-cli 0.8.2 → 0.9.1
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 +13 -6
- package/cli/commands/guard.mjs +8 -0
- package/cli/commands/llms.mjs +159 -0
- package/cli/commands/score.mjs +259 -11
- package/cli/docguard.mjs +6 -0
- package/cli/scanners/speckit.mjs +234 -0
- package/cli/shared.mjs +35 -0
- package/cli/validators/doc-quality.mjs +629 -0
- package/cli/validators/docs-sync.mjs +53 -0
- package/cli/validators/schema-sync.mjs +219 -0
- package/cli/validators/test-spec.mjs +51 -4
- package/cli/validators/todo-tracking.mjs +295 -0
- package/cli/validators/traceability.mjs +194 -8
- package/package.json +1 -1
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Traceability Validator — Checks that canonical docs are linked to source code
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* 1. Source Traceability: Canonical docs reference actual source files
|
|
6
|
+
* 2. Requirement Traceability (V-Model): Requirement IDs in docs trace to tests
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* Requirement traceability is opt-in by convention — if no requirement IDs are
|
|
9
|
+
* found (REQ-001, FR-001, etc.), the check silently passes. Once you add IDs,
|
|
10
|
+
* DocGuard automatically enforces traceability.
|
|
11
|
+
*
|
|
12
|
+
* Inspired by ISO/IEC/IEEE 29119, IEEE 1016, and V-Model methodology.
|
|
13
|
+
* V-Model concepts informed by spec-kit-v-model (github.com/leocamello/spec-kit-v-model).
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
12
|
-
import { resolve, join, relative, basename } from 'node:path';
|
|
17
|
+
import { resolve, join, relative, basename, extname } from 'node:path';
|
|
13
18
|
|
|
14
19
|
const IGNORE_DIRS = new Set([
|
|
15
20
|
'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
|
|
@@ -65,10 +70,25 @@ const TRACE_MAP = {
|
|
|
65
70
|
},
|
|
66
71
|
};
|
|
67
72
|
|
|
73
|
+
// ──── Default requirement ID patterns ────
|
|
74
|
+
// Users can override via config.traceability.requirementPattern
|
|
75
|
+
const DEFAULT_REQ_PATTERNS = [
|
|
76
|
+
/\b(REQ)-(\d{2,4})\b/g,
|
|
77
|
+
/\b(FR)-(\d{2,4})\b/g,
|
|
78
|
+
/\b(NFR)-(\d{2,4})\b/g,
|
|
79
|
+
/\b(US)-(\d{2,4})\b/g,
|
|
80
|
+
/\b(STORY)-(\d{2,4})\b/g,
|
|
81
|
+
/\b(AC)-(\d{2,4})\b/g,
|
|
82
|
+
/\b(UC)-(\d{2,4})\b/g,
|
|
83
|
+
/\b(SYS)-(\d{2,4})\b/g,
|
|
84
|
+
/\b(ARCH)-(\d{2,4})\b/g,
|
|
85
|
+
/\b(MOD)-(\d{2,4})\b/g,
|
|
86
|
+
];
|
|
87
|
+
|
|
68
88
|
/**
|
|
69
|
-
* Validate traceability — ensures canonical docs have corresponding source artifacts
|
|
89
|
+
* Validate traceability — ensures canonical docs have corresponding source artifacts,
|
|
90
|
+
* and requirement IDs trace through to test files.
|
|
70
91
|
* Respects config.requiredFiles.canonical — only checks docs the user requires.
|
|
71
|
-
* Also warns about orphaned files (exist but excluded from config).
|
|
72
92
|
* @returns {{ errors: string[], warnings: string[], passed: number, total: number }}
|
|
73
93
|
*/
|
|
74
94
|
export function validateTraceability(projectDir, config) {
|
|
@@ -92,7 +112,7 @@ export function validateTraceability(projectDir, config) {
|
|
|
92
112
|
const projectFiles = [];
|
|
93
113
|
scanDir(projectDir, projectDir, projectFiles);
|
|
94
114
|
|
|
95
|
-
// ──
|
|
115
|
+
// ── Part 1: Source Traceability (existing) ──
|
|
96
116
|
for (const [docName, traceInfo] of Object.entries(TRACE_MAP)) {
|
|
97
117
|
// Skip docs not in the user's required list
|
|
98
118
|
if (!requiredDocs.has(docName)) continue;
|
|
@@ -130,9 +150,175 @@ export function validateTraceability(projectDir, config) {
|
|
|
130
150
|
}
|
|
131
151
|
} catch { /* ignore */ }
|
|
132
152
|
|
|
153
|
+
// ── Part 2: Requirement ID Traceability (V-Model) ──
|
|
154
|
+
const reqResult = validateRequirementTraceability(projectDir, config, projectFiles);
|
|
155
|
+
errors.push(...reqResult.errors);
|
|
156
|
+
warnings.push(...reqResult.warnings);
|
|
157
|
+
passed += reqResult.passed;
|
|
158
|
+
total += reqResult.total;
|
|
159
|
+
|
|
160
|
+
return { errors, warnings, passed, total };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ──── Requirement ID Traceability ────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Scan docs for requirement IDs and verify they appear in test files.
|
|
167
|
+
*
|
|
168
|
+
* Behavior:
|
|
169
|
+
* - If no requirement IDs found anywhere → silently passes (0 checks)
|
|
170
|
+
* - If IDs found → validates each has a matching test reference
|
|
171
|
+
* - Reports untraced requirements and orphaned test refs
|
|
172
|
+
*/
|
|
173
|
+
function validateRequirementTraceability(projectDir, config, projectFiles) {
|
|
174
|
+
const errors = [];
|
|
175
|
+
const warnings = [];
|
|
176
|
+
let passed = 0;
|
|
177
|
+
let total = 0;
|
|
178
|
+
|
|
179
|
+
// Get requirement patterns (user-configurable or defaults)
|
|
180
|
+
const customPattern = config.traceability?.requirementPattern;
|
|
181
|
+
const patterns = customPattern
|
|
182
|
+
? [new RegExp(customPattern, 'g')]
|
|
183
|
+
: DEFAULT_REQ_PATTERNS;
|
|
184
|
+
|
|
185
|
+
// ── Step 1: Collect requirement IDs from documentation ──
|
|
186
|
+
const reqIds = new Map(); // reqId → { file, line }
|
|
187
|
+
const docSearchPaths = getRequirementDocPaths(projectDir, config);
|
|
188
|
+
|
|
189
|
+
for (const docPath of docSearchPaths) {
|
|
190
|
+
if (!existsSync(docPath)) continue;
|
|
191
|
+
|
|
192
|
+
const content = readFileSync(docPath, 'utf-8');
|
|
193
|
+
const lines = content.split('\n');
|
|
194
|
+
const docName = relative(projectDir, docPath);
|
|
195
|
+
|
|
196
|
+
for (let i = 0; i < lines.length; i++) {
|
|
197
|
+
for (const pattern of patterns) {
|
|
198
|
+
// Reset regex lastIndex for each line
|
|
199
|
+
pattern.lastIndex = 0;
|
|
200
|
+
let match;
|
|
201
|
+
while ((match = pattern.exec(lines[i])) !== null) {
|
|
202
|
+
const reqId = match[0]; // e.g., "REQ-001"
|
|
203
|
+
if (!reqIds.has(reqId)) {
|
|
204
|
+
reqIds.set(reqId, { file: docName, line: i + 1 });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// If no requirement IDs found, silently pass — this project doesn't use them
|
|
212
|
+
if (reqIds.size === 0) {
|
|
213
|
+
return { errors, warnings, passed, total };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Step 2: Scan test files for requirement ID references ──
|
|
217
|
+
const testFiles = projectFiles.filter(f =>
|
|
218
|
+
/\.(test|spec)\.(mjs|cjs|[jt]sx?)$/.test(f) ||
|
|
219
|
+
/__tests__\//.test(f) ||
|
|
220
|
+
/tests?\//.test(f)
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const testRefs = new Map(); // reqId → [{ file, line }]
|
|
224
|
+
|
|
225
|
+
for (const relPath of testFiles) {
|
|
226
|
+
const fullPath = resolve(projectDir, relPath);
|
|
227
|
+
if (!existsSync(fullPath)) continue;
|
|
228
|
+
|
|
229
|
+
let content;
|
|
230
|
+
try { content = readFileSync(fullPath, 'utf-8'); } catch { continue; }
|
|
231
|
+
const lines = content.split('\n');
|
|
232
|
+
|
|
233
|
+
for (let i = 0; i < lines.length; i++) {
|
|
234
|
+
for (const pattern of patterns) {
|
|
235
|
+
pattern.lastIndex = 0;
|
|
236
|
+
let match;
|
|
237
|
+
while ((match = pattern.exec(lines[i])) !== null) {
|
|
238
|
+
const reqId = match[0];
|
|
239
|
+
if (!testRefs.has(reqId)) testRefs.set(reqId, []);
|
|
240
|
+
testRefs.get(reqId).push({ file: relPath, line: i + 1 });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Step 3: Report traceability results ──
|
|
247
|
+
|
|
248
|
+
// Check each documented requirement has at least one test reference
|
|
249
|
+
for (const [reqId, location] of reqIds) {
|
|
250
|
+
total++;
|
|
251
|
+
if (testRefs.has(reqId)) {
|
|
252
|
+
passed++;
|
|
253
|
+
} else {
|
|
254
|
+
warnings.push(
|
|
255
|
+
`Requirement ${reqId} (${location.file}:${location.line}) has no test coverage. ` +
|
|
256
|
+
`Add @req ${reqId} comment to the test that verifies this requirement`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check for orphaned test refs (tests referencing non-existent requirements)
|
|
262
|
+
for (const [reqId, refs] of testRefs) {
|
|
263
|
+
if (!reqIds.has(reqId)) {
|
|
264
|
+
total++;
|
|
265
|
+
warnings.push(
|
|
266
|
+
`Test references ${reqId} (${refs[0].file}:${refs[0].line}) but no requirement ` +
|
|
267
|
+
`with this ID exists in documentation. Remove the reference or add the requirement to docs`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
133
272
|
return { errors, warnings, passed, total };
|
|
134
273
|
}
|
|
135
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Get all file paths where requirement IDs might be defined.
|
|
277
|
+
* Checks: docs-canonical/*.md, spec.md, REQUIREMENTS.md, specs/[feature]/spec.md
|
|
278
|
+
*/
|
|
279
|
+
function getRequirementDocPaths(projectDir, config) {
|
|
280
|
+
const paths = [];
|
|
281
|
+
|
|
282
|
+
// docs-canonical/ directory
|
|
283
|
+
const docsDir = resolve(projectDir, 'docs-canonical');
|
|
284
|
+
if (existsSync(docsDir)) {
|
|
285
|
+
try {
|
|
286
|
+
for (const f of readdirSync(docsDir)) {
|
|
287
|
+
if (extname(f).toLowerCase() === '.md') {
|
|
288
|
+
paths.push(join(docsDir, f));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} catch { /* ignore */ }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Root-level docs
|
|
295
|
+
const rootDocs = ['REQUIREMENTS.md', 'spec.md', 'README.md'];
|
|
296
|
+
for (const doc of rootDocs) {
|
|
297
|
+
const p = resolve(projectDir, doc);
|
|
298
|
+
if (existsSync(p)) paths.push(p);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// User-configured requirement docs
|
|
302
|
+
const configDocs = config.traceability?.requirementDocs || [];
|
|
303
|
+
for (const doc of configDocs) {
|
|
304
|
+
const p = resolve(projectDir, doc);
|
|
305
|
+
if (existsSync(p) && !paths.includes(p)) paths.push(p);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Spec Kit artifacts: specs/*/spec.md
|
|
309
|
+
const specsDir = resolve(projectDir, 'specs');
|
|
310
|
+
if (existsSync(specsDir)) {
|
|
311
|
+
try {
|
|
312
|
+
for (const feature of readdirSync(specsDir)) {
|
|
313
|
+
const specPath = join(specsDir, feature, 'spec.md');
|
|
314
|
+
if (existsSync(specPath)) paths.push(specPath);
|
|
315
|
+
}
|
|
316
|
+
} catch { /* ignore */ }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return paths;
|
|
320
|
+
}
|
|
321
|
+
|
|
136
322
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
137
323
|
|
|
138
324
|
function scanDir(rootDir, dir, files) {
|