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
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Structure Validator — Checks that all required CDD files exist
|
|
3
|
+
*
|
|
4
|
+
* v0.29: migrated to structured findings (STR001–STR003). Messages are
|
|
5
|
+
* byte-identical to the legacy strings — resultFromFindings derives the
|
|
6
|
+
* errors/warnings arrays from the same findings, so counts, exit codes, and
|
|
7
|
+
* existing tests are unaffected; guard just renders richer output.
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
10
|
import { existsSync, readFileSync } from 'node:fs';
|
|
6
11
|
import { resolve } from 'node:path';
|
|
7
12
|
import { docHasSection } from '../shared.mjs';
|
|
13
|
+
import { mkFinding, resultFromFindings } from '../findings.mjs';
|
|
8
14
|
|
|
9
15
|
export function validateStructure(projectDir, config) {
|
|
10
|
-
const
|
|
16
|
+
const findings = [];
|
|
17
|
+
let passed = 0;
|
|
18
|
+
let total = 0;
|
|
19
|
+
|
|
20
|
+
const missingFile = (file) => mkFinding({
|
|
21
|
+
code: 'STR001',
|
|
22
|
+
validator: 'structure',
|
|
23
|
+
severity: 'error',
|
|
24
|
+
message: `Missing required file: ${file}`,
|
|
25
|
+
location: file,
|
|
26
|
+
suggestion: { kind: 'fix', text: 'Create it from the professional template', command: 'docguard init' },
|
|
27
|
+
});
|
|
11
28
|
|
|
12
29
|
// Check canonical docs
|
|
13
30
|
for (const file of config.requiredFiles.canonical) {
|
|
14
|
-
|
|
31
|
+
total++;
|
|
15
32
|
const fullPath = resolve(projectDir, file);
|
|
16
33
|
if (existsSync(fullPath)) {
|
|
17
|
-
|
|
34
|
+
passed++;
|
|
18
35
|
} else {
|
|
19
|
-
|
|
36
|
+
findings.push(missingFile(file));
|
|
20
37
|
}
|
|
21
38
|
}
|
|
22
39
|
|
|
@@ -27,44 +44,53 @@ export function validateStructure(projectDir, config) {
|
|
|
27
44
|
? config.requiredFiles.agentFile
|
|
28
45
|
: (typeof config.requiredFiles?.agentFile === 'string' ? [config.requiredFiles.agentFile] : []);
|
|
29
46
|
if (agentFiles.length > 0) {
|
|
30
|
-
|
|
47
|
+
total++;
|
|
31
48
|
const agentFileFound = agentFiles.some(f =>
|
|
32
49
|
existsSync(resolve(projectDir, f))
|
|
33
50
|
);
|
|
34
51
|
if (agentFileFound) {
|
|
35
|
-
|
|
52
|
+
passed++;
|
|
36
53
|
} else {
|
|
37
|
-
|
|
54
|
+
findings.push(mkFinding({
|
|
55
|
+
code: 'STR002',
|
|
56
|
+
validator: 'structure',
|
|
57
|
+
severity: 'error',
|
|
58
|
+
message: `Missing agent file: ${agentFiles.join(' or ')}`,
|
|
59
|
+
location: agentFiles[0],
|
|
60
|
+
suggestion: { kind: 'fix', text: 'Create the agent instructions file', command: 'docguard init' },
|
|
61
|
+
}));
|
|
38
62
|
}
|
|
39
63
|
}
|
|
40
64
|
|
|
41
65
|
// Check changelog — same defensive pattern.
|
|
42
66
|
const changelogPath = config.requiredFiles?.changelog;
|
|
43
67
|
if (changelogPath) {
|
|
44
|
-
|
|
68
|
+
total++;
|
|
45
69
|
if (existsSync(resolve(projectDir, changelogPath))) {
|
|
46
|
-
|
|
70
|
+
passed++;
|
|
47
71
|
} else {
|
|
48
|
-
|
|
72
|
+
findings.push(missingFile(changelogPath));
|
|
49
73
|
}
|
|
50
74
|
}
|
|
51
75
|
|
|
52
76
|
// Check drift log
|
|
53
|
-
|
|
77
|
+
total++;
|
|
54
78
|
if (existsSync(resolve(projectDir, config.requiredFiles.driftLog))) {
|
|
55
|
-
|
|
79
|
+
passed++;
|
|
56
80
|
} else {
|
|
57
|
-
|
|
81
|
+
findings.push(missingFile(config.requiredFiles.driftLog));
|
|
58
82
|
}
|
|
59
83
|
|
|
60
|
-
return
|
|
84
|
+
return { name: 'structure', ...resultFromFindings(findings, { passed, total }) };
|
|
61
85
|
}
|
|
62
86
|
|
|
63
87
|
/**
|
|
64
88
|
* Check that canonical doc files contain required sections
|
|
65
89
|
*/
|
|
66
90
|
export function validateDocSections(projectDir, config) {
|
|
67
|
-
const
|
|
91
|
+
const findings = [];
|
|
92
|
+
let passed = 0;
|
|
93
|
+
let total = 0;
|
|
68
94
|
const ptc = config.projectTypeConfig || {};
|
|
69
95
|
|
|
70
96
|
const requiredSections = {
|
|
@@ -86,7 +112,7 @@ export function validateDocSections(projectDir, config) {
|
|
|
86
112
|
const content = readFileSync(fullPath, 'utf-8');
|
|
87
113
|
|
|
88
114
|
for (const section of sections) {
|
|
89
|
-
|
|
115
|
+
total++;
|
|
90
116
|
// Match a real heading (H2–H6), not a substring in a TOC link or code
|
|
91
117
|
// block. v0.24: synonym- and section-number-tolerant via docHasSection, so
|
|
92
118
|
// arc42/C4 docs ("## 5.4 Layer boundaries", "## Building Block View")
|
|
@@ -110,19 +136,28 @@ export function validateDocSections(projectDir, config) {
|
|
|
110
136
|
'i'
|
|
111
137
|
);
|
|
112
138
|
if (docHasSection(content, section)) {
|
|
113
|
-
|
|
139
|
+
passed++;
|
|
114
140
|
} else if (naRe.test(content)) {
|
|
115
141
|
// v0.16-P7: explicit N/A — counts as passed (the project has owned
|
|
116
142
|
// the absence) and doesn't pollute the warnings list.
|
|
117
|
-
|
|
143
|
+
passed++;
|
|
118
144
|
} else {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
145
|
+
findings.push(mkFinding({
|
|
146
|
+
code: 'STR003',
|
|
147
|
+
validator: 'structure',
|
|
148
|
+
severity: 'warn',
|
|
149
|
+
message: `${file}: missing section "${section}". ` +
|
|
150
|
+
`If genuinely not applicable, add: <!-- docguard:section ${slug} n/a — your reason -->`,
|
|
151
|
+
location: file,
|
|
152
|
+
suggestion: {
|
|
153
|
+
kind: 'suppress',
|
|
154
|
+
text: 'Add the section, or own the absence with the inline N/A marker',
|
|
155
|
+
pragma: `<!-- docguard:section ${slug} n/a — your reason -->`,
|
|
156
|
+
},
|
|
157
|
+
}));
|
|
123
158
|
}
|
|
124
159
|
}
|
|
125
160
|
}
|
|
126
161
|
|
|
127
|
-
return
|
|
162
|
+
return { name: 'doc-sections', ...resultFromFindings(findings, { passed, total }) };
|
|
128
163
|
}
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
|
|
60
60
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
61
61
|
import { resolve, join, basename, extname, relative, dirname } from 'node:path';
|
|
62
|
+
import { mkFinding, resultFromFindings } from '../findings.mjs';
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
65
|
* Expand a simple glob (only supports `*` at the leaf segment level —
|
|
@@ -223,10 +224,13 @@ function extractDocumentedTokens(content) {
|
|
|
223
224
|
|
|
224
225
|
/**
|
|
225
226
|
* Validate surface drift for a single surface against its target docs.
|
|
226
|
-
* Returns
|
|
227
|
+
* Returns findings, fixes, passed count, and total count.
|
|
228
|
+
*
|
|
229
|
+
* v0.29: migrated to structured findings (SSY001–SSY002). Messages are
|
|
230
|
+
* byte-identical to the legacy strings.
|
|
227
231
|
*/
|
|
228
232
|
function checkSurface(projectDir, surface) {
|
|
229
|
-
const out = {
|
|
233
|
+
const out = { findings: [], fixes: [], passed: 0, total: 0 };
|
|
230
234
|
const name = surface.name || 'unnamed';
|
|
231
235
|
const extractor = surface.extract || 'basename-no-ext';
|
|
232
236
|
const ignore = new Set(surface.ignore || []);
|
|
@@ -236,9 +240,14 @@ function checkSurface(projectDir, surface) {
|
|
|
236
240
|
|
|
237
241
|
// Discover code-truth set from glob.
|
|
238
242
|
if (!surface.glob || typeof surface.glob !== 'string') {
|
|
239
|
-
out.
|
|
240
|
-
|
|
241
|
-
|
|
243
|
+
out.findings.push(mkFinding({
|
|
244
|
+
code: 'SSY001',
|
|
245
|
+
validator: 'surfaceSync',
|
|
246
|
+
severity: 'warn',
|
|
247
|
+
message: `surfaceSync: surface "${name}" has no \`glob\` — skipping. Add a glob like "cli/commands/*.mjs".`,
|
|
248
|
+
location: '.docguard.json',
|
|
249
|
+
suggestion: { kind: 'fix', text: `Add a \`glob\` to the "${name}" surface entry under surfaceSync.surfaces in .docguard.json` },
|
|
250
|
+
}));
|
|
242
251
|
return out;
|
|
243
252
|
}
|
|
244
253
|
const files = expandGlob(projectDir, surface.glob);
|
|
@@ -319,7 +328,14 @@ function checkSurface(projectDir, surface) {
|
|
|
319
328
|
const tail = extra > 0 ? ` (+${extra} more)` : '';
|
|
320
329
|
parts.push(`${missingFromCode.length} listed in ${docRel} but not found in code: ${shown}${tail}`);
|
|
321
330
|
}
|
|
322
|
-
out.
|
|
331
|
+
out.findings.push(mkFinding({
|
|
332
|
+
code: 'SSY002',
|
|
333
|
+
validator: 'surfaceSync',
|
|
334
|
+
severity: 'warn',
|
|
335
|
+
message: `Surface "${name}" drift: ${parts.join('; ')}`,
|
|
336
|
+
location: docRel,
|
|
337
|
+
suggestion: { kind: 'review', text: `Update the "${name}" list/table in ${docRel} to match the code-truth set — add missing items, remove ghosts, or put aliases in the surface's ignore list` },
|
|
338
|
+
}));
|
|
323
339
|
}
|
|
324
340
|
|
|
325
341
|
return out;
|
|
@@ -347,21 +363,24 @@ export function validateSurfaceSync(projectDir, config) {
|
|
|
347
363
|
? config.surfaceSync.surfaces
|
|
348
364
|
: [];
|
|
349
365
|
|
|
350
|
-
const result = { errors: [], warnings: [], fixes: [], passed: 0, total: 0 };
|
|
351
|
-
|
|
352
366
|
if (surfaceCfg.length === 0) {
|
|
353
367
|
// No surfaces configured → N/A. The validator infrastructure surfaces
|
|
354
|
-
// this as "nothing to validate" rather than a fail.
|
|
355
|
-
return
|
|
368
|
+
// this as "nothing to validate" rather than a fail. Legacy shape kept.
|
|
369
|
+
return { errors: [], warnings: [], fixes: [], passed: 0, total: 0 };
|
|
356
370
|
}
|
|
357
371
|
|
|
372
|
+
const findings = [];
|
|
373
|
+
const fixes = [];
|
|
374
|
+
let passed = 0;
|
|
375
|
+
let total = 0;
|
|
376
|
+
|
|
358
377
|
for (const surface of surfaceCfg) {
|
|
359
378
|
const r = checkSurface(projectDir, surface);
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
379
|
+
findings.push(...r.findings);
|
|
380
|
+
fixes.push(...r.fixes);
|
|
381
|
+
passed += r.passed;
|
|
382
|
+
total += r.total;
|
|
364
383
|
}
|
|
365
384
|
|
|
366
|
-
return
|
|
385
|
+
return { ...resultFromFindings(findings, { passed, total }), fixes };
|
|
367
386
|
}
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Test Spec Validator — Checks that tests exist per TEST-SPEC.md coverage rules
|
|
3
3
|
* Now respects projectTypeConfig (e.g., skip E2E for CLI tools)
|
|
4
|
+
*
|
|
5
|
+
* v0.29: migrated to structured findings (TSP001–TSP007). Messages are
|
|
6
|
+
* byte-identical to the legacy strings — resultFromFindings derives the
|
|
7
|
+
* errors/warnings arrays from the same findings, so counts, exit codes, and
|
|
8
|
+
* existing tests are unaffected; guard just renders richer output.
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
7
12
|
import { resolve } from 'node:path';
|
|
8
13
|
import { resolveSourceRoots } from '../shared-source.mjs';
|
|
14
|
+
import { mkFinding, resultFromFindings } from '../findings.mjs';
|
|
9
15
|
|
|
10
16
|
export function validateTestSpec(projectDir, config) {
|
|
11
|
-
const
|
|
17
|
+
const findings = [];
|
|
18
|
+
let passed = 0;
|
|
19
|
+
let total = 0;
|
|
20
|
+
let note;
|
|
12
21
|
|
|
13
|
-
const
|
|
22
|
+
const specDoc = 'docs-canonical/TEST-SPEC.md';
|
|
23
|
+
const testSpecPath = resolve(projectDir, specDoc);
|
|
14
24
|
if (!existsSync(testSpecPath)) {
|
|
15
|
-
|
|
25
|
+
// Structure validator catches this. Keep the exact legacy shape here
|
|
26
|
+
// (no `findings` key) — tests deep-equal this early return.
|
|
27
|
+
return { name: 'test-spec', errors: [], warnings: [], passed: 0, total: 0 };
|
|
16
28
|
}
|
|
17
29
|
|
|
18
30
|
const content = readFileSync(testSpecPath, 'utf-8');
|
|
@@ -80,22 +92,44 @@ export function validateTestSpec(projectDir, config) {
|
|
|
80
92
|
// author's CLAIM, not proof — it is NOT counted as a pass. The real pass
|
|
81
93
|
// comes from the file-existence checks below (code truth, not the glyph).
|
|
82
94
|
if (status.includes('❌')) {
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
total++;
|
|
96
|
+
findings.push(mkFinding({
|
|
97
|
+
code: 'TSP001',
|
|
98
|
+
validator: 'testSpec',
|
|
99
|
+
severity: 'warn',
|
|
100
|
+
message: `TEST-SPEC declares ${sourceFile} as ❌ — missing tests`,
|
|
101
|
+
location: specDoc,
|
|
102
|
+
suggestion: { kind: 'fix', text: 'Write the missing tests, then update the row status to ✅' },
|
|
103
|
+
}));
|
|
85
104
|
} else if (status.includes('⚠️')) {
|
|
86
|
-
|
|
87
|
-
|
|
105
|
+
total++;
|
|
106
|
+
findings.push(mkFinding({
|
|
107
|
+
code: 'TSP002',
|
|
108
|
+
validator: 'testSpec',
|
|
109
|
+
severity: 'warn',
|
|
110
|
+
message: `TEST-SPEC declares ${sourceFile} as ⚠️ — partial coverage`,
|
|
111
|
+
location: specDoc,
|
|
112
|
+
suggestion: { kind: 'fix', text: 'Extend coverage for this source, then update the row status to ✅' },
|
|
113
|
+
}));
|
|
88
114
|
}
|
|
89
115
|
|
|
90
116
|
// ── File existence checks ───────────────────────────────────────
|
|
91
117
|
// Verify source file still exists (catch stale map entries).
|
|
92
118
|
const cleanSource = sourceFile.replace(/`/g, '').trim();
|
|
93
119
|
if (cleanSource && cleanSource !== '—' && cleanSource !== 'Source File' && isPathLike(cleanSource)) {
|
|
94
|
-
|
|
120
|
+
total++;
|
|
95
121
|
if (existsSync(resolve(projectDir, cleanSource))) {
|
|
96
|
-
|
|
122
|
+
passed++;
|
|
97
123
|
} else {
|
|
98
|
-
|
|
124
|
+
findings.push(mkFinding({
|
|
125
|
+
code: 'TSP003',
|
|
126
|
+
validator: 'testSpec',
|
|
127
|
+
severity: 'warn',
|
|
128
|
+
confidence: 'low',
|
|
129
|
+
message: `Source-to-Test Map: source file \`${cleanSource}\` not found on disk — stale entry?`,
|
|
130
|
+
location: specDoc,
|
|
131
|
+
suggestion: { kind: 'review', text: 'Update or remove the stale row if the source file moved or was deleted' },
|
|
132
|
+
}));
|
|
99
133
|
}
|
|
100
134
|
}
|
|
101
135
|
|
|
@@ -104,11 +138,18 @@ export function validateTestSpec(projectDir, config) {
|
|
|
104
138
|
for (const ti of testIdxs) {
|
|
105
139
|
const cleanTest = (cells[ti] || '').replace(/`/g, '').trim();
|
|
106
140
|
if (isPlaceholder(cleanTest) || !isPathLike(cleanTest)) continue;
|
|
107
|
-
|
|
141
|
+
total++;
|
|
108
142
|
if (existsSync(resolve(projectDir, cleanTest))) {
|
|
109
|
-
|
|
143
|
+
passed++;
|
|
110
144
|
} else {
|
|
111
|
-
|
|
145
|
+
findings.push(mkFinding({
|
|
146
|
+
code: 'TSP004',
|
|
147
|
+
validator: 'testSpec',
|
|
148
|
+
severity: 'warn',
|
|
149
|
+
message: `Source-to-Test Map: test file \`${cleanTest}\` not found — referenced by ${cleanSource}`,
|
|
150
|
+
location: specDoc,
|
|
151
|
+
suggestion: { kind: 'fix', text: 'Create the test file, or point the row at the actual test path' },
|
|
152
|
+
}));
|
|
112
153
|
}
|
|
113
154
|
}
|
|
114
155
|
}
|
|
@@ -140,10 +181,15 @@ export function validateTestSpec(projectDir, config) {
|
|
|
140
181
|
if (num.startsWith('<!--') || num === '#' || journey.startsWith('<!--')) continue;
|
|
141
182
|
|
|
142
183
|
if (status && status.includes('❌')) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
184
|
+
total++;
|
|
185
|
+
findings.push(mkFinding({
|
|
186
|
+
code: 'TSP005',
|
|
187
|
+
validator: 'testSpec',
|
|
188
|
+
severity: 'warn',
|
|
189
|
+
message: `E2E Journey #${num} (${journey}) — missing test: ${testFile}`,
|
|
190
|
+
location: specDoc,
|
|
191
|
+
suggestion: { kind: 'fix', text: 'Implement the journey test, then update the row status to ✅' },
|
|
192
|
+
}));
|
|
147
193
|
continue;
|
|
148
194
|
}
|
|
149
195
|
|
|
@@ -154,14 +200,19 @@ export function validateTestSpec(projectDir, config) {
|
|
|
154
200
|
if (testFile && testFile.trim() !== '—' && !testFile.includes('N/A')) {
|
|
155
201
|
const paths = parseTestPathCell(testFile);
|
|
156
202
|
if (paths.length > 0) {
|
|
157
|
-
|
|
203
|
+
total++;
|
|
158
204
|
const anyExists = paths.some(p => testEvidenceExists(projectDir, p));
|
|
159
205
|
if (anyExists) {
|
|
160
|
-
|
|
206
|
+
passed++;
|
|
161
207
|
} else {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
208
|
+
findings.push(mkFinding({
|
|
209
|
+
code: 'TSP006',
|
|
210
|
+
validator: 'testSpec',
|
|
211
|
+
severity: 'warn',
|
|
212
|
+
message: `E2E Journey #${num} (${journey}) marked ✅ but test file not found: ${paths.join(', ')}`,
|
|
213
|
+
location: specDoc,
|
|
214
|
+
suggestion: { kind: 'review', text: 'Fix the test path in the row, or restore the missing test file' },
|
|
215
|
+
}));
|
|
165
216
|
}
|
|
166
217
|
}
|
|
167
218
|
}
|
|
@@ -172,7 +223,7 @@ export function validateTestSpec(projectDir, config) {
|
|
|
172
223
|
// If TEST-SPEC.md declared no service-to-test mappings, there is nothing to
|
|
173
224
|
// verify against. Do NOT manufacture a 1/1 pass just because tests exist
|
|
174
225
|
// somewhere — that rendered a confident green ✅ for a doc that mapped nothing.
|
|
175
|
-
if (
|
|
226
|
+
if (total === 0) {
|
|
176
227
|
// 1. Check top-level test dirs
|
|
177
228
|
const commonTestDirs = ['tests', 'test', '__tests__', 'spec'];
|
|
178
229
|
const hasTestDir = commonTestDirs.some(d =>
|
|
@@ -200,16 +251,23 @@ export function validateTestSpec(projectDir, config) {
|
|
|
200
251
|
// file, and the last as status — so both the minimal 3-column shape and
|
|
201
252
|
// the 4-column table `docguard generate` emits are accepted. Say so, since
|
|
202
253
|
// the guidance previously contradicted the generated skeleton (field report).
|
|
203
|
-
|
|
254
|
+
note = 'TEST-SPEC.md declares no service-to-test mappings. Add a "## Source-to-Test Map" table — column 1 is the source, column 2 the test file, the last column the status. Both `| Source | Test file | Status |` and the generated `| Source File | Unit Test | Integration Test | Status |` shapes work. Run `docguard explain testSpec` for details.';
|
|
204
255
|
} else {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
256
|
+
findings.push(mkFinding({
|
|
257
|
+
code: 'TSP007',
|
|
258
|
+
validator: 'testSpec',
|
|
259
|
+
severity: 'warn',
|
|
260
|
+
message: 'No test directory or co-located test files found. ' +
|
|
261
|
+
'Expected: tests/, src/**/__tests__/, or src/**/*.test.* files',
|
|
262
|
+
location: null,
|
|
263
|
+
suggestion: { kind: 'fix', text: 'Create a tests/ directory or co-located *.test.* files, then map them in TEST-SPEC.md' },
|
|
264
|
+
}));
|
|
209
265
|
}
|
|
210
266
|
}
|
|
211
267
|
|
|
212
|
-
|
|
268
|
+
const res = { name: 'test-spec', ...resultFromFindings(findings, { passed, total }) };
|
|
269
|
+
if (note) res.note = note;
|
|
270
|
+
return res;
|
|
213
271
|
}
|
|
214
272
|
|
|
215
273
|
/**
|