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];
|
package/cli/commands/guard.mjs
CHANGED
|
@@ -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) {
|
package/cli/commands/trace.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
// ──
|
|
106
|
+
// ── 3. Scan project files ──
|
|
102
107
|
const projectFiles = [];
|
|
103
108
|
scanDir(projectDir, projectDir, projectFiles);
|
|
104
109
|
|
|
105
|
-
// ──
|
|
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
|
-
// ──
|
|
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
|
+
}
|