docguard-cli 0.30.1 → 0.31.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.
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Diff-Overlap Suspicion validator (DSP) — v0.31.0.
3
+ *
4
+ * Research basis: the outdated-comment work (arXiv 2010.01625) found that a
5
+ * purely deterministic rule — "flag the prose as suspect if its tokens overlap
6
+ * a Delete/ReplaceOld span of the code change" — hits F1 74.7, beating every
7
+ * post-hoc neural model. We apply it to DOCS instead of comments.
8
+ *
9
+ * Precision-first pairing (two independent signals must BOTH hold):
10
+ * 1. the doc REFERENCES the changed code file (path / basename / `module`),
11
+ * 2. the doc's wording OVERLAPS tokens that were REMOVED from that file.
12
+ * Requiring both is what keeps this from firing on every doc that happens to
13
+ * share a common word with a diff. All findings are confidence:'low' (soft /
14
+ * reportable) — this is a "review this" signal, never a hard failure.
15
+ *
16
+ * Change-driven: reads `config.changedSinceRef` (set by `guard --changed-only`)
17
+ * or falls back to HEAD~1. Returns applicable:false when there's no git history
18
+ * or no code change carries removed tokens, so it stays silent off-CI.
19
+ */
20
+
21
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
22
+ import { resolve, basename } from 'node:path';
23
+ import { isGitRepo, getDiffText } from '../shared-git.mjs';
24
+ import { parseUnifiedDiff, removedTokens, tokenize, tokenOverlap } from '../shared-diff.mjs';
25
+ import { mkFinding, resultFromFindings } from '../findings.mjs';
26
+
27
+ const CODE_EXTENSIONS = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift|scala|dart)$/;
28
+
29
+ // Generic framework / language noise that overlaps between ANY React/TS doc and
30
+ // ANY diff — empirically the residual false-positive source (v0.31.0 corpus:
31
+ // "SECURITY.md ↔ page.tsx: page,set,use,state,active"). We require the shared
32
+ // tokens to contain DOMAIN identifiers, so these are stripped before counting.
33
+ const GENERIC_TOKENS = new Set([
34
+ 'page', 'components', 'component', 'shared', 'nav', 'state', 'use', 'set',
35
+ 'active', 'tab', 'react', 'props', 'prop', 'string', 'type', 'types', 'value',
36
+ 'values', 'data', 'status', 'config', 'client', 'none', 'all', 'com', 'https',
37
+ 'http', 'url', 'text', 'download', 'request', 'response', 'error', 'index',
38
+ 'item', 'items', 'list', 'name', 'key', 'map', 'log', 'update', 'updated',
39
+ 'version', 'content', 'document', 'description', 'service', 'services',
40
+ 'object', 'array', 'number', 'boolean', 'async', 'await', 'promise', 'void',
41
+ 'render', 'component', 'element', 'style', 'styles', 'class', 'div', 'span',
42
+ 'button', 'input', 'form', 'label', 'title', 'header', 'footer', 'main',
43
+ // presentational / CSS — styling churn is not API-contract drift
44
+ 'font', 'color', 'colors', 'tracking', 'surface', 'auto', 'full', 'next',
45
+ 'body', 'sans', 'blue', 'accent', 'size', 'spacing', 'margin', 'padding',
46
+ 'width', 'height', 'flex', 'grid', 'bold', 'bg', 'rounded',
47
+ // HTTP / REST / handler plumbing — generic across any API route (globalshares
48
+ // corpus: a route-inventory doc + heavy rewrites flooded findings with these)
49
+ 'code', 'json', 'err', 'error', 'message', 'auth', 'get', 'put', 'post',
50
+ 'patch', 'delete', 'req', 'res', 'route', 'routes', 'handler', 'endpoint',
51
+ 'method', 'headers', 'query', 'params', 'param', 'cron', 'process', 'api',
52
+ 'path', 'lib', 'util', 'utils', 'helper', 'helpers', 'admin', 'roles', 'role',
53
+ 'user', 'users', 'email', 'price', 'json', 'fetch', 'axios', 'send', 'call',
54
+ ]);
55
+
56
+ function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
57
+
58
+ // Index canonical docs + root agent-instruction files → Map<name, {lines, tokens}>.
59
+ function indexDocs(projectDir) {
60
+ const docs = new Map();
61
+ const add = (name, full) => {
62
+ try {
63
+ const content = readFileSync(full, 'utf-8');
64
+ docs.set(name, { lines: content.split('\n'), tokens: tokenize(content) });
65
+ } catch { /* skip unreadable */ }
66
+ };
67
+ const docsDir = resolve(projectDir, 'docs-canonical');
68
+ if (existsSync(docsDir)) {
69
+ try {
70
+ for (const f of readdirSync(docsDir)) {
71
+ if (f.endsWith('.md')) add(f, resolve(docsDir, f));
72
+ }
73
+ } catch { /* skip */ }
74
+ }
75
+ // Agent-instruction files are documentation too — they routinely name code.
76
+ for (const agent of ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']) {
77
+ const p = resolve(projectDir, agent);
78
+ if (existsSync(p)) add(agent, p);
79
+ }
80
+ return docs;
81
+ }
82
+
83
+ // Does the doc reference this file? We deliberately accept ONLY `path` (the
84
+ // doc wrote the real file path) and `module` (the doc backticked the module
85
+ // stem) references. We DROP bare `basename` matches: empirical corpus testing
86
+ // (v0.31.0) showed basename refs pair an architecture doc that lists every
87
+ // `page.tsx` with framework-noise tokens (page/components/state/use/nav),
88
+ // producing false positives. path/module refs are intentional and high-signal.
89
+ function referenceKind(docLines, file) {
90
+ const normalized = file.replace(/^\.\//, '');
91
+ const base = basename(normalized);
92
+ const stem = base.replace(/\.[^.]+$/, '');
93
+ const stemRe = new RegExp(`\`${escapeRegex(stem)}\``);
94
+ for (const line of docLines) {
95
+ if (line.includes(normalized)) return 'path';
96
+ if (stemRe.test(line)) return 'module';
97
+ }
98
+ return null;
99
+ }
100
+
101
+ export function validateDiffSuspicion(projectDir, config = {}) {
102
+ const cfg = config.diffSuspicion || {};
103
+ const minOverlap = Number.isInteger(cfg.minOverlap) ? cfg.minOverlap : 2;
104
+ const ref = cfg.since || config.changedSinceRef || 'HEAD~1';
105
+
106
+ if (!isGitRepo(projectDir)) {
107
+ return resultFromFindings([], { passed: 0, total: 0, applicable: false });
108
+ }
109
+
110
+ const diffText = getDiffText(projectDir, ref);
111
+ const changedFiles = parseUnifiedDiff(diffText).filter(
112
+ f => f.newPath && CODE_EXTENSIONS.test(f.newPath) && f.status !== 'deleted'
113
+ );
114
+ // Precompute removed-token sets; drop files whose change removed nothing.
115
+ const changed = changedFiles
116
+ .map(f => ({ path: f.newPath, removed: removedTokens(f) }))
117
+ .filter(f => f.removed.size > 0);
118
+
119
+ if (changed.length === 0) {
120
+ return resultFromFindings([], { passed: 0, total: 0, applicable: false });
121
+ }
122
+
123
+ const docs = indexDocs(projectDir);
124
+ if (docs.size === 0) {
125
+ return resultFromFindings([], { passed: 0, total: 0, applicable: false });
126
+ }
127
+
128
+ const maxPerDoc = Number.isInteger(cfg.maxPerDoc) ? cfg.maxPerDoc : 5;
129
+ const findings = [];
130
+ let pairsChecked = 0;
131
+ for (const [docName, doc] of docs) {
132
+ // Collect this doc's suspect pairs, strongest overlap first, so a doc that
133
+ // inventories many changed files (e.g. an API-route reference) is capped
134
+ // with an elision note rather than flooding — the same discipline SPK008 uses.
135
+ const hits = [];
136
+ for (const cf of changed) {
137
+ const kind = referenceKind(doc.lines, cf.path);
138
+ if (!kind) continue; // signal 1: doc must reference the changed file (path|module)
139
+ pairsChecked++;
140
+ const { shared: rawShared } = tokenOverlap(doc.tokens, cf.removed);
141
+ // signal 2: doc must share DOMAIN (non-generic) removed tokens
142
+ const shared = rawShared.filter(t => !GENERIC_TOKENS.has(t));
143
+ if (shared.length < minOverlap) continue;
144
+ hits.push({ path: cf.path, kind, shared });
145
+ }
146
+ hits.sort((a, b) => b.shared.length - a.shared.length);
147
+ for (const h of hits.slice(0, maxPerDoc)) {
148
+ findings.push(mkFinding({
149
+ code: 'DSP001',
150
+ validator: 'diff-suspicion',
151
+ severity: 'warn',
152
+ confidence: 'low',
153
+ message: `${docName} describes ${h.path} (${h.kind} ref), which just had ${h.shared.slice(0, 5).join(', ')}${h.shared.length > 5 ? '…' : ''} removed/changed (${ref}..HEAD) — verify the doc still matches.`,
154
+ location: { file: docName },
155
+ suggestion: {
156
+ summary: `Re-read ${docName} against the current ${h.path}; the removed symbols (${h.shared.slice(0, 8).join(', ')}) may now be wrong.`,
157
+ },
158
+ }));
159
+ }
160
+ if (hits.length > maxPerDoc) {
161
+ findings.push(mkFinding({
162
+ code: 'DSP001',
163
+ validator: 'diff-suspicion',
164
+ severity: 'warn',
165
+ confidence: 'low',
166
+ message: `${docName} references ${hits.length - maxPerDoc} more changed file(s) with removed domain symbols (${ref}..HEAD) — a broad change; review ${docName} as a whole.`,
167
+ location: { file: docName },
168
+ suggestion: { summary: `${docName} looks broadly affected by this change set — review it end-to-end rather than line by line.` },
169
+ }));
170
+ }
171
+ }
172
+
173
+ return resultFromFindings(findings, {
174
+ passed: pairsChecked - findings.length,
175
+ total: pairsChecked,
176
+ applicable: true,
177
+ });
178
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Two-Revision Reference-Existence validator (REF001) — v0.31.0.
3
+ *
4
+ * Method (arXiv 2212.01479, field-tested at ~50% maintainer acceptance):
5
+ * extract code-element references from a doc, then compare their existence in
6
+ * the source tree at TWO revisions — the commit where the doc was LAST UPDATED
7
+ * vs HEAD. A reference that matched source when the doc was written but matches
8
+ * ZERO source instances now is flagged outdated. Fully deterministic.
9
+ *
10
+ * Performance: the "present now?" gate is answered from ONE in-memory identifier
11
+ * set built by a single source walk (not a git grep per symbol — that was ~7s
12
+ * on a 167-ref repo). The walk skips dot-directories (.github, .specify), so a
13
+ * symbol the walk misses is CONFIRMED absent with an authoritative `git grep` at
14
+ * HEAD before we trust it — otherwise a symbol living in a dot-dir reads as a
15
+ * false "removed" (caught dogfooding DocGuard's own AGENTS.md). Only symbols
16
+ * absent from BOTH the walk and HEAD git grep — the rare case — pay for the
17
+ * historical `git grep` at the doc's last-update revision.
18
+ *
19
+ * Precision guards:
20
+ * - Only PURE COMPOUND identifiers (camelCase / snake_case / Pascal-multiword)
21
+ * from backticks are checked — never prose words ("`token`" is ignored), and
22
+ * dotted member/file refs are out of scope for v1 (documented).
23
+ * - CLI/config flags (`--foo`) excluded — the "removed but still relevant"
24
+ * false-positive mode (a) from the paper.
25
+ * - present-then-AND-absent-now is required, so a symbol that never existed
26
+ * at doc-time (a typo, an external lib) is not accused — false-positive
27
+ * mode (b) ("literal deleted but logic remains") is bounded by compound
28
+ * shape + the two-revision gate.
29
+ * All findings are confidence:'low' / soft — "verify", never a hard failure.
30
+ */
31
+
32
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
33
+ import { resolve, extname } from 'node:path';
34
+ import { isGitRepo, lastCommitHash, symbolExistsAtRev } from '../shared-git.mjs';
35
+ import { walkFiles } from '../shared-ignore.mjs';
36
+ import { readScannable } from '../shared-source.mjs';
37
+ import { mkFinding, resultFromFindings } from '../findings.mjs';
38
+
39
+ const CODE_EXT = new Set([
40
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java',
41
+ '.kt', '.rb', '.php', '.cs', '.swift', '.scala', '.dart', '.c', '.cpp', '.h',
42
+ ]);
43
+ const IDENT = /[A-Za-z_][A-Za-z0-9_]*/g;
44
+
45
+ // Compound = clearly a code symbol, not a prose word: camelCase, snake_case,
46
+ // or PascalCase-multiword. Pure lowercase single words are excluded.
47
+ const COMPOUND = /[a-z][A-Z]|_|^[A-Z][a-z]+[A-Z]/;
48
+ function isCodeIdentifier(s) {
49
+ if (s.length < 4 || s.length > 80) return false;
50
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s)) return false; // pure identifier (no dots)
51
+ return COMPOUND.test(s);
52
+ }
53
+
54
+ // One pass over the current source tree → Set of every whole identifier present.
55
+ function buildHeadIdentifierSet(projectDir) {
56
+ const set = new Set();
57
+ walkFiles(projectDir, (full) => {
58
+ if (!CODE_EXT.has(extname(full))) return;
59
+ const content = readScannable(full);
60
+ if (!content) return;
61
+ const m = content.match(IDENT);
62
+ if (m) for (const id of m) set.add(id);
63
+ });
64
+ return set;
65
+ }
66
+
67
+ function extractRefs(content) {
68
+ const refs = new Set();
69
+ const backtick = /`([^`\n]{2,80})`/g;
70
+ let m;
71
+ while ((m = backtick.exec(content)) !== null) {
72
+ let tok = m[1].trim().replace(/\(.*$/, '').replace(/[.,;:]+$/, '').trim();
73
+ if (/^-/.test(tok)) continue; // CLI flag → FP mode (a)
74
+ if (isCodeIdentifier(tok)) refs.add(tok);
75
+ }
76
+ return [...refs];
77
+ }
78
+
79
+ function indexDocs(projectDir) {
80
+ const docs = [];
81
+ const push = (name, full) => {
82
+ try {
83
+ const content = readFileSync(full, 'utf-8');
84
+ docs.push({ name, path: full, refs: extractRefs(content) });
85
+ } catch { /* skip */ }
86
+ };
87
+ const docsDir = resolve(projectDir, 'docs-canonical');
88
+ if (existsSync(docsDir)) {
89
+ try {
90
+ for (const f of readdirSync(docsDir)) if (f.endsWith('.md')) push(f, resolve(docsDir, f));
91
+ } catch { /* skip */ }
92
+ }
93
+ for (const agent of ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']) {
94
+ const p = resolve(projectDir, agent);
95
+ if (existsSync(p)) push(agent, p);
96
+ }
97
+ return docs;
98
+ }
99
+
100
+ export function validateReferenceExistence(projectDir, config = {}) {
101
+ const cfg = config.referenceExistence || {};
102
+ const maxRefsPerDoc = cfg.maxRefsPerDoc || 80;
103
+
104
+ if (!isGitRepo(projectDir)) {
105
+ return resultFromFindings([], { passed: 0, total: 0, applicable: false });
106
+ }
107
+ const docs = indexDocs(projectDir);
108
+ if (docs.length === 0 || docs.every(d => d.refs.length === 0)) {
109
+ return resultFromFindings([], { passed: 0, total: 0, applicable: false });
110
+ }
111
+
112
+ const headIds = buildHeadIdentifierSet(projectDir); // cheap gate, built once
113
+ const revPresence = new Map(); // `${sym}\0${rev}` → bool
114
+ // Authoritative HEAD absence (covers dot-dirs the walk skipped). Cached.
115
+ const headAbsent = new Map();
116
+ const confirmedAbsentAtHead = (sym) => {
117
+ if (!headAbsent.has(sym)) headAbsent.set(sym, !symbolExistsAtRev(projectDir, sym, 'HEAD'));
118
+ return headAbsent.get(sym);
119
+ };
120
+
121
+ const findings = [];
122
+ let totalChecked = 0;
123
+ let absentAtHead = 0;
124
+ for (const doc of docs) {
125
+ let rev = null; // resolve lazily — only if a ref is actually absent now
126
+ for (const sym of doc.refs.slice(0, maxRefsPerDoc)) {
127
+ totalChecked++;
128
+ if (headIds.has(sym)) continue; // in the walked tree → present (cheap)
129
+ if (!confirmedAbsentAtHead(sym)) continue; // walk missed it but git finds it (dot-dir) → present
130
+ absentAtHead++;
131
+ if (rev === null) rev = lastCommitHash(projectDir, doc.path) || '';
132
+ if (!rev) continue; // untracked doc → no "then" snapshot
133
+ const key = `${sym}\0${rev}`;
134
+ if (!revPresence.has(key)) revPresence.set(key, symbolExistsAtRev(projectDir, sym, rev));
135
+ if (!revPresence.get(key)) continue; // never existed at doc-time → not our signal
136
+ findings.push(mkFinding({
137
+ code: 'REF001',
138
+ validator: 'reference-existence',
139
+ severity: 'warn',
140
+ confidence: 'low',
141
+ message: `${doc.name} references \`${sym}\`, which existed in the code when the doc was last updated but has ZERO matches at HEAD — likely renamed or removed.`,
142
+ location: { file: doc.name },
143
+ suggestion: {
144
+ summary: `Update or remove the \`${sym}\` reference in ${doc.name} (or suppress if it is a still-relevant user-facing name).`,
145
+ },
146
+ }));
147
+ }
148
+ }
149
+
150
+ const res = resultFromFindings(findings, {
151
+ passed: totalChecked - findings.length,
152
+ total: totalChecked,
153
+ applicable: true,
154
+ });
155
+ res.absentAtHead = absentAtHead; // instrumentation: proves the pipeline reaches the rev check
156
+ return res;
157
+ }
@@ -18,6 +18,25 @@ import { resolve, join, relative, basename, extname } from 'node:path';
18
18
  import { TRACE_MAP, TEST_PATTERNS, isTraceableSource } from '../shared-trace-patterns.mjs';
