docguard-cli 0.14.0 → 0.14.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.
@@ -27,7 +27,7 @@
27
27
  */
28
28
 
29
29
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
30
- import { resolve, join, dirname, basename } from 'node:path';
30
+ import { resolve, join, dirname, basename, relative } from 'node:path';
31
31
 
32
32
  /**
33
33
  * Slugify a heading the way GitHub's markdown anchors work.
@@ -294,6 +294,7 @@ function collectCanonicalDocs(projectDir) {
294
294
  export function validateCrossReferences(projectDir, _config = {}) {
295
295
  const errors = [];
296
296
  const warnings = [];
297
+ const fixes = [];
297
298
  let passed = 0;
298
299
  let total = 0;
299
300
 
@@ -364,13 +365,27 @@ export function validateCrossReferences(projectDir, _config = {}) {
364
365
  if (!matches) {
365
366
  const where = targetPath === docPath ? 'same doc' : basename(targetPath);
366
367
  // 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
368
  const suggestion = anchors ? suggestAnchor(normalizedAnchor, anchors) : null;
370
369
  const hint = suggestion ? ` (did you mean #${suggestion}?)` : '';
370
+ // v0.14.1-S12+: when the suggestion is HIGH-CONFIDENCE — unambiguous
371
+ // and very close (edit distance <= 2) — emit a mechanical fix so
372
+ // `docguard fix --write` resolves it without AI. Other near-misses
373
+ // still get the hint but no fix (the user needs to verify intent).
374
+ const isHighConfidence = suggestion && isUnambiguousSuggestion(normalizedAnchor, suggestion, anchors);
371
375
  warnings.push(
372
- `${docName}:${ref.line} — broken anchor: "#${ref.anchor}" in ${where} doesn't match any heading${hint}`
376
+ `${docName}:${ref.line} — broken anchor: "#${ref.anchor}" in ${where} doesn't match any heading${hint}` +
377
+ (isHighConfidence ? ' [auto-fixable]' : '')
373
378
  );
379
+ if (isHighConfidence) {
380
+ fixes.push({
381
+ type: 'replace-anchor',
382
+ doc: relative(projectDir, docPath),
383
+ line: ref.line,
384
+ from: ref.anchor,
385
+ to: suggestion,
386
+ summary: `${docName}:${ref.line} #${ref.anchor} → #${suggestion}`,
387
+ });
388
+ }
374
389
  continue;
375
390
  }
376
391
  }
@@ -379,5 +394,28 @@ export function validateCrossReferences(projectDir, _config = {}) {
379
394
  }
380
395
  }
381
396
 
382
- return { errors, warnings, passed, total };
397
+ return { errors, warnings, passed, total, fixes };
398
+ }
399
+
400
+ /**
401
+ * v0.14.1-S12+ — is the suggested anchor an unambiguous, high-confidence
402
+ * match? Two criteria, both must hold:
403
+ * 1. Edit distance between the broken anchor and the suggestion is <= 2
404
+ * (catches typos and minor renames, refuses major slug rewrites).
405
+ * 2. The suggestion is the ONLY anchor that close — no other anchor
406
+ * within distance <= 2. Avoids fixing to the wrong candidate when
407
+ * multiple are similar.
408
+ */
409
+ function isUnambiguousSuggestion(broken, suggestion, anchors) {
410
+ if (!broken || !suggestion || !anchors) return false;
411
+ const sugDist = editDistance(broken, suggestion);
412
+ if (sugDist > 2) return false;
413
+ // Count other anchors that are also within the same tight budget.
414
+ let closeCandidates = 0;
415
+ for (const a of anchors) {
416
+ if (a === suggestion) continue;
417
+ if (editDistance(broken, a) <= 2) closeCandidates++;
418
+ if (closeCandidates > 0) return false; // ambiguous
419
+ }
420
+ return true;
383
421
  }
@@ -64,6 +64,14 @@ export function validateMetricsConsistency(projectDir, config, guardResults) {
64
64
  { key: 'validators', regex: /(?<!\d\/)\b(\d{2,})\s+validators?\b/gi, label: 'validators' },
65
65
  ];
66
66
 
