docguard-cli 0.7.0 → 0.7.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.
@@ -406,8 +406,8 @@ function generateArchitecture(dir, config, stack, scan, flags, docTools) {
406
406
  componentRows.push(`| Storybook | UI component docs | .storybook/ (${docTools.storybook.storyCount || '?'} stories) | |`);
407
407
  }
408
408
 
409
- // Doc tools section
410
- const docToolRows = [];
409
+ // Doc tools section — always include DocGuard since it generated these docs
410
+ const docToolRows = ['| DocGuard | `.docguard.json` | Active |'];
411
411
  if (docTools?._detected?.length > 0) {
412
412
  for (const tool of docTools._detected) {
413
413
  const info = docTools[tool];
@@ -17,6 +17,7 @@ import { validateSecurity } from '../validators/security.mjs';
17
17
  import { validateDocsSync } from '../validators/docs-sync.mjs';
18
18
  import { validateArchitecture } from '../validators/architecture.mjs';
19
19
  import { validateFreshness } from '../validators/freshness.mjs';
20
+ import { validateTraceability } from '../validators/traceability.mjs';
20
21
 
21
22
  /**
22
23
  * Internal guard — returns structured data, no console output, no process.exit.
@@ -48,6 +49,7 @@ export function runGuardInternal(projectDir, config) {
48
49
  }
49
50
  return { errors, warnings, passed, total: passed + warnings.length + errors.length };
50
51
  }},
52
+ { key: 'traceability', name: 'Traceability', fn: () => validateTraceability(projectDir, config) },
51
53
  ];
52
54
 
53
55
  for (const { key, name, fn } of validatorMap) {
@@ -53,7 +53,7 @@ const TRACE_MAP = {
53
53
  'TEST-SPEC.md': {
54
54
  standard: 'ISO/IEC/IEEE 29119-3',
55
55
  sourcePatterns: [
56
- { label: 'Test files', glob: /\.(test|spec)\.[jt]sx?$/ },
56
+ { label: 'Test files', glob: /\.(test|spec)\.(mjs|cjs|[jt]sx?)$/ },
57
57
  { label: 'Test config', glob: /(jest|vitest|playwright|cypress)\.config/ },
58
58
  { label: 'E2E tests', glob: /(e2e|integration)\// },
59
59
  ],
@@ -89,7 +89,12 @@ export function runTrace(projectDir, config, flags) {
89
89
  console.log(`${c.dim} Directory: ${projectDir}${c.reset}`);
90
90
  console.log(`${c.dim} Generating requirements traceability matrix...${c.reset}\n`);
91
91
 
92
- // ── 1. Inventory canonical docs ──
92
+ // ── 1. Build set of required doc basenames from config ──
93
+ const requiredDocs = new Set(
94
+ (config.requiredFiles?.canonical || []).map(f => basename(f))
95
+ );
96
+
97
+ // ── 2. Inventory canonical docs ──
93
98
  const docsDir = resolve(projectDir, 'docs-canonical');
94
99
  const canonicalDocs = [];
95
100
  if (existsSync(docsDir)) {
@@ -98,16 +103,26 @@ export function runTrace(projectDir, config, flags) {
98
103
  }
99
104
  }
100
105
 
101
- // ── 2. Scan project files ──
106
+ // ── 3. Scan project files ──
102
107
  const projectFiles = [];
103
108
  scanDir(projectDir, projectDir, projectFiles);
104
109
 
105
- // ── 3. Build traceability matrix ──
110
+ // ── 4. Build traceability matrix (only required docs) ──
106
111
  const matrix = [];
112
+ const orphanedDocs = [];
107
113
 
108
114
  for (const [docName, traceInfo] of Object.entries(TRACE_MAP)) {
109
115
  const docPath = resolve(docsDir, docName);
110
116
  const docExists = existsSync(docPath);
117
+
118
+ // Check if this doc is excluded from config
119
+ if (!requiredDocs.has(docName)) {
120
+ if (docExists) {
121
+ orphanedDocs.push(docName);
122
+ }
123
+ continue; // Skip excluded docs from the matrix
124
+ }
125
+
111
126
  let lastModified = null;
112
127
  let docSize = 0;
113
128
 
@@ -152,15 +167,15 @@ export function runTrace(projectDir, config, flags) {
152
167
  });
153
168
  }
154
169
 
155
- // ── 4. Output ──
170
+ // ── 5. Output ──
156
171
  if (flags.format === 'json') {
157
- outputJSON(config.projectName, matrix);
172
+ outputJSON(config.projectName, matrix, orphanedDocs);
158
173
  } else {
159
- outputText(config.projectName, matrix, canonicalDocs);
174
+ outputText(config.projectName, matrix, canonicalDocs, orphanedDocs);
160
175
  }
161
176
  }
162
177
 
163
- function outputJSON(projectName, matrix) {
178
+ function outputJSON(projectName, matrix, orphanedDocs) {
164
179
  const result = {
165
180
  project: projectName,
166
181
  traceability: matrix.map(m => ({
@@ -173,19 +188,21 @@ function outputJSON(projectName, matrix) {
173
188
  tests: m.relatedTests.length,
174
189
  traces: m.traces,
175
190
  })),
191
+ orphanedDocs,
176
192
  summary: {
177
193
  total: matrix.length,
178
194
  traced: matrix.filter(m => m.coverageSignal === 'TRACED').length,
179
195
  partial: matrix.filter(m => m.coverageSignal === 'PARTIAL').length,
180
196
  unlinked: matrix.filter(m => m.coverageSignal === 'UNLINKED').length,
181
197
  missing: matrix.filter(m => m.coverageSignal === 'MISSING').length,
198
+ orphaned: orphanedDocs.length,
182
199
  },
183
200
  timestamp: new Date().toISOString(),
184
201
  };
185
202
  console.log(JSON.stringify(result, null, 2));
186
203
  }
187
204
 
188
- function outputText(projectName, matrix, canonicalDocs) {
205
+ function outputText(projectName, matrix, canonicalDocs, orphanedDocs) {
189
206
  // Header table
190
207
  console.log(` ${c.bold}Traceability Matrix${c.reset}\n`);
191
208
  console.log(` ${c.dim}${'Document'.padEnd(22)} ${'Standard'.padEnd(28)} ${'Status'.padEnd(10)} ${'Sources'.padEnd(9)} ${'Tests'.padEnd(7)} ${'Last Modified'}${c.reset}`);
@@ -257,6 +274,16 @@ function outputText(projectName, matrix, canonicalDocs) {
257
274
  console.log(` ${c.dim}Run ${c.cyan}docguard diagnose${c.dim} to fix coverage gaps.${c.reset}`);
258
275
  }
259
276
 
277
+ // ── Orphaned docs warning ──
278
+ if (orphanedDocs.length > 0) {
279
+ console.log(`\n ${c.yellow}⚠️ Orphaned Files (${orphanedDocs.length})${c.reset}`);
280
+ console.log(` ${c.dim}These files exist in docs-canonical/ but are excluded from your config:${c.reset}`);
281
+ for (const doc of orphanedDocs) {
282
+ console.log(` ${c.yellow}→${c.reset} ${doc}`);
283
+ }
284
+ console.log(` ${c.dim}Delete them or add to .docguard.json requiredFiles.canonical${c.reset}`);
285
+ }
286
+
260
287
  console.log(`\n ${c.dim}Traceability methodology: ISO/IEC/IEEE 29119 (Lopez et al., AITPG, IEEE TSE 2026)${c.reset}\n`);
261
288
  }
262
289
 
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Traceability Validator — Checks that canonical docs are linked to source code
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.
6
+ *
7
+ * Inspired by ISO/IEC/IEEE 29119 traceability requirements
8
+ * and Lopez et al., AITPG (IEEE TSE 2026).
9
+ */
10
+
11
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
12
+ import { resolve, join, relative, basename } from 'node:path';
13
+
14
+ const IGNORE_DIRS = new Set([
15
+ 'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
16
+ '.cache', '__pycache__', '.venv', 'vendor', '.turbo', '.vercel',
17
+ '.amplify-hosting', '.serverless',
18
+ ]);
19
+
20
+ /**
21
+ * Mapping of canonical docs to source code patterns they should trace to.
22
+ */
23
+ const TRACE_MAP = {
24
+ 'ARCHITECTURE.md': {
25
+ sourcePatterns: [
26
+ { label: 'Entry points', glob: /^(index|main|app|server)\.[jt]sx?$/ },
27
+ { label: 'Config files', glob: /^(package\.json|tsconfig.*|next\.config|vite\.config)/ },
28
+ { label: 'Route handlers', glob: /(routes?|api|pages|app)\// },
29
+ ],
30
+ },
31
+ 'DATA-MODEL.md': {
32
+ sourcePatterns: [
33
+ { label: 'Schema definitions', glob: /(schema|model|entity|migration|prisma)/i },
34
+ { label: 'Type definitions', glob: /types?\.[jt]sx?$/ },
35
+ { label: 'Database configs', glob: /(drizzle|knex|sequelize|typeorm)/i },
36
+ ],
37
+ },
38
+ 'TEST-SPEC.md': {
39
+ sourcePatterns: [
40
+ { label: 'Test files', glob: /\.(test|spec)\.(mjs|cjs|[jt]sx?)$/ },
41
+ { label: 'Test config', glob: /(jest|vitest|playwright|cypress)\.config/ },
42
+ { label: 'E2E tests', glob: /(e2e|integration)\// },
43
+ ],
44
+ },
45
+ 'SECURITY.md': {
46
+ sourcePatterns: [
47
+ { label: 'Auth modules', glob: /(auth|login|session|jwt|oauth|middleware)/i },
48
+ { label: 'Secret configs', glob: /\.(env|env\.example|env\.local)$/ },
49
+ { label: 'Gitignore', glob: /^\.gitignore$/ },
50
+ ],
51
+ },
52
+ 'ENVIRONMENT.md': {
53
+ sourcePatterns: [
54
+ { label: 'Env files', glob: /\.env/ },
55
+ { label: 'Docker configs', glob: /(Dockerfile|docker-compose|\.dockerignore)/ },
56
+ { label: 'CI/CD configs', glob: /\.(github|gitlab-ci|circleci)/ },
57
+ ],
58
+ },
59
+ 'API-REFERENCE.md': {
60
+ sourcePatterns: [
61
+ { label: 'Route handlers', glob: /(routes?|controllers?|handlers?)\// },
62
+ { label: 'OpenAPI spec', glob: /(openapi|swagger)\.(json|ya?ml)/ },
63
+ { label: 'API middleware', glob: /middleware\// },
64
+ ],
65
+ },
66
+ };
67
+
68
+ /**
69
+ * Validate traceability — ensures canonical docs have corresponding source artifacts.
70
+ * Respects config.requiredFiles.canonical — only checks docs the user requires.
71
+ * Also warns about orphaned files (exist but excluded from config).
72
+ * @returns {{ errors: string[], warnings: string[], passed: number, total: number }}
73
+ */
74
+ export function validateTraceability(projectDir, config) {
75
+ const errors = [];
76
+ const warnings = [];
77
+ let passed = 0;
78
+ let total = 0;
79
+
80
+ const docsDir = resolve(projectDir, 'docs-canonical');
81
+ if (!existsSync(docsDir)) {
82
+ // No docs-canonical dir at all — structure validator handles this
83
+ return { errors, warnings, passed: 0, total: 0 };
84
+ }
85
+
86
+ // Build set of required doc basenames from config
87
+ const requiredDocs = new Set(
88
+ (config.requiredFiles?.canonical || []).map(f => basename(f))
89
+ );
90
+
91
+ // Scan project files once
92
+ const projectFiles = [];
93
+ scanDir(projectDir, projectDir, projectFiles);
94
+
95
+ // ── Check required docs for traceability ──
96
+ for (const [docName, traceInfo] of Object.entries(TRACE_MAP)) {
97
+ // Skip docs not in the user's required list
98
+ if (!requiredDocs.has(docName)) continue;
99
+
100
+ total++;
101
+ const docPath = resolve(docsDir, docName);
102
+ const docExists = existsSync(docPath);
103
+
104
+ if (!docExists) {
105
+ warnings.push(`${docName} — required but missing, no traceability possible`);
106
+ continue;
107
+ }
108
+
109
+ // Count matching source files
110
+ let totalSources = 0;
111
+ for (const pattern of traceInfo.sourcePatterns) {
112
+ const matches = projectFiles.filter(f => pattern.glob.test(f));
113
+ totalSources += matches.length;
114
+ }
115
+
116
+ if (totalSources > 0) {
117
+ passed++;
118
+ } else {
119
+ warnings.push(`${docName} — exists but no matching source code found (unlinked doc)`);
120
+ }
121
+ }
122
+
123
+ // ── Detect orphaned files (exist but not required) ──
124
+ try {
125
+ const existingDocs = readdirSync(docsDir).filter(f => f.endsWith('.md'));
126
+ for (const docFile of existingDocs) {
127
+ if (!requiredDocs.has(docFile) && TRACE_MAP[docFile]) {
128
+ warnings.push(`${docFile} — file exists in docs-canonical/ but is not in your requiredFiles config. Consider deleting it or adding it to .docguard.json requiredFiles.canonical`);
129
+ }
130
+ }
131
+ } catch { /* ignore */ }
132
+
133
+ return { errors, warnings, passed, total };
134
+ }
135
+
136
+ // ── Helpers ────────────────────────────────────────────────────────────────
137
+
138
+ function scanDir(rootDir, dir, files) {
139
+ let entries;
140
+ try { entries = readdirSync(dir); } catch { return; }
141
+
142
+ for (const entry of entries) {
143
+ if (IGNORE_DIRS.has(entry)) continue;
144
+ if (entry.startsWith('.') && entry !== '.env' && entry !== '.env.example'
145
+ && entry !== '.gitignore' && !entry.startsWith('.github')) continue;
146
+
147
+ const full = join(dir, entry);
148
+ let stat;
149
+ try { stat = statSync(full); } catch { continue; }
150
+
151
+ if (stat.isDirectory()) {
152
+ scanDir(rootDir, full, files);
153
+ } else {
154
+ files.push(relative(rootDir, full));
155
+ }
156
+ }
157
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.7.0",
3
+ "version": "0.7.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": {