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.
@@ -1,15 +1,20 @@
1
1
  /**
2
2
  * Traceability Validator — Checks that canonical docs are linked to source code
3
3
  *
4
- * Returns warnings for PARTIAL/UNLINKED canonical docs, and errors for MISSING ones.
5
- * This runs as part of `docguard guard` on every invocation.
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
- * Inspired by ISO/IEC/IEEE 29119 traceability requirements
8
- * and Lopez et al., AITPG (IEEE TSE 2026).
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
- // ── Check required docs for traceability ──
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.8.2",
3
+ "version": "0.9.1",
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": {