19
19
  import { walkFiles as sharedWalkFiles } from '../shared-ignore.mjs';
20
20
  import { mkFinding, resultFromFindings } from '../findings.mjs';
21
+ import { tokenize } from '../shared-diff.mjs';
22
+ import { rankBySimilarity } from '../shared-ir.mjs';
23
+
24
+ // IR soft-link recovery (feat 5): tokenize test files once so an untraced
25
+ // requirement can be matched to the test that most likely already covers it
26
+ // (TF-IDF cosine, VSM). Capped so a huge test suite can't blow up guard.
27
+ function buildTestCorpus(projectDir, projectFiles, { maxFiles = 250, maxTokens = 400 } = {}) {
28
+ const testFiles = projectFiles.filter(f =>
29
+ TEST_PATTERNS.some(p => p.test(f)) || /__tests__\//.test(f) || /tests?\//.test(f)
30
+ ).slice(0, maxFiles);
31
+ const corpus = [];
32
+ for (const relPath of testFiles) {
33
+ try {
34
+ const content = readFileSync(resolve(projectDir, relPath), 'utf-8');
35
+ corpus.push({ id: relPath, tokens: tokenize(content).slice(0, maxTokens) });
36
+ } catch { /* skip unreadable */ }
37
+ }
38
+ return corpus;
39
+ }
21
40
 
