docguard-cli 0.27.0 → 0.29.0
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.es.md +102 -0
- package/README.md +65 -31
- package/README.pt-BR.md +101 -0
- package/STANDARD.md +20 -10
- package/cli/commands/agents.mjs +149 -0
- package/cli/commands/diff.mjs +6 -15
- package/cli/commands/explain.mjs +8 -6
- package/cli/commands/generate.mjs +14 -1001
- package/cli/commands/guard.mjs +149 -15
- package/cli/commands/init.mjs +23 -1
- package/cli/commands/llms.mjs +67 -5
- package/cli/commands/mcp.mjs +263 -0
- package/cli/commands/memory.mjs +115 -0
- package/cli/commands/score.mjs +76 -12
- package/cli/commands/sync-tests.mjs +272 -0
- package/cli/commands/sync.mjs +6 -0
- package/cli/commands/verify.mjs +67 -0
- package/cli/docguard.mjs +62 -5
- package/cli/findings.mjs +499 -0
- package/cli/scanners/agent-readability.mjs +202 -0
- package/cli/scanners/semantic-claims.mjs +160 -0
- package/cli/scanners/speckit.mjs +98 -28
- package/cli/shared-ignore.mjs +148 -16
- package/cli/shared.mjs +45 -1
- package/cli/validators/api-surface.mjs +182 -29
- package/cli/validators/architecture.mjs +91 -56
- package/cli/validators/canonical-sync.mjs +59 -28
- package/cli/validators/changelog.mjs +41 -17
- package/cli/validators/cross-reference.mjs +28 -11
- package/cli/validators/doc-quality.mjs +78 -44
- package/cli/validators/docs-coverage.mjs +90 -63
- package/cli/validators/docs-diff.mjs +63 -64
- package/cli/validators/docs-sync.mjs +48 -33
- package/cli/validators/drift.mjs +40 -34
- package/cli/validators/environment.mjs +67 -27
- package/cli/validators/freshness.mjs +12 -5
- package/cli/validators/generated-staleness.mjs +26 -10
- package/cli/validators/metadata-sync.mjs +28 -25
- package/cli/validators/metrics-consistency.mjs +89 -47
- package/cli/validators/schema-sync.mjs +37 -32
- package/cli/validators/security.mjs +7 -20
- package/cli/validators/spec-kit.mjs +3 -0
- package/cli/validators/structure.mjs +58 -23
- package/cli/validators/surface-sync.mjs +34 -15
- package/cli/validators/test-spec.mjs +87 -29
- package/cli/validators/todo-tracking.mjs +83 -74
- package/cli/validators/traceability.mjs +67 -39
- package/cli/writers/doc-generators.mjs +853 -0
- package/cli/writers/generate-io.mjs +142 -0
- package/cli/writers/sarif.mjs +129 -0
- package/commands/docguard.fix.md +56 -53
- package/commands/docguard.guard.md +53 -47
- package/commands/docguard.review.md +49 -31
- package/docs/ai-integration.md +133 -134
- package/docs/commands.md +49 -3
- package/docs/configuration.md +38 -0
- package/docs/faq.md +15 -0
- package/extensions/spec-kit-docguard/extension.yml +1 -1
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +2 -2
- package/package.json +1 -1
- package/schemas/docguard-config.schema.json +17 -0
- package/templates/ENVIRONMENT.md.template +5 -0
- package/templates/REQUIREMENTS.md.template +2 -0
- package/templates/SECURITY.md.template +6 -1
- package/templates/TEST-SPEC.md.template +5 -0
- package/templates/commands/docguard.fix.md +33 -10
- package/templates/commands/docguard.guard.md +40 -26
- package/templates/commands/docguard.init.md +23 -11
- package/templates/commands/docguard.review.md +25 -8
- package/templates/commands/docguard.update.md +14 -4
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
19
19
|
import { resolve, join, relative, extname } from 'node:path';
|
|
20
|
-
import { shouldIgnore } from '../shared-ignore.mjs';
|
|
20
|
+
import { shouldIgnore, walkFiles as sharedWalkFiles } from '../shared-ignore.mjs';
|
|
21
|
+
import { mkFinding, resultFromFindings } from '../findings.mjs';
|
|
21
22
|
|
|
22
23
|
const IGNORE_DIRS = new Set([
|
|
23
24
|
'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
|
|
@@ -77,25 +78,34 @@ const SKIP_REASON_PATTERN = /\/\/\s*(REASON|SKIP|TODO|FIXME|NOTE|WHY)\s*:/i;
|
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
80
|
* Main validator — checks for untracked TODOs and unexplained test skips.
|
|
81
|
+
*
|
|
82
|
+
* v0.29: migrated to structured findings (TDO001–TDO003). Messages are
|
|
83
|
+
* byte-identical to the legacy strings — resultFromFindings derives the
|
|
84
|
+
* errors/warnings arrays from the same findings array.
|
|
80
85
|
*/
|
|
81
86
|
export function validateTodoTracking(projectDir, config) {
|
|
82
|
-
const
|
|
87
|
+
const findings = [];
|
|
88
|
+
let passed = 0;
|
|
89
|
+
let total = 0;
|
|
83
90
|
|
|
84
91
|
// ── Part 1: Skipped Tests ──
|
|
85
92
|
const skipResults = checkSkippedTests(projectDir, config);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
results.total += skipResults.total;
|
|
93
|
+
findings.push(...skipResults.findings);
|
|
94
|
+
passed += skipResults.passed;
|
|
95
|
+
total += skipResults.total;
|
|
90
96
|
|
|
91
97
|
// ── Part 2: Untracked Annotations ──
|
|
92
98
|
const todoResults = checkUntrackedTodos(projectDir, config);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
findings.push(...todoResults.findings);
|
|
100
|
+
passed += todoResults.passed;
|
|
101
|
+
total += todoResults.total;
|
|
102
|
+
|
|
103
|
+
const res = resultFromFindings(findings, { passed, total });
|
|
104
|
+
// Back-compat: on a clean run the legacy result carried no extra keys (the
|
|
105
|
+
// empty-project test deep-equals the whole object), and an empty findings
|
|
106
|
+
// array has nothing to render — so omit the key when there are no findings.
|
|
107
|
+
if (findings.length === 0) delete res.findings;
|
|
108
|
+
return res;
|
|
99
109
|
}
|
|
100
110
|
|
|
101
111
|
// ──── Skipped Tests ────────────────────────────────────────────────────────
|
|
@@ -104,15 +114,14 @@ export function validateTodoTracking(projectDir, config) {
|
|
|
104
114
|
* Scan test files for skip/todo patterns without adjacent explanation comments.
|
|
105
115
|
*/
|
|
106
116
|
function checkSkippedTests(projectDir, config) {
|
|
107
|
-
const
|
|
108
|
-
const warnings = [];
|
|
117
|
+
const findings = [];
|
|
109
118
|
let passed = 0;
|
|
110
119
|
let total = 0;
|
|
111
120
|
|
|
112
121
|
const testFiles = [];
|
|
113
122
|
findTestFiles(projectDir, projectDir, testFiles, config);
|
|
114
123
|
|
|
115
|
-
if (testFiles.length === 0) return {
|
|
124
|
+
if (testFiles.length === 0) return { findings, passed, total };
|
|
116
125
|
|
|
117
126
|
// Check: "Project has test files" → pass
|
|
118
127
|
total++;
|
|
@@ -157,10 +166,19 @@ function checkSkippedTests(projectDir, config) {
|
|
|
157
166
|
skippedWithReason++;
|
|
158
167
|
} else {
|
|
159
168
|
skippedWithoutReason++;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
findings.push(mkFinding({
|
|
170
|
+
code: 'TDO001',
|
|
171
|
+
validator: 'todoTracking',
|
|
172
|
+
severity: 'warn',
|
|
173
|
+
message: `Skipped test without explanation at ${relPath}:${i + 1}. ` +
|
|
174
|
+
`Add a // REASON: comment explaining why the test is skipped`,
|
|
175
|
+
location: `${relPath}:${i + 1}`,
|
|
176
|
+
suggestion: {
|
|
177
|
+
kind: 'fix',
|
|
178
|
+
text: 'Add a // REASON: comment on or up to 3 lines above the skip explaining why',
|
|
179
|
+
pragma: '// REASON: <why this test is skipped>',
|
|
180
|
+
},
|
|
181
|
+
}));
|
|
164
182
|
}
|
|
165
183
|
}
|
|
166
184
|
}
|
|
@@ -173,7 +191,7 @@ function checkSkippedTests(projectDir, config) {
|
|
|
173
191
|
}
|
|
174
192
|
}
|
|
175
193
|
|
|
176
|
-
return {
|
|
194
|
+
return { findings, passed, total };
|
|
177
195
|
}
|
|
178
196
|
|
|
179
197
|
// ──── Untracked Annotations ────────────────────────────────────────────────
|
|
@@ -183,8 +201,7 @@ function checkSkippedTests(projectDir, config) {
|
|
|
183
201
|
* in tracking documentation.
|
|
184
202
|
*/
|
|
185
203
|
function checkUntrackedTodos(projectDir, config) {
|
|
186
|
-
const
|
|
187
|
-
const warnings = [];
|
|
204
|
+
const findings = [];
|
|
188
205
|
let passed = 0;
|
|
189
206
|
let total = 0;
|
|
190
207
|
|
|
@@ -196,7 +213,7 @@ function checkUntrackedTodos(projectDir, config) {
|
|
|
196
213
|
// No TODOs found — that's clean code
|
|
197
214
|
total++;
|
|
198
215
|
passed++;
|
|
199
|
-
return {
|
|
216
|
+
return { findings, passed, total };
|
|
200
217
|
}
|
|
201
218
|
|
|
202
219
|
// Check if TODOs are tracked in documentation
|
|
@@ -235,23 +252,41 @@ function checkUntrackedTodos(projectDir, config) {
|
|
|
235
252
|
untrackedCount++;
|
|
236
253
|
// Only report first 5 to avoid noise
|
|
237
254
|
if (untrackedCount <= 5) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
255
|
+
findings.push(mkFinding({
|
|
256
|
+
code: 'TDO002',
|
|
257
|
+
validator: 'todoTracking',
|
|
258
|
+
severity: 'warn',
|
|
259
|
+
message: `Untracked ${todo.keyword} at ${todo.file}:${todo.line}: "${todo.text.substring(0, 60)}". ` +
|
|
260
|
+
`Add to ROADMAP.md, CURRENT-STATE.md, or a GitHub issue`,
|
|
261
|
+
location: `${todo.file}:${todo.line}`,
|
|
262
|
+
suggestion: {
|
|
263
|
+
kind: 'fix',
|
|
264
|
+
text: 'Track it in ROADMAP.md or CURRENT-STATE.md (or resolve it) — or exclude the path via todoIgnore in .docguard.json',
|
|
265
|
+
},
|
|
266
|
+
}));
|
|
242
267
|
}
|
|
243
268
|
}
|
|
244
269
|
}
|
|
245
270
|
|
|
246
271
|
if (untrackedCount > 5) {
|
|
247
|
-
|
|
272
|
+
findings.push(mkFinding({
|
|
273
|
+
code: 'TDO003',
|
|
274
|
+
validator: 'todoTracking',
|
|
275
|
+
severity: 'warn',
|
|
276
|
+
message: `...and ${untrackedCount - 5} more untracked TODO/FIXME items`,
|
|
277
|
+
location: null,
|
|
278
|
+
suggestion: {
|
|
279
|
+
kind: 'suppress',
|
|
280
|
+
text: 'Address the items above and re-run guard to surface the rest — or exclude noisy paths via todoIgnore in .docguard.json',
|
|
281
|
+
},
|
|
282
|
+
}));
|
|
248
283
|
}
|
|
249
284
|
|
|
250
285
|
if (untrackedCount === 0) {
|
|
251
286
|
passed++;
|
|
252
287
|
}
|
|
253
288
|
|
|
254
|
-
return {
|
|
289
|
+
return { findings, passed, total };
|
|
255
290
|
}
|
|
256
291
|
|
|
257
292
|
/**
|
|
@@ -285,34 +320,23 @@ function loadTrackingDocs(projectDir, config) {
|
|
|
285
320
|
|
|
286
321
|
// ──── File Scanners ────────────────────────────────────────────────────────
|
|
287
322
|
|
|
323
|
+
// v0.29 consolidation: traversal delegates to the shared canonical walker;
|
|
324
|
+
// test-file pattern matching and config-ignore filtering stay per-file here.
|
|
288
325
|
function findTestFiles(rootDir, dir, files, config) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
findTestFiles(rootDir, full, files, config);
|
|
302
|
-
} else {
|
|
303
|
-
const ext = extname(entry).toLowerCase();
|
|
304
|
-
if (!TEST_EXTENSIONS.has(ext)) continue;
|
|
305
|
-
|
|
306
|
-
// Match test file patterns
|
|
307
|
-
if (/\.(test|spec)\.(mjs|cjs|[jt]sx?)$/.test(entry) ||
|
|
308
|
-
/__(tests|test)__/.test(relative(rootDir, full))) {
|
|
309
|
-
const relPath = relative(rootDir, full);
|
|
310
|
-
// Apply config ignore patterns (todoIgnore + global ignore)
|
|
311
|
-
if (config && shouldIgnore(relPath, config, 'todoIgnore')) continue;
|
|
312
|
-
files.push(relPath);
|
|
313
|
-
}
|
|
326
|
+
sharedWalkFiles(dir, (full) => {
|
|
327
|
+
const entry = full.slice(full.lastIndexOf('/') + 1);
|
|
328
|
+
const ext = extname(entry).toLowerCase();
|
|
329
|
+
if (!TEST_EXTENSIONS.has(ext)) return;
|
|
330
|
+
|
|
331
|
+
// Match test file patterns
|
|
332
|
+
if (/\.(test|spec)\.(mjs|cjs|[jt]sx?)$/.test(entry) ||
|
|
333
|
+
/__(tests|test)__/.test(relative(rootDir, full))) {
|
|
334
|
+
const relPath = relative(rootDir, full);
|
|
335
|
+
// Apply config ignore patterns (todoIgnore + global ignore)
|
|
336
|
+
if (config && shouldIgnore(relPath, config, 'todoIgnore')) return;
|
|
337
|
+
files.push(relPath);
|
|
314
338
|
}
|
|
315
|
-
}
|
|
339
|
+
}, { ignoreDirs: IGNORE_DIRS });
|
|
316
340
|
}
|
|
317
341
|
|
|
318
342
|
// Test-file path patterns — TODO scanning skips these by default to avoid
|
|
@@ -349,25 +373,10 @@ function findTodos(rootDir, dir, todos, config) {
|
|
|
349
373
|
return;
|
|
350
374
|
}
|
|
351
375
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
for (const entry of entries) {
|
|
358
|
-
if (IGNORE_DIRS.has(entry)) continue;
|
|
359
|
-
if (entry.startsWith('.')) continue;
|
|
360
|
-
|
|
361
|
-
const full = join(dir, entry);
|
|
362
|
-
let stat;
|
|
363
|
-
try { stat = statSync(full); } catch { continue; }
|
|
364
|
-
|
|
365
|
-
if (stat.isDirectory()) {
|
|
366
|
-
findTodos(rootDir, full, todos, config);
|
|
367
|
-
} else {
|
|
368
|
-
_scanTodoFile(rootDir, full, todos, config);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
376
|
+
// v0.29 consolidation: traversal delegates to the shared canonical walker.
|
|
377
|
+
sharedWalkFiles(dir, (full) => _scanTodoFile(rootDir, full, todos, config), {
|
|
378
|
+
ignoreDirs: IGNORE_DIRS,
|
|
379
|
+
});
|
|
371
380
|
}
|
|
372
381
|
|
|
373
382
|
/**
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
17
17
|
import { resolve, join, relative, basename, extname } from 'node:path';
|
|
18
18
|
import { TRACE_MAP, TEST_PATTERNS, isTraceableSource } from '../shared-trace-patterns.mjs';
|
|
19
|
+
import { walkFiles as sharedWalkFiles } from '../shared-ignore.mjs';
|
|
20
|
+
import { mkFinding, resultFromFindings } from '../findings.mjs';
|
|
19
21
|
|
|
20
22
|
const IGNORE_DIRS = new Set([
|
|
21
23
|
'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
|
|
@@ -51,18 +53,21 @@ const DEFAULT_REQ_PATTERNS = [
|
|
|
51
53
|
* Validate traceability — ensures canonical docs have corresponding source artifacts,
|
|
52
54
|
* and requirement IDs trace through to test files.
|
|
53
55
|
* Respects config.requiredFiles.canonical — only checks docs the user requires.
|
|
56
|
+
*
|
|
57
|
+
* v0.29: migrated to structured findings (TRC001–TRC005). Messages are
|
|
58
|
+
* byte-identical to the legacy strings — resultFromFindings derives the
|
|
59
|
+
* errors/warnings arrays from the same findings array.
|
|
54
60
|
* @returns {{ errors: string[], warnings: string[], passed: number, total: number }}
|
|
55
61
|
*/
|
|
56
62
|
export function validateTraceability(projectDir, config) {
|
|
57
|
-
const
|
|
58
|
-
const warnings = [];
|
|
63
|
+
const findings = [];
|
|
59
64
|
let passed = 0;
|
|
60
65
|
let total = 0;
|
|
61
66
|
|
|
62
67
|
const docsDir = resolve(projectDir, 'docs-canonical');
|
|
63
68
|
if (!existsSync(docsDir)) {
|
|
64
69
|
// No docs-canonical dir at all — structure validator handles this
|
|
65
|
-
return
|
|
70
|
+
return resultFromFindings([], { passed: 0, total: 0 });
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
// Build set of required doc basenames from config
|
|
@@ -93,7 +98,14 @@ export function validateTraceability(projectDir, config) {
|
|
|
93
98
|
const docExists = existsSync(docPath);
|
|
94
99
|
|
|
95
100
|
if (!docExists) {
|
|
96
|
-
|
|
101
|
+
findings.push(mkFinding({
|
|
102
|
+
code: 'TRC001',
|
|
103
|
+
validator: 'traceability',
|
|
104
|
+
severity: 'warn',
|
|
105
|
+
message: `${docName} — required but missing, no traceability possible`,
|
|
106
|
+
location: `docs-canonical/${docName}`,
|
|
107
|
+
suggestion: { kind: 'fix', text: 'Create the required doc from the professional template', command: 'docguard init' },
|
|
108
|
+
}));
|
|
97
109
|
continue;
|
|
98
110
|
}
|
|
99
111
|
|
|
@@ -121,7 +133,18 @@ export function validateTraceability(projectDir, config) {
|
|
|
121
133
|
if (hasSource) {
|
|
122
134
|
passed++;
|
|
123
135
|
} else {
|
|
124
|
-
|
|
136
|
+
findings.push(mkFinding({
|
|
137
|
+
code: 'TRC002',
|
|
138
|
+
validator: 'traceability',
|
|
139
|
+
severity: 'warn',
|
|
140
|
+
message: `${docName} — exists but no matching source code found (unlinked doc)`,
|
|
141
|
+
location: `docs-canonical/${docName}`,
|
|
142
|
+
suggestion: {
|
|
143
|
+
kind: 'fix',
|
|
144
|
+
text: 'Link a source file explicitly with a header annotation if the code lives in a non-standard location',
|
|
145
|
+
pragma: `// @doc ${docName}`,
|
|
146
|
+
},
|
|
147
|
+
}));
|
|
125
148
|
}
|
|
126
149
|
}
|
|
127
150
|
|
|
@@ -130,19 +153,25 @@ export function validateTraceability(projectDir, config) {
|
|
|
130
153
|
const existingDocs = readdirSync(docsDir).filter(f => f.endsWith('.md'));
|
|
131
154
|
for (const docFile of existingDocs) {
|
|
132
155
|
if (!requiredDocs.has(docFile) && TRACE_MAP[docFile]) {
|
|
133
|
-
|
|
156
|
+
findings.push(mkFinding({
|
|
157
|
+
code: 'TRC003',
|
|
158
|
+
validator: 'traceability',
|
|
159
|
+
severity: 'warn',
|
|
160
|
+
message: `${docFile} — file exists in docs-canonical/ but is not in your requiredFiles config. Consider deleting it or adding it to .docguard.json requiredFiles.canonical`,
|
|
161
|
+
location: `docs-canonical/${docFile}`,
|
|
162
|
+
suggestion: { kind: 'review', text: 'Delete the doc, or add it to requiredFiles.canonical in .docguard.json so it gets validated' },
|
|
163
|
+
}));
|
|
134
164
|
}
|
|
135
165
|
}
|
|
136
166
|
} catch { /* ignore */ }
|
|
137
167
|
|
|
138
168
|
// ── Part 2: Requirement ID Traceability (V-Model) ──
|
|
139
169
|
const reqResult = validateRequirementTraceability(projectDir, config, projectFiles);
|
|
140
|
-
|
|
141
|
-
warnings.push(...reqResult.warnings);
|
|
170
|
+
findings.push(...reqResult.findings);
|
|
142
171
|
passed += reqResult.passed;
|
|
143
172
|
total += reqResult.total;
|
|
144
173
|
|
|
145
|
-
return
|
|
174
|
+
return resultFromFindings(findings, { passed, total });
|
|
146
175
|
}
|
|
147
176
|
|
|
148
177
|
// ──── Requirement ID Traceability ────────────────────────────────────────────
|
|
@@ -156,8 +185,7 @@ export function validateTraceability(projectDir, config) {
|
|
|
156
185
|
* - Reports untraced requirements and orphaned test refs
|
|
157
186
|
*/
|
|
158
187
|
function validateRequirementTraceability(projectDir, config, projectFiles) {
|
|
159
|
-
const
|
|
160
|
-
const warnings = [];
|
|
188
|
+
const findings = [];
|
|
161
189
|
let passed = 0;
|
|
162
190
|
let total = 0;
|
|
163
191
|
|
|
@@ -172,7 +200,7 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
|
|
|
172
200
|
|
|
173
201
|
// If no requirement IDs found, silently pass — this project doesn't use them
|
|
174
202
|
if (reqIds.size === 0) {
|
|
175
|
-
return {
|
|
203
|
+
return { findings, passed, total };
|
|
176
204
|
}
|
|
177
205
|
|
|
178
206
|
// ── Step 2: Scan test files for requirement ID references ──
|
|
@@ -186,10 +214,15 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
|
|
|
186
214
|
if (testRefs.has(reqId)) {
|
|
187
215
|
passed++;
|
|
188
216
|
} else {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
217
|
+
findings.push(mkFinding({
|
|
218
|
+
code: 'TRC004',
|
|
219
|
+
validator: 'traceability',
|
|
220
|
+
severity: 'warn',
|
|
221
|
+
message: `Requirement ${reqId} (${location.file}:${location.line}) has no test coverage. ` +
|
|
222
|
+
`Add @req ${reqId} comment to the test that verifies this requirement`,
|
|
223
|
+
location: `${location.file}:${location.line}`,
|
|
224
|
+
suggestion: { kind: 'fix', text: `Add an @req ${reqId} comment to the test that verifies this requirement` },
|
|
225
|
+
}));
|
|
193
226
|
}
|
|
194
227
|
}
|
|
195
228
|
|
|
@@ -197,14 +230,19 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
|
|
|
197
230
|
for (const [reqId, refs] of testRefs) {
|
|
198
231
|
if (!reqIds.has(reqId)) {
|
|
199
232
|
total++;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
233
|
+
findings.push(mkFinding({
|
|
234
|
+
code: 'TRC005',
|
|
235
|
+
validator: 'traceability',
|
|
236
|
+
severity: 'warn',
|
|
237
|
+
message: `Test references ${reqId} (${refs[0].file}:${refs[0].line}) but no requirement ` +
|
|
238
|
+
`with this ID exists in documentation. Remove the reference or add the requirement to docs`,
|
|
239
|
+
location: `${refs[0].file}:${refs[0].line}`,
|
|
240
|
+
suggestion: { kind: 'review', text: 'Remove the stale reference, or add the requirement to the documentation' },
|
|
241
|
+
}));
|
|
204
242
|
}
|
|
205
243
|
}
|
|
206
244
|
|
|
207
|
-
return {
|
|
245
|
+
return { findings, passed, total };
|
|
208
246
|
}
|
|
209
247
|
|
|
210
248
|
function collectRequirementIds(projectDir, config, patterns) {
|
|
@@ -377,23 +415,13 @@ function scanDocAnnotations(projectFiles, projectDir) {
|
|
|
377
415
|
return map;
|
|
378
416
|
}
|
|
379
417
|
|
|
418
|
+
// v0.29 consolidation: traversal delegates to the shared canonical walker.
|
|
419
|
+
// keepDot preserves the traceability-relevant dot entries (.env, .env.example,
|
|
420
|
+
// .gitignore, .github/) that a doc may legitimately reference.
|
|
380
421
|
function scanDir(rootDir, dir, files) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (entry.startsWith('.') && entry !== '.env' && entry !== '.env.example'
|
|
387
|
-
&& entry !== '.gitignore' && !entry.startsWith('.github')) continue;
|
|
388
|
-
|
|
389
|
-
const full = join(dir, entry);
|
|
390
|
-
let stat;
|
|
391
|
-
try { stat = statSync(full); } catch { continue; }
|
|
392
|
-
|
|
393
|
-
if (stat.isDirectory()) {
|
|
394
|
-
scanDir(rootDir, full, files);
|
|
395
|
-
} else {
|
|
396
|
-
files.push(relative(rootDir, full));
|
|
397
|
-
}
|
|
398
|
-
}
|
|
422
|
+
sharedWalkFiles(dir, (full) => files.push(relative(rootDir, full)), {
|
|
423
|
+
ignoreDirs: IGNORE_DIRS,
|
|
424
|
+
keepDot: (entry) => entry === '.env' || entry === '.env.example'
|
|
425
|
+
|| entry === '.gitignore' || entry.startsWith('.github'),
|
|
426
|
+
});
|
|
399
427
|
}
|