67
+ // v0.14.1-N1: dedup by (file, label, found) — a file that mentions the
68
+ // stale number multiple times produces ONE warning, not one per occurrence.
69
+ // The replace-count applier already uses replace-all semantics, so a single
70
+ // fix per (file, label) is sufficient. Previously: "X.md" appearing 2× with
71
+ // the same drift would generate 2 warnings + 2 fixes (the second a no-op).
72
+ const reportedDrift = new Set(); // key: `${relPath}|${label}|${found}`
73
+ const reportedPass = new Set(); // key: `${relPath}|${label}` — only count one pass per (file, label)
74
+
67
75
  for (const mdFile of mdFiles) {
68
76
  const relPath = relative(projectDir, mdFile);
69
77
  // Skip changelog (historical numbers are fine by definition)
@@ -79,16 +87,32 @@ export function validateMetricsConsistency(projectDir, config, guardResults) {
79
87
 
80
88
  regex.lastIndex = 0;
81
89
  let match;
90
+ // Collect distinct (found-value) instances within THIS file first,
91
+ // then emit ONE warning per distinct value. A file that says "20" on
92
+ // line 5 and "20" on line 50 is the same drift; "20" on line 5 and
93
+ // "19" on line 50 are two distinct drifts.
94
+ const distinctFoundInFile = new Set();
82
95
  while ((match = regex.exec(content)) !== null) {
83
- total++;
84
- const found = parseInt(match[1], 10);
85
- if (found !== actuals[key] && found > 0) {
96
+ distinctFoundInFile.add(parseInt(match[1], 10));
97
+ }
98
+ if (distinctFoundInFile.size === 0) continue;
99
+
100
+ for (const found of distinctFoundInFile) {
101
+ if (found > 0 && found !== actuals[key]) {
102
+ const driftKey = `${relPath}|${label}|${found}`;
103
+ if (reportedDrift.has(driftKey)) continue;
104
+ reportedDrift.add(driftKey);
105
+ total++;
86
106
  warnings.push(
87
107
  `${relPath} says "${found} ${label}" but actual count is ${actuals[key]}. Fix with \`docguard fix --write\``
88
108
  );
89
- // Deterministic, surgical token replacement — safe to auto-apply.
90
109
  fixes.push({ type: 'replace-count', file: relPath, label, found, actual: actuals[key] });
91
110
  } else {
111
+ // Matches the actual count — one pass per (file, label), not per occurrence.
112
+ const passKey = `${relPath}|${label}`;
113
+ if (reportedPass.has(passKey)) continue;
114
+ reportedPass.add(passKey);
115
+ total++;
92
116
  passed++;
93
117
  }
94
118
  }
@@ -143,12 +143,45 @@ function applyRegenerateSection(projectDir, fix) {
143
143
  return { applied: true, detail: `${fix.doc}: regenerated § ${fix.sectionId}` };
144
144
  }
145
145
 
146
+ /**
147
+ * v0.14.1-S12+ — replace-anchor: rewrite a broken markdown anchor with a
148
+ * high-confidence suggested slug. Emitted by Cross-Reference when its
149
+ * fuzzy match is unambiguous (edit distance <= 2, no other close candidates).
150
+ *
151
+ * fix shape: { type: 'replace-anchor', doc, from, to, line?, summary? }
152
+ *
153
+ * Bounded: only rewrites occurrences of `](#${from})` and `](#X${from})`-like
154
+ * forms — won't touch the broken slug if it happens to appear as plain text.
155
+ * Idempotent: if no occurrence is found (already fixed), no-op.
156
+ */
157
+ function applyReplaceAnchor(projectDir, fix) {
158
+ if (!fix.doc || !fix.from || !fix.to) {
159
+ return { applied: false, skipped: 'replace-anchor needs doc, from, to' };
160
+ }
161
+ const full = resolve(projectDir, fix.doc);
162
+ if (!existsSync(full)) return { applied: false, skipped: `doc not found: ${fix.doc}` };
163
+ const content = readFileSync(full, 'utf-8');
164
+
165
+ // Match an anchor inside a markdown link: `](#from)` OR `](path#from)`.
166
+ // Use a regex that captures the prefix and suffix so we only touch the
167
+ // anchor part — leaving the link text and path intact.
168
+ const fromEsc = esc(fix.from);
169
+ const re = new RegExp(`(\\]\\([^)]*#)${fromEsc}([)\\s])`, 'g');
170
+ const next = content.replace(re, `$1${fix.to}$2`);
171
+ if (next === content) {
172
+ return { applied: false, skipped: `${fix.doc}: anchor #${fix.from} not found (already fixed?)` };
173
+ }
174
+ writeFileSync(full, next, 'utf-8');
175
+ return { applied: true, detail: `${fix.doc}: #${fix.from} → #${fix.to}` };
176
+ }
177
+
146
178
  const APPLIERS = {
147
179
  'replace-count': applyReplaceCount,
148
180
  'replace-version': applyReplaceVersion,
149
181
  'insert-changelog-unreleased': applyInsertChangelogUnreleased,
150
182
  'remove-endpoint': applyRemoveEndpoint,
151
183
  'regenerate-section': applyRegenerateSection,
184
+ 'replace-anchor': applyReplaceAnchor,
152
185
  };
153
186
 
154
187
  export const MECHANICAL_FIX_TYPES = Object.keys(APPLIERS);
@@ -3,7 +3,7 @@ schema_version: "1.0"
3
3
  extension:
4
4
  id: "docguard"
5
5
  name: "DocGuard — CDD Enforcement"
6
- version: "0.14.0"
6
+ version: "0.14.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.14.0
9
+ version: 0.14.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-fix
11
11
  ---
12
- <!-- docguard:version: 0.14.0 -->
12
+ <!-- docguard:version: 0.14.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.14.0
10
+ version: 0.14.1
11
11
  source: extensions/spec-kit-docguard/skills/docguard-guard
12
12
  ---
13
- <!-- docguard:version: 0.14.0 -->
13
+ <!-- docguard:version: 0.14.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.14.0
9
+ version: 0.14.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-review
11
11
  ---
12
- <!-- docguard:version: 0.14.0 -->
12
+ <!-- docguard:version: 0.14.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.14.0
9
+ version: 0.14.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-score
11
11
  ---
12
- <!-- docguard:version: 0.14.0 -->
12
+ <!-- docguard:version: 0.14.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.14.0
7
+ version: 0.14.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.14.0",
3
+ "version": "0.14.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": {