docguard-cli 0.13.0 → 0.13.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.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Impact Command — S-11
3
+ *
4
+ * After a commit (or before opening a PR), shows which canonical doc
5
+ * sections reference any file that changed since `--since` (default HEAD~1).
6
+ * Combines the L-2 reverse-trace logic with the changed-files diff so you
7
+ * get "you should re-read these doc sections" in one command.
8
+ *
9
+ * Use cases:
10
+ * - Post-commit hook: `docguard impact --since HEAD~1` runs after each
11
+ * commit and reminds the developer which docs to update.
12
+ * - PR prep: `docguard impact --since main` shows the doc surface area
13
+ * touched by the whole branch.
14
+ *
15
+ * JSON mode emits a structured `{ changedFiles, affectedDocs }` payload
16
+ * for CI integrations and PR-comment bots.
17
+ *
18
+ * @req SC-S11-001 — impact reports per-file → doc mappings
19
+ * @req SC-S11-002 — files with no doc references are listed as "no impact"
20
+ * @req SC-S11-003 — --format json emits parseable structured output
21
+ * @req SC-S11-004 — non-code files (.md, .json, etc.) are skipped from impact analysis
22
+ */
23
+
24
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
25
+ import { resolve, basename } from 'node:path';
26
+
27
+ import { c } from '../shared.mjs';
28
+ import { changedFilesSince, isGitRepo } from '../shared-git.mjs';
29
+
30
+ /**
31
+ * File extensions we consider "code" for the purposes of impact analysis.
32
+ * Match the set used by other validators (Docs-Sync, Freshness).
33
+ */
34
+ const CODE_EXTENSIONS = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift)$/;
35
+
36
+ function escapeRegex(s) {
37
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
38
+ }
39
+
40
+ /**
41
+ * Find canonical doc references for a single file. Reuses the same three
42
+ * match strategies as trace --reverse for consistency: direct path,
43
+ * basename, backticked module name.
44
+ */
45
+ function findReferences(file, docs) {
46
+ const refs = [];
47
+ const normalized = file.replace(/^\.\//, '');
48
+ const base = basename(normalized);
49
+ const stem = base.replace(/\.[^.]+$/, '');
50
+ const stemRe = new RegExp(`\`${escapeRegex(stem)}\``);
51
+ for (const [docName, lines] of docs) {
52
+ for (let i = 0; i < lines.length; i++) {
53
+ const line = lines[i];
54
+ let kind = null;
55
+ if (line.includes(normalized)) kind = 'path';
56
+ else if (line.includes(base)) kind = 'basename';
57
+ else if (stemRe.test(line)) kind = 'module';
58
+ if (kind) {
59
+ refs.push({ doc: docName, line: i + 1, kind });
60
+ }
61
+ }
62
+ }
63
+ return refs;
64
+ }
65
+
66
+ export function runImpact(projectDir, _config, flags) {
67
+ const isJson = flags.format === 'json';
68
+ const since = flags.since || 'HEAD~1';
69
+
70
+ if (!isGitRepo(projectDir)) {
71
+ if (isJson) {
72
+ console.log(JSON.stringify({ since, error: 'not a git repository', changedFiles: [], affectedDocs: [] }, null, 2));
73
+ } else {
74
+ console.error(`${c.red}Not a git repository — impact requires git history.${c.reset}`);
75
+ }
76
+ process.exit(1);
77
+ }
78
+
79
+ const changed = changedFilesSince(projectDir, since);
80
+ // Filter to code files only — markdown/json/yaml changes don't have "doc
81
+ // impact" in the same sense; they ARE the docs (or config).
82
+ const codeChanged = changed.filter(f => CODE_EXTENSIONS.test(f));
83
+
84
+ // Index canonical docs once
85
+ const docsDir = resolve(projectDir, 'docs-canonical');
86
+ const docsIndex = new Map(); // docName → lines[]
87
+ if (existsSync(docsDir)) {
88
+ try {
89
+ for (const f of readdirSync(docsDir)) {
90
+ if (!f.endsWith('.md')) continue;
91
+ try {
92
+ const content = readFileSync(resolve(docsDir, f), 'utf-8');
93
+ docsIndex.set(f, content.split('\n'));
94
+ } catch { /* skip unreadable */ }
95
+ }
96
+ } catch { /* skip if dir unreadable */ }
97
+ }
98
+
99
+ // Compute per-file references
100
+ const fileImpact = []; // { file, references: [{doc, line, kind}] }
101
+ for (const f of codeChanged) {
102
+ fileImpact.push({ file: f, references: findReferences(f, docsIndex) });
103
+ }
104
+
105
+ // Roll up: which docs are affected, with all source files
106
+ const docMap = new Map(); // doc → Set<file>
107
+ for (const { file, references } of fileImpact) {
108
+ for (const r of references) {
109
+ if (!docMap.has(r.doc)) docMap.set(r.doc, new Set());
110
+ docMap.get(r.doc).add(file);
111
+ }
112
+ }
113
+ const affectedDocs = Array.from(docMap.entries()).map(([doc, files]) => ({
114
+ doc,
115
+ files: Array.from(files),
116
+ }));
117
+
118
+ // ── JSON output ──
119
+ if (isJson) {
120
+ console.log(JSON.stringify({
121
+ since,
122
+ changedFiles: codeChanged,
123
+ ignoredFiles: changed.filter(f => !CODE_EXTENSIONS.test(f)),
124
+ affectedDocs,
125
+ timestamp: new Date().toISOString(),
126
+ }, null, 2));
127
+ return;
128
+ }
129
+
130
+ // ── Text output ──
131
+ console.log(`${c.bold}📊 DocGuard Impact${c.reset} ${c.dim}(since ${since})${c.reset}\n`);
132
+
133
+ if (changed.length === 0) {
134
+ console.log(` ${c.green}✅ No file changes since ${since}.${c.reset}`);
135
+ return;
136
+ }
137
+ if (codeChanged.length === 0) {
138
+ console.log(` ${c.dim}No code files changed (${changed.length} non-code files: ${changed.slice(0, 3).join(', ')}${changed.length > 3 ? '…' : ''}).${c.reset}`);
139
+ return;
140
+ }
141
+
142
+ console.log(` ${c.cyan}${codeChanged.length}${c.reset} code file(s) changed.\n`);
143
+
144
+ if (affectedDocs.length === 0) {
145
+ console.log(` ${c.yellow}⚠ No canonical docs reference any of the changed files.${c.reset}`);
146
+ console.log(` ${c.dim}This often means the changed code is undocumented. Consider:${c.reset}`);
147
+ console.log(` ${c.dim} - Running ${c.cyan}docguard generate --plan${c.dim} to add doc skeletons${c.reset}`);
148
+ console.log(` ${c.dim} - Reviewing whether the change belongs in an existing doc${c.reset}`);
149
+ return;
150
+ }
151
+
152
+ console.log(` ${c.green}${affectedDocs.length}${c.reset} canonical doc(s) reference the changed files:\n`);
153
+ for (const { doc, files } of affectedDocs) {
154
+ console.log(` ${c.cyan}${doc}${c.reset} ${c.dim}(${files.length} file${files.length > 1 ? 's' : ''})${c.reset}`);
155
+ for (const f of files.slice(0, 5)) {
156
+ console.log(` ${c.dim}via${c.reset} ${f}`);
157
+ }
158
+ if (files.length > 5) console.log(` ${c.dim}... ${files.length - 5} more${c.reset}`);
159
+ }
160
+
161
+ // List code files with NO doc references — these may need new docs
162
+ const orphaned = fileImpact.filter(fi => fi.references.length === 0).map(fi => fi.file);
163
+ if (orphaned.length > 0) {
164
+ console.log(`\n ${c.yellow}${orphaned.length} changed file(s) have NO canonical doc reference:${c.reset}`);
165
+ for (const f of orphaned.slice(0, 5)) console.log(` ${c.dim}• ${f}${c.reset}`);
166
+ if (orphaned.length > 5) console.log(` ${c.dim}... ${orphaned.length - 5} more${c.reset}`);
167
+ console.log(` ${c.dim}These may be undocumented — review whether they belong in an existing doc.${c.reset}`);
168
+ }
169
+ }
package/cli/docguard.mjs CHANGED
@@ -41,6 +41,7 @@ import { runTrace } from './commands/trace.mjs';
41
41
  import { runLlms } from './commands/llms.mjs';
42
42
  import { runSetup } from './commands/setup.mjs';
43
43
  import { runUpgrade } from './commands/upgrade.mjs';
44
+ import { runImpact } from './commands/impact.mjs';
44
45
  import { ensureSkills } from './ensure-skills.mjs';
45
46
 
46
47
  // ── Shared constants (imported to break circular dependencies) ──────────
@@ -514,6 +515,9 @@ async function main() {
514
515
  case 'update':
515
516
  await runUpgrade(projectDir, config, flags);
516
517
  break;
518
+ case 'impact':
519
+ runImpact(projectDir, config, flags);
520
+ break;
517
521
  default:
518
522
  console.error(`${c.red}Unknown command: ${command}${c.reset}`);
519
523
  console.log(`Run ${c.cyan}docguard --help${c.reset} for usage.`);
@@ -155,14 +155,103 @@ export function extractRefs(content, sourcePath) {
155
155
  * Resolve a target file path relative to a source markdown file.
156
156
  * Returns the absolute path or null if the file doesn't exist.
157
157
  */
158
+ /**
159
+ * S-12: Suggest the closest matching anchor when a broken-anchor warning
160
+ * fires. Uses a cheap two-pass match:
161
+ * 1. Exact substring match (anchor is contained in or contains a heading)
162
+ * 2. Levenshtein-like edit-distance within a budget (max 3 edits)
163
+ *
164
+ * Returns the best-matching slug string, or null when no candidate scores
165
+ * well enough to suggest with confidence. Suggestion threshold tuned so
166
+ * cosmetic typos surface but unrelated headings don't false-positive.
167
+ *
168
+ * @param {string} broken - the slug the user wrote (e.g. "athena-setup")
169
+ * @param {Set<string>} candidates - anchors that exist in the target doc
170
+ * @returns {string|null}
171
+ */
172
+ export function suggestAnchor(broken, candidates) {
173
+ if (!broken || !candidates || candidates.size === 0) return null;
174
+
175
+ // Pass 1: substring containment — high-confidence match. Both sides must
176
+ // be at least 4 chars to avoid spurious matches against very short anchors
177
+ // (e.g. `#a` would otherwise match any broken slug containing the letter a).
178
+ const MIN_SUBSTRING = 4;
179
+ for (const c of candidates) {
180
+ if (c.length < MIN_SUBSTRING || broken.length < MIN_SUBSTRING) continue;
181
+ if (c.startsWith(broken) || broken.startsWith(c) || c.includes(broken) || broken.includes(c)) {
182
+ // Additionally require >= 50% overlap of the shorter into the longer.
183
+ // Avoids "user-id" matching "user-management-and-administration" via
184
+ // the bare "user" prefix.
185
+ const overlap = Math.min(c.length, broken.length) / Math.max(c.length, broken.length);
186
+ if (overlap >= 0.5) return c;
187
+ }
188
+ }
189
+
190
+ // Pass 2: edit distance — pick the closest if within budget.
191
+ let best = null;
192
+ let bestDist = Infinity;
193
+ for (const c of candidates) {
194
+ // Cheap early-out: huge length difference can't be within budget.
195
+ if (Math.abs(c.length - broken.length) > 8) continue;
196
+ const d = editDistance(broken, c);
197
+ if (d < bestDist) { bestDist = d; best = c; }
198
+ }
199
+ // Budget: max(3, length / 5) — proportional to slug length but cap small.
200
+ const budget = Math.max(3, Math.floor(broken.length / 5));
201
+ if (bestDist <= budget) return best;
202
+ return null;
203
+ }
204
+
205
+ /**
206
+ * Levenshtein edit distance. O(m·n) time, O(min) space. We bound input
207
+ * size before calling (S-12's pass 2 pre-filters), so a textbook impl is
208
+ * fine. Adding a dependency for one cheap routine isn't worth it.
209
+ */
210
+ function editDistance(a, b) {
211
+ if (a === b) return 0;
212
+ if (!a.length) return b.length;
213
+ if (!b.length) return a.length;
214
+ let prev = new Array(b.length + 1);
215
+ let curr = new Array(b.length + 1);
216
+ for (let j = 0; j <= b.length; j++) prev[j] = j;
217
+ for (let i = 1; i <= a.length; i++) {
218
+ curr[0] = i;
219
+ for (let j = 1; j <= b.length; j++) {
220
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
221
+ curr[j] = Math.min(
222
+ prev[j] + 1, // deletion
223
+ curr[j - 1] + 1, // insertion
224
+ prev[j - 1] + cost, // substitution
225
+ );
226
+ }
227
+ [prev, curr] = [curr, prev];
228
+ }
229
+ return prev[b.length];
230
+ }
231
+
158
232
  function resolveTarget(sourcePath, targetRel, projectDir) {
159
233
  if (!targetRel) return null;
160
- // Try relative to source's directory first
161
- const fromSource = resolve(dirname(sourcePath), targetRel);
162
- if (existsSync(fromSource)) return fromSource;
163
- // Also try from project root (some authors write `docs-canonical/X.md`)
164
- const fromRoot = resolve(projectDir, targetRel);
165
- if (existsSync(fromRoot)) return fromRoot;
234
+ // B-6: try BOTH the literal path and the URL-decoded form. CommonMark
235
+ // accepts `[name](../WU%20Documentation/foo.md)` for paths with spaces,
236
+ // and the decoded form (`../WU Documentation/foo.md`) is what hits the
237
+ // filesystem. The angle-bracket form `<../WU Documentation/foo.md>` is
238
+ // already non-URL-encoded by the time it reaches us. Try literal first
239
+ // (handles paths that legitimately contain `%`), then decoded.
240
+ const candidates = [targetRel];
241
+ try {
242
+ const decoded = decodeURIComponent(targetRel);
243
+ if (decoded !== targetRel) candidates.push(decoded);
244
+ } catch {
245
+ // Malformed % escapes — fall back to literal-only.
246
+ }
247
+ for (const cand of candidates) {
248
+ // Try relative to source's directory first
249
+ const fromSource = resolve(dirname(sourcePath), cand);
250
+ if (existsSync(fromSource)) return fromSource;
251
+ // Also try from project root (some authors write `docs-canonical/X.md`)
252
+ const fromRoot = resolve(projectDir, cand);
253
+ if (existsSync(fromRoot)) return fromRoot;
254
+ }
166
255
  return null;
167
256
  }
168
257
 
@@ -274,8 +363,13 @@ export function validateCrossReferences(projectDir, _config = {}) {
274
363
  const matches = anchors && (anchors.has(ref.anchor) || anchors.has(normalizedAnchor));
275
364
  if (!matches) {
276
365
  const where = targetPath === docPath ? 'same doc' : basename(targetPath);
366
+ // S-12: suggest the closest matching anchor when there's a near-miss.
367
+ // Three of five wu user-fixes were "heading renamed, link not updated"
368
+ // — a suggested-slug hint makes those deterministic-fixable.
369
+ const suggestion = anchors ? suggestAnchor(normalizedAnchor, anchors) : null;
370
+ const hint = suggestion ? ` (did you mean #${suggestion}?)` : '';
277
371
  warnings.push(
278
- `${docName}:${ref.line} — broken anchor: "#${ref.anchor}" in ${where} doesn't match any heading`
372
+ `${docName}:${ref.line} — broken anchor: "#${ref.anchor}" in ${where} doesn't match any heading${hint}`
279
373
  );
280
374
  continue;
281
375
  }
@@ -9,7 +9,24 @@
9
9
  import { existsSync, readdirSync, statSync } from 'node:fs';
10
10
  import { resolve, join, extname } from 'node:path';
11
11
  import { execSync, execFileSync } from 'node:child_process';
12
- import { getLastCommitDate } from '../shared-git.mjs';
12
+
13
+ // B-5 fix (v0.13.1): use a defensive import. If `shared-git.mjs` is missing
14
+ // or unloadable in the end-user install (whatever the root cause — partial
15
+ // upgrade, package corruption, weird module resolution), we fall back to
16
+ // the original inline implementation below. The worst-case outcome is
17
+ // "rename detection doesn't work", NOT "validator crashes with a useless
18
+ // ReferenceError". Reported by wu-whatsappinbox v0.13.x feedback.
19
+ let _sharedGetLastCommitDate = null;
20
+ try {
21
+ const mod = await import('../shared-git.mjs');
22
+ if (mod && typeof mod.getLastCommitDate === 'function') {
23
+ _sharedGetLastCommitDate = mod.getLastCommitDate;
24
+ }
25
+ } catch {
26
+ // Silently fall back. Test in tests/freshness-resilience.test.mjs verifies
27
+ // the validator stays operational when the import goes sideways.
28
+ _sharedGetLastCommitDate = null;
29
+ }
13
30
 
14
31
  const IGNORE_DIRS = new Set([
15
32
  'node_modules', '.git', '.next', 'dist', 'build',
@@ -22,10 +39,29 @@ const IGNORE_DIRS = new Set([
22
39
  * Returns null if the file isn't tracked or git isn't available.
23
40
  */
24
41
  function getLastGitDate(filePath, dir) {
25
- // Delegate to shared-git so rename history (--follow) is preserved.
26
- // Without --follow, a `git mv` resets the file's "last commit date" and
27
- // the Freshness counter silently misses drift introduced by the rename.
28
- return getLastCommitDate(dir, filePath);
42
+ // Prefer the shared-git --follow-aware path when available (v0.13+ default).
43
+ // Fall back to inline implementation if the import failed at module load —
44
+ // this guarantees the validator never throws a ReferenceError even in
45
+ // environments where ESM resolution is broken.
46
+ if (_sharedGetLastCommitDate) {
47
+ try {
48
+ return _sharedGetLastCommitDate(dir, filePath);
49
+ } catch {
50
+ // fall through to inline
51
+ }
52
+ }
53
+ // Inline pre-v0.13 implementation — works without rename detection, but
54
+ // is guaranteed to not throw a "not defined" error.
55
+ try {
56
+ const result = execFileSync(
57
+ 'git',
58
+ ['log', '-1', '--format=%aI', '--', filePath],
59
+ { cwd: dir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
60
+ ).trim();
61
+ return result ? new Date(result) : null;
62
+ } catch {
63
+ return null;
64
+ }
29
65
  }
30
66
 
31
67
  /**
@@ -23,12 +23,38 @@
23
23
  * @req SC-M1-004 — N/A when no source=code sections present in any doc
24
24
  */
25
25
 
26
- import { existsSync, readFileSync } from 'node:fs';
26
+ import { existsSync, readFileSync, statSync } from 'node:fs';
27
27
  import { resolve, basename } from 'node:path';
28
28
 
29
29
  import { buildMemoryPlan } from '../scanners/memory-plan.mjs';
30
30
  import { getSection } from '../writers/sections.mjs';
31
31
 
32
+ /**
33
+ * S-7: how long a generated doc may sit in `status: draft` before we warn.
34
+ * 14 days is the v0.13.1 default — long enough to absorb a typical sprint,
35
+ * short enough to surface forgotten skeletons before they rot.
36
+ */
37
+ const DRAFT_STALENESS_DAYS = 14;
38
+
39
+ /**
40
+ * Parse the frontmatter `status:` field from a markdown doc.
41
+ * Returns the trimmed value or null. Tolerant of either YAML-style
42
+ * fences (`---`) or HTML-comment-style (`<!-- status: draft -->`) markers.
43
+ */
44
+ function extractDocStatus(content) {
45
+ if (!content) return null;
46
+ // YAML frontmatter: --- ... ---
47
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
48
+ if (fmMatch) {
49
+ const sm = fmMatch[1].match(/^\s*status:\s*(\S+)\s*$/m);
50
+ if (sm) return sm[1].toLowerCase();
51
+ }
52
+ // Inline `<!-- status: draft -->` marker (common in docguard:generated docs).
53
+ const inline = content.match(/<!--\s*status:\s*(\w+)\s*-->/i);
54
+ if (inline) return inline[1].toLowerCase();
55
+ return null;
56
+ }
57
+
32
58
  export function validateGeneratedStaleness(projectDir, config = {}) {
33
59
  const result = { errors: [], warnings: [], passed: 0, total: 0 };
34
60
 
@@ -46,6 +72,9 @@ export function validateGeneratedStaleness(projectDir, config = {}) {
46
72
 
47
73
  // Walk each doc's source=code sections and compare against on-disk content.
48
74
  let anySourceCodeSection = false;
75
+ const draftThresholdDays = (config.draftStalenessDays != null)
76
+ ? Number(config.draftStalenessDays)
77
+ : DRAFT_STALENESS_DAYS;
49
78
 
50
79
  for (const doc of plan.docs) {
51
80
  const fullPath = resolve(projectDir, doc.path);
@@ -53,6 +82,29 @@ export function validateGeneratedStaleness(projectDir, config = {}) {
53
82
  let content;
54
83
  try { content = readFileSync(fullPath, 'utf-8'); } catch { continue; }
55
84
 
85
+ // S-7: a docguard:generated doc with frontmatter `status: draft` that
86
+ // hasn't been updated in N days is probably a forgotten skeleton.
87
+ // Counted as a check (so total reflects it) and warned when stale.
88
+ const status = extractDocStatus(content);
89
+ if (status === 'draft') {
90
+ result.total++;
91
+ try {
92
+ const mtime = statSync(fullPath).mtime;
93
+ const ageDays = (Date.now() - mtime.getTime()) / (1000 * 60 * 60 * 24);
94
+ if (ageDays > draftThresholdDays) {
95
+ result.warnings.push(
96
+ `${basename(doc.path)} has been in \`status: draft\` for ${Math.floor(ageDays)} days. ` +
97
+ `Promote to status:current or remove. Run \`/docguard.fix --doc ${basename(doc.path)}\` to draft the prose.`
98
+ );
99
+ } else {
100
+ result.passed++;
101
+ }
102
+ } catch {
103
+ // Couldn't stat the file — skip the staleness check, don't count it.
104
+ result.total--;
105
+ }
106
+ }
107
+
56
108
  for (const sec of doc.sections) {
57
109
  if (sec.source !== 'code') continue;
58
110
  anySourceCodeSection = true;
@@ -89,7 +141,10 @@ export function validateGeneratedStaleness(projectDir, config = {}) {
89
141
  }
90
142
  }
91
143
 
92
- if (!anySourceCodeSection) {
144
+ // S-7: even when no source=code sections exist, a draft-status check
145
+ // counts the validator as applicable. Only return N/A when we genuinely
146
+ // had nothing to evaluate.
147
+ if (!anySourceCodeSection && result.total === 0) {
93
148
  return { ...result, applicable: false };
94
149
  }
95
150
 
@@ -19,23 +19,33 @@ export function validateStructure(projectDir, config) {
19
19
  }
20
20
  }
21
21
 
22
- // Check agent file (any one is fine)
23
- results.total++;
24
- const agentFileFound = config.requiredFiles.agentFile.some(f =>
25
- existsSync(resolve(projectDir, f))
26
- );
27
- if (agentFileFound) {
28
- results.passed++;
29
- } else {
30
- results.errors.push(`Missing agent file: ${config.requiredFiles.agentFile.join(' or ')}`);
22
+ // Check agent file (any one is fine) — defensive: tolerate missing config
23
+ // shapes (B-5 class of safety net: never let a config gap leak as a
24
+ // ReferenceError / TypeError into the user's guard output).
25
+ const agentFiles = Array.isArray(config.requiredFiles?.agentFile)
26
+ ? config.requiredFiles.agentFile
27
+ : (typeof config.requiredFiles?.agentFile === 'string' ? [config.requiredFiles.agentFile] : []);
28
+ if (agentFiles.length > 0) {
29
+ results.total++;
30
+ const agentFileFound = agentFiles.some(f =>
31
+ existsSync(resolve(projectDir, f))
32
+ );
33
+ if (agentFileFound) {
34
+ results.passed++;
35
+ } else {
36
+ results.errors.push(`Missing agent file: ${agentFiles.join(' or ')}`);
37
+ }
31
38
  }
32
39
 
33
- // Check changelog
34
- results.total++;
35
- if (existsSync(resolve(projectDir, config.requiredFiles.changelog))) {
36
- results.passed++;
37
- } else {
38
- results.errors.push(`Missing required file: ${config.requiredFiles.changelog}`);
40
+ // Check changelog — same defensive pattern.
41
+ const changelogPath = config.requiredFiles?.changelog;
42
+ if (changelogPath) {
43
+ results.total++;
44
+ if (existsSync(resolve(projectDir, changelogPath))) {
45
+ results.passed++;
46
+ } else {
47
+ results.errors.push(`Missing required file: ${changelogPath}`);
48
+ }
39
49
  }
40
50
 
41
51
  // Check drift log
@@ -3,7 +3,7 @@ schema_version: "1.0"
3
3
  extension:
4
4
  id: "docguard"
5
5
  name: "DocGuard — CDD Enforcement"
6
- version: "0.13.0"
6
+ version: "0.13.1"
7
7
  description: "Canonical-Driven Development enforcement as a true spec-kit extension. LLM-first design with 19 automated validators, 4 AI behavior skills, spec-kit skill chaining, and workflow hooks. Zero NPM runtime dependencies."
8
8
  author: "Ricardo Accioly"
9
9
  repository: "https://github.com/raccioly/docguard"
@@ -6,10 +6,10 @@ description: AI-driven documentation repair with structured research workflow, t
6
6
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
7
7
  metadata:
8
8
  author: docguard
9
- version: 0.13.0
9
+ version: 0.13.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-fix
11
11
  ---
12
- <!-- docguard:version: 0.13.0 -->
12
+ <!-- docguard:version: 0.13.1 -->
13
13
 
14
14
  # DocGuard Fix Skill
15
15
 
@@ -7,10 +7,10 @@ description: Run DocGuard guard validation against Canonical-Driven Development
7
7
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
8
8
  metadata:
9
9
  author: docguard
10
- version: 0.13.0
10
+ version: 0.13.1
11
11
  source: extensions/spec-kit-docguard/skills/docguard-guard
12
12
  ---
13
- <!-- docguard:version: 0.13.0 -->
13
+ <!-- docguard:version: 0.13.1 -->
14
14
 
15
15
  # DocGuard Guard Skill
16
16
 
@@ -6,10 +6,10 @@ description: Cross-document consistency analysis and quality assessment. Perform
6
6
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
7
7
  metadata:
8
8
  author: docguard
9
- version: 0.13.0
9
+ version: 0.13.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-review
11
11
  ---
12
- <!-- docguard:version: 0.13.0 -->
12
+ <!-- docguard:version: 0.13.1 -->
13
13
 
14
14
  # DocGuard Review Skill
15
15
 
@@ -6,10 +6,10 @@ description: CDD maturity assessment with category-aware improvement roadmap. Ru
6
6
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
7
7
  metadata:
8
8
  author: docguard
9
- version: 0.13.0
9
+ version: 0.13.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-score
11
11
  ---
12
- <!-- docguard:version: 0.13.0 -->
12
+ <!-- docguard:version: 0.13.1 -->
13
13
 
14
14
  # DocGuard Score Skill
15
15
 
@@ -4,7 +4,7 @@ description: Keep canonical documentation ALWAYS UP TO DATE. Refreshes code-trut
4
4
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
5
5
  metadata:
6
6
  author: docguard
7
- version: 0.13.0
7
+ version: 0.13.1
8
8
  source: extensions/spec-kit-docguard/skills/docguard-sync
9
9
  ---
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "The enforcement tool for Canonical-Driven Development (CDD). Audit, generate, and guard your project documentation.",
5
5
  "type": "module",
6
6
  "bin": {