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.
Files changed (74) hide show
  1. package/README.es.md +102 -0
  2. package/README.md +65 -31
  3. package/README.pt-BR.md +101 -0
  4. package/STANDARD.md +20 -10
  5. package/cli/commands/agents.mjs +149 -0
  6. package/cli/commands/diff.mjs +6 -15
  7. package/cli/commands/explain.mjs +8 -6
  8. package/cli/commands/generate.mjs +14 -1001
  9. package/cli/commands/guard.mjs +149 -15
  10. package/cli/commands/init.mjs +23 -1
  11. package/cli/commands/llms.mjs +67 -5
  12. package/cli/commands/mcp.mjs +263 -0
  13. package/cli/commands/memory.mjs +115 -0
  14. package/cli/commands/score.mjs +76 -12
  15. package/cli/commands/sync-tests.mjs +272 -0
  16. package/cli/commands/sync.mjs +6 -0
  17. package/cli/commands/verify.mjs +67 -0
  18. package/cli/docguard.mjs +62 -5
  19. package/cli/findings.mjs +499 -0
  20. package/cli/scanners/agent-readability.mjs +202 -0
  21. package/cli/scanners/semantic-claims.mjs +160 -0
  22. package/cli/scanners/speckit.mjs +98 -28
  23. package/cli/shared-ignore.mjs +148 -16
  24. package/cli/shared.mjs +45 -1
  25. package/cli/validators/api-surface.mjs +182 -29
  26. package/cli/validators/architecture.mjs +91 -56
  27. package/cli/validators/canonical-sync.mjs +59 -28
  28. package/cli/validators/changelog.mjs +41 -17
  29. package/cli/validators/cross-reference.mjs +28 -11
  30. package/cli/validators/doc-quality.mjs +78 -44
  31. package/cli/validators/docs-coverage.mjs +90 -63
  32. package/cli/validators/docs-diff.mjs +63 -64
  33. package/cli/validators/docs-sync.mjs +48 -33
  34. package/cli/validators/drift.mjs +40 -34
  35. package/cli/validators/environment.mjs +67 -27
  36. package/cli/validators/freshness.mjs +12 -5
  37. package/cli/validators/generated-staleness.mjs +26 -10
  38. package/cli/validators/metadata-sync.mjs +28 -25
  39. package/cli/validators/metrics-consistency.mjs +89 -47
  40. package/cli/validators/schema-sync.mjs +37 -32
  41. package/cli/validators/security.mjs +7 -20
  42. package/cli/validators/spec-kit.mjs +3 -0
  43. package/cli/validators/structure.mjs +58 -23
  44. package/cli/validators/surface-sync.mjs +34 -15
  45. package/cli/validators/test-spec.mjs +87 -29
  46. package/cli/validators/todo-tracking.mjs +83 -74
  47. package/cli/validators/traceability.mjs +67 -39
  48. package/cli/writers/doc-generators.mjs +853 -0
  49. package/cli/writers/generate-io.mjs +142 -0
  50. package/cli/writers/sarif.mjs +129 -0
  51. package/commands/docguard.fix.md +56 -53
  52. package/commands/docguard.guard.md +53 -47
  53. package/commands/docguard.review.md +49 -31
  54. package/docs/ai-integration.md +133 -134
  55. package/docs/commands.md +49 -3
  56. package/docs/configuration.md +38 -0
  57. package/docs/faq.md +15 -0
  58. package/extensions/spec-kit-docguard/extension.yml +1 -1
  59. package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
  60. package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
  61. package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
  62. package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
  63. package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +2 -2
  64. package/package.json +1 -1
  65. package/schemas/docguard-config.schema.json +17 -0
  66. package/templates/ENVIRONMENT.md.template +5 -0
  67. package/templates/REQUIREMENTS.md.template +2 -0
  68. package/templates/SECURITY.md.template +6 -1
  69. package/templates/TEST-SPEC.md.template +5 -0
  70. package/templates/commands/docguard.fix.md +33 -10
  71. package/templates/commands/docguard.guard.md +40 -26
  72. package/templates/commands/docguard.init.md +23 -11
  73. package/templates/commands/docguard.review.md +25 -8
  74. package/templates/commands/docguard.update.md +14 -4