22
41
  const IGNORE_DIRS = new Set([
23
42
  'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
@@ -208,20 +227,40 @@ function validateRequirementTraceability(projectDir, config, projectFiles) {
208
227
 
209
228
  // ── Step 3: Report traceability results ──
210
229
 
230
+ // IR soft-link recovery (feat 5): build the tokenized test corpus once, only
231
+ // if there are untraced requirements to match. Threshold is deliberately low
232
+ // — short requirement text vs a whole test file yields modest cosine scores;
233
+ // the hint is a suggestion, not proof.
234
+ const softThreshold = config.traceability?.irSoftThreshold ?? 0.10;
235
+ let testCorpus = null;
236
+
211
237
  // Check each documented requirement has at least one test reference
212
238
  for (const [reqId, location] of reqIds) {
213
239
  total++;
214
240
  if (testRefs.has(reqId)) {
215
241
  passed++;
216
242
  } else {
243
+ // Try to recover a likely-but-unannotated test via TF-IDF cosine.
244
+ let softHint = '';
245
+ let softText = `Add an @req ${reqId} comment to the test that verifies this requirement`;
246
+ const queryText = location.text && location.text.length > reqId.length ? location.text : reqId;
247
+ if (testCorpus === null) testCorpus = buildTestCorpus(projectDir, projectFiles);
248
+ if (testCorpus.length > 0) {
249
+ const ranked = rankBySimilarity(tokenize(queryText), testCorpus);
250
+ const top = ranked[0];
251
+ if (top && top.score >= softThreshold) {
252
+ const pct = (top.score * 100).toFixed(0);
253
+ softHint = ` — IR soft-match: ${top.id} (${pct}% similar) may already cover it`;
254
+ softText = `${top.id} looks like it already tests this (${pct}% similar) — add @req ${reqId} there, or if unrelated, write the missing test`;
255
+ }
256
+ }
217
257
  findings.push(mkFinding({
218
258
  code: 'TRC004',
219
259
  validator: 'traceability',
220
260
  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`,
261
+ message: `Requirement ${reqId} (${location.file}:${location.line}) has no test coverage.${softHint || ' Add @req ' + reqId + ' comment to the test that verifies this requirement'}`,
223
262
  location: `${location.file}:${location.line}`,
224
- suggestion: { kind: 'fix', text: `Add an @req ${reqId} comment to the test that verifies this requirement` },
263
+ suggestion: { kind: 'fix', text: softText },
225
264
  }));
226
265
  }
227
266
  }
@@ -269,7 +308,8 @@ function collectRequirementIds(projectDir, config, patterns) {
269
308
  while ((match = pattern.exec(lines[i])) !== null) {
270
309
  const reqId = match[0]; // e.g., "REQ-001"
271
310
  if (!reqIds.has(reqId)) {
272
- reqIds.set(reqId, { file: docName, line: i + 1 });
311
+ // capture the line text (the requirement description) for IR soft-match
312
+ reqIds.set(reqId, { file: docName, line: i + 1, text: lines[i].trim() });
273
313
  }
274
314
  }
275
315
  }
@@ -68,7 +68,7 @@ diagnose → AI reads prompts → AI fixes docs → guard verifies
68
68
  ## Verify
69
69
 
70
70
  ```bash
71
- npx docguard-cli guard # Pass/fail check (24 validators)
71
+ npx docguard-cli guard # Pass/fail check (27 validators)
72
72
  npx docguard-cli score # 0-100 maturity score
73
73
  ```
74
74
 
@@ -3,7 +3,7 @@ schema_version: "1.0"
3
3
  extension:
4
4
  id: "docguard"
5
5
  name: "DocGuard — CDD Enforcement"
6
- version: "0.30.0"
6
+ version: "0.31.0"
7
7
  description: "Canonical-Driven Development enforcement as a true spec-kit extension. LLM-first design with automated validators, 4 AI behavior skills, spec-kit skill chaining, and workflow hooks. One pinned runtime dependency (@babel/parser); pure Node.js otherwise."
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.30.0
9
+ version: 0.31.0
10
10
  source: extensions/spec-kit-docguard/skills/docguard-fix
11
11
  ---
12
- <!-- docguard:version: 0.30.0 -->
12
+ <!-- docguard:version: 0.31.0 -->
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.30.0
10
+ version: 0.31.0
11
11
  source: extensions/spec-kit-docguard/skills/docguard-guard
12
12
  ---
13
- <!-- docguard:version: 0.30.0 -->
13
+ <!-- docguard:version: 0.31.0 -->
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.30.0
9
+ version: 0.31.0
10
10
  source: extensions/spec-kit-docguard/skills/docguard-review
11
11
  ---
12
- <!-- docguard:version: 0.30.0 -->
12
+ <!-- docguard:version: 0.31.0 -->
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.30.0
9
+ version: 0.31.0
10
10
  source: extensions/spec-kit-docguard/skills/docguard-score
11
11
  ---
12
- <!-- docguard:version: 0.30.0 -->
12
+ <!-- docguard:version: 0.31.0 -->
13
13
 
14
14
  # DocGuard Score Skill
15
15
 
@@ -4,10 +4,10 @@ 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.30.0
7
+ version: 0.31.0
8
8
  source: extensions/spec-kit-docguard/skills/docguard-sync
9
9
  ---
10
- <!-- docguard:version: 0.30.0 -->
10
+ <!-- docguard:version: 0.31.0 -->
11
11
 
12
12
  # DocGuard Sync Skill
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.30.1",
3
+ "version": "0.31.0",
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": {