docguard-cli 0.7.0 → 0.7.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.
|
@@ -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
|
],
|
|
@@ -0,0 +1,142 @@
|
|
|
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
|
+
* @returns {{ errors: string[], warnings: string[], passed: number, total: number }}
|
|
71
|
+
*/
|
|
72
|
+
export function validateTraceability(projectDir, config) {
|
|
73
|
+
const errors = [];
|
|
74
|
+
const warnings = [];
|
|
75
|
+
let passed = 0;
|
|
76
|
+
let total = 0;
|
|
77
|
+
|
|
78
|
+
const docsDir = resolve(projectDir, 'docs-canonical');
|
|
79
|
+
if (!existsSync(docsDir)) {
|
|
80
|
+
// No docs-canonical dir at all — structure validator handles this
|
|
81
|
+
return { errors, warnings, passed: 0, total: 0 };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Scan project files once
|
|
85
|
+
const projectFiles = [];
|
|
86
|
+
scanDir(projectDir, projectDir, projectFiles);
|
|
87
|
+
|
|
88
|
+
for (const [docName, traceInfo] of Object.entries(TRACE_MAP)) {
|
|
89
|
+
total++;
|
|
90
|
+
const docPath = resolve(docsDir, docName);
|
|
91
|
+
const docExists = existsSync(docPath);
|
|
92
|
+
|
|
93
|
+
if (!docExists) {
|
|
94
|
+
// Only warn for API-REFERENCE.md (not all projects have APIs)
|
|
95
|
+
if (docName === 'API-REFERENCE.md') {
|
|
96
|
+
// Don't count API-REFERENCE as missing — it's optional
|
|
97
|
+
total--;
|
|
98
|
+
} else {
|
|
99
|
+
warnings.push(`${docName} — document missing, no traceability possible`);
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Count matching source files
|
|
105
|
+
let totalSources = 0;
|
|
106
|
+
for (const pattern of traceInfo.sourcePatterns) {
|
|
107
|
+
const matches = projectFiles.filter(f => pattern.glob.test(f));
|
|
108
|
+
totalSources += matches.length;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (totalSources > 0) {
|
|
112
|
+
passed++;
|
|
113
|
+
} else {
|
|
114
|
+
warnings.push(`${docName} — exists but no matching source code found (unlinked doc)`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { errors, warnings, passed, total };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
function scanDir(rootDir, dir, files) {
|
|
124
|
+
let entries;
|
|
125
|
+
try { entries = readdirSync(dir); } catch { return; }
|
|
126
|
+
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
if (IGNORE_DIRS.has(entry)) continue;
|
|
129
|
+
if (entry.startsWith('.') && entry !== '.env' && entry !== '.env.example'
|
|
130
|
+
&& entry !== '.gitignore' && !entry.startsWith('.github')) continue;
|
|
131
|
+
|
|
132
|
+
const full = join(dir, entry);
|
|
133
|
+
let stat;
|
|
134
|
+
try { stat = statSync(full); } catch { continue; }
|
|
135
|
+
|
|
136
|
+
if (stat.isDirectory()) {
|
|
137
|
+
scanDir(rootDir, full, files);
|
|
138
|
+
} else {
|
|
139
|
+
files.push(relative(rootDir, full));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|