@@ -21,7 +21,8 @@
21
21
  */
22
22
 
23
23
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
24
- import { resolve, join, extname } from 'node:path';
24
+ import { resolve, join, extname, relative } from 'node:path';
25
+ import { mkFinding, resultFromFindings } from '../findings.mjs';
25
26
 
26
27
  // ──── Metric Thresholds ────
27
28
  // These define "good" vs "warning" boundaries for each metric.
@@ -548,13 +549,21 @@ function analyzeDocument(doc) {
548
549
  *
549
550
  * Scans all canonical docs, runs 8 metrics on each, and reports
550
551
  * per-doc findings as warnings when thresholds are exceeded.
552
+ *
553
+ * v0.29: migrated to structured findings (DQ001–DQ008, one code per metric).
554
+ * Messages are byte-identical to the legacy strings — resultFromFindings
555
+ * derives the errors/warnings arrays from the same findings, so counts, exit
556
+ * codes, and existing tests are unaffected; guard just renders richer output.
551
557
  */
552
558
  export function validateDocQuality(projectDir, config) {
553
- const results = { errors: [], warnings: [], passed: 0, total: 0 };
559
+ const findings = [];
560
+ let passed = 0;
561
+ let total = 0;
554
562
 
555
563
  const docs = getCanonicalDocs(projectDir);
556
564
  if (docs.length === 0) {
557
- return results;
565
+ // Literal legacy shape (no findings key) — tests deepEqual this object.
566
+ return { errors: [], warnings: [], passed: 0, total: 0 };
558
567
  }
559
568
 
560
569
  for (const doc of docs) {
@@ -564,107 +573,132 @@ export function validateDocQuality(projectDir, config) {
564
573
  if (analysis.skipped) continue;
565
574
 
566
575
  const m = analysis.metrics;
576
+ const loc = relative(projectDir, doc.path);
577
+ const warn = (code, message, suggestion) => {
578
+ findings.push(mkFinding({
579
+ code,
580
+ validator: 'docQuality',
581
+ severity: 'warn',
582
+ message,
583
+ location: loc,
584
+ suggestion,
585
+ }));
586
+ };
567
587
 
568
588
  // ── Check 1: Passive Voice ──
569
- results.total++;
589
+ total++;
570
590
  const passiveOv = analysis.overrides?.passiveVoice;
571
591
  const passiveThreshold = passiveOv?.threshold
572
592
  ?? config.docQuality?.passiveVoiceThreshold
573
593
  ?? THRESHOLDS.passiveVoiceRatio.warn;
574
594
  if (passiveOv?.off || m.passiveVoiceRatio <= passiveThreshold) {
575
- results.passed++;
595
+ passed++;
576
596
  } else {
577
- results.warnings.push(
597
+ warn('DQ001',
578
598
  `${doc.name}: High passive voice ratio (${(m.passiveVoiceRatio * 100).toFixed(0)}% of sentences). ` +
579
599
  `Use active voice for clarity. Found ${analysis.details.passive.count}/${analysis.details.passive.total} passive sentences. ` +
580
- `If the passive voice is intentional (sequence/flow doc), add: <!-- docguard:quality passive-voice off — your reason -->`
581
- );
600
+ `If the passive voice is intentional (sequence/flow doc), add: <!-- docguard:quality passive-voice off — your reason -->`,
601
+ {
602
+ kind: 'suppress',
603
+ text: 'Rewrite in active voice, or opt this doc out if passive is intentional',
604
+ pragma: '<!-- docguard:quality passive-voice off — your reason -->',
605
+ });
582
606
  }
583
607
 
584
608
  // ── Check 2: Ambiguous Pronouns ──
585
- results.total++;
609
+ total++;
586
610
  if (m.ambiguousPronounRatio <= THRESHOLDS.ambiguousPronounRatio.warn) {
587
- results.passed++;
611
+ passed++;
588
612
  } else {
589
- results.warnings.push(
613
+ warn('DQ002',
590
614
  `${doc.name}: High ambiguous pronoun ratio (${(m.ambiguousPronounRatio * 100).toFixed(1)}%). ` +
591
- `Replace "it/this/that/they" with specific nouns for clarity`
592
- );
615
+ `Replace "it/this/that/they" with specific nouns for clarity`,
616
+ { kind: 'fix', text: 'Replace vague pronouns with the specific noun they refer to' });
593
617
  }
594
618
 
595
619
  // ── Check 3: Atomicity ──
596
- results.total++;
620
+ total++;
597
621
  if (m.atomicityScore <= THRESHOLDS.atomicityScore.warn) {
598
- results.passed++;
622
+ passed++;
599
623
  } else {
600
- results.warnings.push(
624
+ warn('DQ003',
601
625
  `${doc.name}: Low atomicity (${(m.atomicityScore * 100).toFixed(0)}% compound sentences). ` +
602
- `Split compound sentences for easier verification (IEEE 830 §4.1)`
603
- );
626
+ `Split compound sentences for easier verification (IEEE 830 §4.1)`,
627
+ { kind: 'fix', text: 'Split compound sentences into one statement per sentence' });
604
628
  }
605
629
 
606
630
  // ── Check 4: Flesch Reading Ease ──
607
- results.total++;
631
+ total++;
608
632
  if (m.fleschReadingEase >= THRESHOLDS.fleschReadingEase.warn) {
609
- results.passed++;
633
+ passed++;
610
634
  } else {
611
- results.warnings.push(
635
+ warn('DQ004',
612
636
  `${doc.name}: Very low readability (Flesch score: ${m.fleschReadingEase}/100 — ${getReadabilityLabel(m.fleschReadingEase)}). ` +
613
- `Shorten sentences and use simpler words`
614
- );
637
+ `Shorten sentences and use simpler words`,
638
+ { kind: 'fix', text: 'Shorten sentences and prefer simpler words' });
615
639
  }
616
640
 
617
641
  // ── Check 5: Flesch-Kincaid Grade ──
618
- results.total++;
642
+ total++;
619
643
  if (m.fleschKincaidGrade <= THRESHOLDS.fleschKincaidGrade.warn) {
620
- results.passed++;
644
+ passed++;
621
645
  } else {
622
- results.warnings.push(
646
+ warn('DQ005',
623
647
  `${doc.name}: Reading level too high (grade ${m.fleschKincaidGrade} — ${getGradeLabel(m.fleschKincaidGrade)}). ` +
624
- `Aim for grade 12-16 for technical docs`
625
- );
648
+ `Aim for grade 12-16 for technical docs`,
649
+ { kind: 'fix', text: 'Simplify the prose toward a grade 12-16 reading level' });
626
650
  }
627
651
 
628
652
  // ── Check 6: Sentence Length ──
629
- results.total++;
653
+ total++;
630
654
  if (m.avgSentenceLength <= THRESHOLDS.avgSentenceLength.warn) {
631
- results.passed++;
655
+ passed++;
632
656
  } else {
633
- results.warnings.push(
657
+ warn('DQ006',
634
658
  `${doc.name}: Average sentence too long (${m.avgSentenceLength} words). ` +
635
- `Target ≤30 words per sentence for readability`
636
- );
659
+ `Target ≤30 words per sentence for readability`,
660
+ { kind: 'fix', text: 'Break long sentences up — target 30 words or fewer' });
637
661
  }
638
662
 
639
663
  // ── Check 7: Negation Load ──
640
664
  // Per-doc override (security/operational docs legitimately use "never",
641
665
  // "must not", "cannot") and a project-wide config threshold both honored.
642
- results.total++;
666
+ total++;
643
667
  const negOv = analysis.overrides?.negationLoad;
644
668
  const negThreshold = negOv?.threshold
645
669
  ?? config.docQuality?.negationLoadThreshold
646
670
  ?? THRESHOLDS.negationLoad.warn;
647
671
  if (negOv?.off || m.negationLoad <= negThreshold) {
648
- results.passed++;
672
+ passed++;
649
673
  } else {
650
- results.warnings.push(
674
+ warn('DQ007',
651
675
  `${doc.name}: High negation load (${(m.negationLoad * 100).toFixed(0)}% of sentences use negation). ` +
652
676
  `Rephrase in positive terms: "must not fail" → "must succeed" (IEEE 830 §4.3). ` +
653
- `If the negation is intentional, add: <!-- docguard:quality negation-load off — your reason -->`
654
- );
677
+ `If the negation is intentional, add: <!-- docguard:quality negation-load off — your reason -->`,
678
+ {
679
+ kind: 'suppress',
680
+ text: 'Rephrase in positive terms, or opt this doc out if the negation is intentional',
681
+ pragma: '<!-- docguard:quality negation-load off — your reason -->',
682
+ });
655
683
  }
656
684
 
657
685
  // ── Check 8: Conditional Load ──
658
- results.total++;
686
+ total++;
659
687
  if (m.conditionalLoad <= THRESHOLDS.conditionalLoad.warn) {
660
- results.passed++;
688
+ passed++;
661
689
  } else {
662
- results.warnings.push(
690
+ warn('DQ008',
663
691
  `${doc.name}: High conditional load (${(m.conditionalLoad * 100).toFixed(0)}% of sentences are conditional). ` +
664
- `Simplify by splitting conditionals into separate requirements`
665
- );
692
+ `Simplify by splitting conditionals into separate requirements`,
693
+ { kind: 'fix', text: 'Split conditional sentences into separate, unconditional requirements' });
666
694
  }
667
695
  }
668
696
 
669
- return results;
697
+ if (total === 0) {
698
+ // Every doc was skipped (insufficient prose) — same literal legacy shape,
699
+ // because tests deepEqual this exact object for the all-skipped case too.
700
+ return { errors: [], warnings: [], passed: 0, total: 0 };
701
+ }
702
+
703
+ return resultFromFindings(findings, { passed, total });
670
704
  }
@@ -11,13 +11,19 @@
11
11
  * - package.json bin entries not documented
12
12
  * - Source directories not referenced in ARCHITECTURE.md
13
13
  * - README.md missing standard sections (inspired by Standard README spec)
14
+ *
15
+ * v0.29: migrated to structured findings (DCV001–DCV006). Messages are
16
+ * byte-identical to the legacy strings — resultFromFindings derives the
17
+ * errors/warnings arrays from the same findings, so counts, exit codes, and
18
+ * existing tests are unaffected; guard just renders richer output.
14
19
  */
15
20
 
16
21
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
17
22
  import { resolve, join, relative, basename, extname } from 'node:path';
18
23
  import { resolveSourceRoots } from '../shared-source.mjs';
19
- import { shouldIgnore } from '../shared-ignore.mjs';
24
+ import { shouldIgnore, walkFiles as sharedWalkFiles } from '../shared-ignore.mjs';
20
25
  import { detectIaC, hasInfrastructureHeading, buildIaCWarning } from '../scanners/iac.mjs';
26
+ import { mkFinding, resultFromFindings } from '../findings.mjs';
21
27
 
22
28
  const IGNORE_DIRS = new Set([
23
29
  'node_modules', '.git', '.next', '.nuxt', 'dist', 'build', 'out',
@@ -56,14 +62,15 @@ function isGeneratedArtifact(name) {
56
62
  * @returns {{ errors: string[], warnings: string[], passed: number, total: number }}
57
63
  */
58
64
  export function validateDocsCoverage(projectDir, config) {
59
- const warnings = [];
65
+ const findings = [];
60
66
  let passed = 0;
61
67
  let total = 0;
62
68
 
63
69
  // Collect all doc content for searching
64
70
  const allDocContent = collectDocContent(projectDir);
65
71
  if (!allDocContent) {
66
- return { errors: [], warnings, passed: 0, total: 0 };
72
+ // Literal legacy shape (no findings key) tests deepEqual this exact object.
73
+ return { errors: [], warnings: [], passed: 0, total: 0 };
67
74
  }
68
75
 
69
76
  // IaC detection runs once and informs both Check 3 (suppression) and
@@ -74,39 +81,39 @@ export function validateDocsCoverage(projectDir, config) {
74
81
  const configChecks = checkConfigFiles(projectDir, allDocContent, config);
75
82
  total += configChecks.total;
76
83
  passed += configChecks.passed;
77
- warnings.push(...configChecks.warnings);
84
+ findings.push(...configChecks.findings);
78
85
 
79
86
  // ── Check 2: package.json bin entries documented ──
80
87
  const binChecks = checkPackageBins(projectDir, allDocContent);
81
88
  total += binChecks.total;
82
89
  passed += binChecks.passed;
83
- warnings.push(...binChecks.warnings);
90
+ findings.push(...binChecks.findings);
84
91
 
85
92
  // ── Check 3: Source directory structure matches ARCHITECTURE.md ──
86
93
  const dirChecks = checkSourceDirs(projectDir, allDocContent, config, iac);
87
94
  total += dirChecks.total;
88
95
  passed += dirChecks.passed;
89
- warnings.push(...dirChecks.warnings);
96
+ findings.push(...dirChecks.findings);
90
97
 
91
98
  // ── Check 4: Config filenames referenced in source code but not documented ──
92
99
  const codeConfigChecks = checkCodeReferencedConfigs(projectDir, allDocContent, config);
93
100
  total += codeConfigChecks.total;
94
101
  passed += codeConfigChecks.passed;
95
- warnings.push(...codeConfigChecks.warnings);
102
+ findings.push(...codeConfigChecks.findings);
96
103
 
97
104
  // ── Check 5: README section completeness (Standard README spec) ──
98
105
  const readmeChecks = checkReadmeSections(projectDir);
99
106
  total += readmeChecks.total;
100
107
  passed += readmeChecks.passed;
101
- warnings.push(...readmeChecks.warnings);
108
+ findings.push(...readmeChecks.findings);
102
109
 
103
110
  // ── Check 6: IaC-aware Infrastructure documentation ──
104
111
  const iacChecks = checkIaCDocumentation(projectDir, iac);
105
112
  total += iacChecks.total;
106
113
  passed += iacChecks.passed;
107
- warnings.push(...iacChecks.warnings);
114
+ findings.push(...iacChecks.findings);
108
115
 
109
- return { errors: [], warnings, passed, total };
116
+ return resultFromFindings(findings, { passed, total });
110
117
  }
111
118
 
112
119
  // ── Check Functions ─────────────────────────────────────────────────────────
@@ -118,12 +125,12 @@ export function validateDocsCoverage(projectDir, config) {
118
125
  * consistently across all docs-coverage checks).
119
126
  */
120
127
  function checkConfigFiles(projectDir, allDocContent, config = {}) {
121
- const warnings = [];
128
+ const findings = [];
122
129
  let passed = 0;
123
130
  let total = 0;
124
131
 
125
132
  let entries;
126
- try { entries = readdirSync(projectDir); } catch { return { warnings, passed, total }; }
133
+ try { entries = readdirSync(projectDir); } catch { return { findings, passed, total }; }
127
134
 
128
135
  const lowerDocContent = allDocContent.toLowerCase();
129
136
 
@@ -155,28 +162,33 @@ function checkConfigFiles(projectDir, allDocContent, config = {}) {
155
162
  if (lowerDocContent.includes(entry.toLowerCase())) {
156
163
  passed++;
157
164
  } else {
158
- warnings.push(
159
- `Config file "${entry}" exists but is not mentioned in any documentation. Document its purpose in ARCHITECTURE.md or README.md`
160
- );
165
+ findings.push(mkFinding({
166
+ code: 'DCV001',
167
+ validator: 'docsCoverage',
168
+ severity: 'warn',
169
+ message: `Config file "${entry}" exists but is not mentioned in any documentation. Document its purpose in ARCHITECTURE.md or README.md`,
170
+ location: entry,
171
+ suggestion: { kind: 'fix', text: 'Explain what this config file does in ARCHITECTURE.md or README.md' },
172
+ }));
161
173
  }
162
174
  }
163
175
 
164
- return { warnings, passed, total };
176
+ return { findings, passed, total };
165
177
  }
166
178
 
167
179
  /**
168
180
  * Check 2: package.json bin entries (CLI commands users run) are documented.
169
181
  */
170
182
  function checkPackageBins(projectDir, allDocContent) {
171
- const warnings = [];
183
+ const findings = [];
172
184
  let passed = 0;
173
185
  let total = 0;
174
186
 
175
187
  const pkgPath = resolve(projectDir, 'package.json');
176
- if (!existsSync(pkgPath)) return { warnings, passed, total };
188
+ if (!existsSync(pkgPath)) return { findings, passed, total };
177
189
 
178
190
  let pkg;
179
- try { pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); } catch { return { warnings, passed, total }; }
191
+ try { pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); } catch { return { findings, passed, total }; }
180
192
 
181
193
  const bins = typeof pkg.bin === 'string'
182
194
  ? { [pkg.name]: pkg.bin }
@@ -189,13 +201,18 @@ function checkPackageBins(projectDir, allDocContent) {
189
201
  if (lowerDocContent.includes(binName.toLowerCase())) {
190
202
  passed++;
191
203
  } else {
192
- warnings.push(
193
- `package.json defines CLI command "${binName}" but it's not mentioned in any documentation`
194
- );
204
+ findings.push(mkFinding({
205
+ code: 'DCV002',
206
+ validator: 'docsCoverage',
207
+ severity: 'warn',
208
+ message: `package.json defines CLI command "${binName}" but it's not mentioned in any documentation`,
209
+ location: 'package.json',
210
+ suggestion: { kind: 'fix', text: `Document the "${binName}" command in README.md (e.g. under Usage)` },
211
+ }));
195
212
  }
196
213
  }
197
214
 
198
- return { warnings, passed, total };
215
+ return { findings, passed, total };
199
216
  }
200
217
 
201
218
  /**
@@ -207,15 +224,15 @@ function checkPackageBins(projectDir, allDocContent) {
207
224
  * instead (FR-011).
208
225
  */
209
226
  function checkSourceDirs(projectDir, allDocContent, config = {}, iac = { isIaC: false, tools: [] }) {
210
- const warnings = [];
227
+ const findings = [];
211
228
  let passed = 0;
212
229
  let total = 0;
213
230
 
214
231
  const archPath = resolve(projectDir, 'docs-canonical/ARCHITECTURE.md');
215
- if (!existsSync(archPath)) return { warnings, passed, total };
232
+ if (!existsSync(archPath)) return { findings, passed, total };
216
233
 
217
234
  let archContent;
218
- try { archContent = readFileSync(archPath, 'utf-8'); } catch { return { warnings, passed, total }; }
235
+ try { archContent = readFileSync(archPath, 'utf-8'); } catch { return { findings, passed, total }; }
219
236
 
220
237
  const lowerArchContent = archContent.toLowerCase();
221
238
  const infraDocumented = hasInfrastructureHeading(archContent);
@@ -264,14 +281,19 @@ function checkSourceDirs(projectDir, allDocContent, config = {}, iac = { isIaC:
264
281
  if (lowerArchContent.includes(searchName) || lowerArchContent.includes(root + '/' + entry)) {
265
282
  passed++;
266
283
  } else {
267
- warnings.push(
268
- `Source directory "${root}/${entry}/" is not referenced in ARCHITECTURE.md`
269
- );
284
+ findings.push(mkFinding({
285
+ code: 'DCV003',
286
+ validator: 'docsCoverage',
287
+ severity: 'warn',
288
+ message: `Source directory "${root}/${entry}/" is not referenced in ARCHITECTURE.md`,
289
+ location: relPath,
290
+ suggestion: { kind: 'fix', text: 'Add this directory to the Component Map in docs-canonical/ARCHITECTURE.md' },
291
+ }));
270
292
  }
271
293
  }
272
294
  }
273
295
 
274
- return { warnings, passed, total };
296
+ return { findings, passed, total };
275
297
  }
276
298
 
277
299
  /**
@@ -310,30 +332,37 @@ function isInsideIaCPackage(relPath, packageDirs) {
310
332
  * warnings that would otherwise fire for bin/, lib/, modules/, handlers/, etc.
311
333
  */
312
334
  function checkIaCDocumentation(projectDir, iac) {
313
- const warnings = [];
314
- if (!iac || !iac.isIaC) return { warnings, passed: 0, total: 0 };
335
+ const findings = [];
336
+ if (!iac || !iac.isIaC) return { findings, passed: 0, total: 0 };
315
337
 
316
338
  const archPath = resolve(projectDir, 'docs-canonical/ARCHITECTURE.md');
317
339
  if (!existsSync(archPath)) {
318
340
  // No ARCHITECTURE.md at all — structure validator will catch that.
319
341
  // Don't double-warn here.
320
- return { warnings, passed: 0, total: 0 };
342
+ return { findings, passed: 0, total: 0 };
321
343
  }
322
344
 
323
345
  let archContent;
324
- try { archContent = readFileSync(archPath, 'utf-8'); } catch { return { warnings, passed: 0, total: 0 }; }
346
+ try { archContent = readFileSync(archPath, 'utf-8'); } catch { return { findings, passed: 0, total: 0 }; }
325
347
 
326
348
  if (hasInfrastructureHeading(archContent)) {
327
349
  // One pass per tool — counted as total per IaC tool present.
328
- return { warnings, passed: iac.tools.length, total: iac.tools.length };
350
+ return { findings, passed: iac.tools.length, total: iac.tools.length };
329
351
  }
330
352
 
331
353
  // One actionable warning per detected IaC tool. Most projects use one tool,
332
354
  // but a multi-tool monorepo gets one targeted message each.
333
355
  for (const tool of iac.tools) {
334
- warnings.push(buildIaCWarning(tool));
356
+ findings.push(mkFinding({
357
+ code: 'DCV006',
358
+ validator: 'docsCoverage',
359
+ severity: 'warn',
360
+ message: buildIaCWarning(tool),
361
+ location: 'docs-canonical/ARCHITECTURE.md',
362
+ suggestion: { kind: 'fix', text: `Add an "Infrastructure" section to ARCHITECTURE.md covering the ${tool.label} layout` },
363
+ }));
335
364
  }
336
- return { warnings, passed: 0, total: iac.tools.length };
365
+ return { findings, passed: 0, total: iac.tools.length };
337
366
  }
338
367
 
339
368
  /**
@@ -344,7 +373,7 @@ function checkIaCDocumentation(projectDir, iac) {
344
373
  * sitting in arrays (scan patterns for detecting other projects' configs).
345
374
  */
346
375
  function checkCodeReferencedConfigs(projectDir, allDocContent, config = {}) {
347
- const warnings = [];
376
+ const findings = [];
348
377
  let passed = 0;
349
378
  let total = 0;
350
379
 
@@ -386,13 +415,18 @@ function checkCodeReferencedConfigs(projectDir, allDocContent, config = {}) {
386
415
  if (lowerDocContent.includes(configName.toLowerCase())) {
387
416
  passed++;
388
417
  } else {
389
- warnings.push(
390
- `Code references config file "${configName}" but no documentation mentions it. Add it to README.md or ARCHITECTURE.md`
391
- );
418
+ findings.push(mkFinding({
419
+ code: 'DCV004',
420
+ validator: 'docsCoverage',
421
+ severity: 'warn',
422
+ message: `Code references config file "${configName}" but no documentation mentions it. Add it to README.md or ARCHITECTURE.md`,
423
+ location: configName,
424
+ suggestion: { kind: 'fix', text: 'Describe this config file (purpose and format) in README.md or ARCHITECTURE.md' },
425
+ }));
392
426
  }
393
427
  }
394
428
 
395
- return { warnings, passed, total };
429
+ return { findings, passed, total };
396
430
  }
397
431
 
398
432
  /**
@@ -401,15 +435,15 @@ function checkCodeReferencedConfigs(projectDir, allDocContent, config = {}) {
401
435
  * and Make a README (https://www.makeareadme.com/).
402
436
  */
403
437
  function checkReadmeSections(projectDir) {
404
- const warnings = [];
438
+ const findings = [];
405
439
  let passed = 0;
406
440
  let total = 0;
407
441
 
408
442
  const readmePath = resolve(projectDir, 'README.md');
409
- if (!existsSync(readmePath)) return { warnings, passed, total };
443
+ if (!existsSync(readmePath)) return { findings, passed, total };
410
444
 
411
445
  let content;
412
- try { content = readFileSync(readmePath, 'utf-8'); } catch { return { warnings, passed, total }; }
446
+ try { content = readFileSync(readmePath, 'utf-8'); } catch { return { findings, passed, total }; }
413
447
 
414
448
  const lowerContent = content.toLowerCase();
415
449
 
@@ -431,7 +465,14 @@ function checkReadmeSections(projectDir) {
431
465
  if (section.patterns.some(p => lowerContent.includes(p))) {
432
466
  passed++;
433
467
  } else {
434
- warnings.push(`README.md is missing a "${section.name}" section (Standard README spec)`);
468
+ findings.push(mkFinding({
469
+ code: 'DCV005',
470
+ validator: 'docsCoverage',
471
+ severity: 'warn',
472
+ message: `README.md is missing a "${section.name}" section (Standard README spec)`,
473
+ location: 'README.md',
474
+ suggestion: { kind: 'fix', text: `Add a "${section.name}" section to README.md` },
475
+ }));
435
476
  }
436
477
  }
437
478
 
@@ -445,7 +486,7 @@ function checkReadmeSections(projectDir) {
445
486
  }
446
487
  }
447
488
 
448
- return { warnings, passed, total };
489
+ return { findings, passed, total };
449
490
  }
450
491
 
451
492
  // ── Helpers ──────────────────────────────────────────────────────────────────
@@ -497,21 +538,7 @@ function collectDocContent(projectDir) {
497
538
  return parts.join('\n');
498
539
  }
499
540
 
541
+ // v0.29 consolidation: traversal delegates to the shared canonical walker.
500
542
  function walkFiles(dir, callback) {
501
- if (!existsSync(dir)) return;
502
- let entries;
503
- try { entries = readdirSync(dir); } catch { return; }
504
-
505
- for (const entry of entries) {
506
- if (IGNORE_DIRS.has(entry) || entry.startsWith('.')) continue;
507
- const fullPath = join(dir, entry);
508
- try {
509
- const stat = statSync(fullPath);
510
- if (stat.isDirectory()) {
511
- walkFiles(fullPath, callback);
512
- } else if (stat.isFile()) {
513
- callback(fullPath);
514
- }
515
- } catch { /* skip */ }
516
- }
543
+ sharedWalkFiles(dir, callback, { ignoreDirs: IGNORE_DIRS });
517
544
  }