docguard-cli 0.30.0 → 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.
- package/README.md +11 -6
- package/cli/commands/explain.mjs +34 -0
- package/cli/commands/guard.mjs +8 -0
- package/cli/commands/impact.mjs +93 -12
- package/cli/commands/mcp.mjs +20 -0
- package/cli/commands/score.mjs +9 -4
- package/cli/commands/setup.mjs +0 -13
- package/cli/commands/verify.mjs +63 -1
- package/cli/config.mjs +7 -0
- package/cli/findings.mjs +26 -0
- package/cli/shared-diff.mjs +209 -0
- package/cli/shared-git.mjs +101 -1
- package/cli/shared-ir.mjs +81 -0
- package/cli/validators/api-doc-smells.mjs +143 -0
- package/cli/validators/diff-suspicion.mjs +178 -0
- package/cli/validators/reference-existence.mjs +157 -0
- package/cli/validators/traceability.mjs +44 -4
- package/docs/quickstart.md +1 -1
- 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/README.md
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
[](https://nodejs.org)
|
|
14
14
|
[-green)](package.json)
|
|
15
15
|
[](https://github.com/github/spec-kit)
|
|
16
|
+
[](https://glama.ai/mcp/servers/raccioly/docguard)
|
|
17
|
+
[](https://registry.modelcontextprotocol.io/)
|
|
16
18
|
|
|
17
19
|
---
|
|
18
20
|
|
|
@@ -74,7 +76,7 @@ graph TD
|
|
|
74
76
|
Commands --> setup["setup wizard"]
|
|
75
77
|
Commands --> other["diff · init · fix · trace · impact · sync<br/>explain · memory · upgrade · agents · hooks · badge · ci · watch"]
|
|
76
78
|
|
|
77
|
-
guard --> Validators["Validators (
|
|
79
|
+
guard --> Validators["Validators (27)"]
|
|
78
80
|
generate --> Scanners["Scanners (4)<br/>routes · schemas · doc-tools · speckit"]
|
|
79
81
|
score --> Scoring["Weighted Scoring<br/>8 categories"]
|
|
80
82
|
diagnose --> Validators
|
|
@@ -252,7 +254,7 @@ DocGuard ships **18 commands** (the "Daily 5" + 13 situational tools, including
|
|
|
252
254
|
| Command | What It Does |
|
|
253
255
|
|:--------|:-------------|
|
|
254
256
|
| `init` | Bootstrap a project (`--wizard` for interactive · `--with <name>` for scaffolders) |
|
|
255
|
-
| `guard` | Validate against canonical docs —
|
|
257
|
+
| `guard` | Validate against canonical docs — 27 validators |
|
|
256
258
|
| `diff` | Show gaps between docs and code (`--since <ref>` for impact mode) |
|
|
257
259
|
| `sync` | Refresh code-truth doc sections — keeps memory always up to date |
|
|
258
260
|
| `score` | CDD maturity score (0-100; `--diff` for delta between refs) |
|
|
@@ -358,7 +360,7 @@ $ npx docguard-cli generate
|
|
|
358
360
|
|
|
359
361
|
## 🔍 Validators
|
|
360
362
|
|
|
361
|
-
DocGuard runs **
|
|
363
|
+
DocGuard runs **27 automated validators** on every `guard` check. Every one is **language-aware** as of v0.16 — patterns for Python (`test_*.py`), Rust (`tests/*.rs`), Go (`*_test.go`), Java (`*Test.java`), Ruby (`*_spec.rb`), PHP, and JS/TS all match.
|
|
362
364
|
|
|
363
365
|
| # | Validator | What It Checks | Default |
|
|
364
366
|
|:--|:----------|:--------------|:--------|
|
|
@@ -385,7 +387,10 @@ DocGuard runs **24 automated validators** on every `guard` check. Every one is *
|
|
|
385
387
|
| 21 | **Generated-Staleness** | `source=code` sections match scanner output; `status: draft` doc age | ✅ On |
|
|
386
388
|
| 22 | **Canonical-Sync** | DocGuard's own README count claims match code-truth (DocGuard repo only — N/A elsewhere) | ✅ On |
|
|
387
389
|
| 23 | **Metrics-Consistency** | Hardcoded numbers match actual counts | ✅ On |
|
|
388
|
-
| 24 | **Surface-Sync** | Item-level enumerable drift — names in doc tables/lists (commands,
|
|
390
|
+
| 24 | **Surface-Sync** | Item-level enumerable drift — names in doc tables/lists (commands, checks, etc.) match code-truth (opt-in via `surfaceSync.surfaces`; N/A unless configured) | ✅ On |
|
|
391
|
+
| 25 | **Diff-Suspicion** | Change-driven: a doc/agent-instruction file that references code changed since the ref AND shares removed domain symbols is flagged for review (arXiv 2010.01625, F1 74.7) | ✅ On |
|
|
392
|
+
| 26 | **Reference-Existence** | Two-revision check: a backticked code symbol present when the doc was last updated but gone at HEAD is flagged as outdated (arXiv 2212.01479) | ✅ On |
|
|
393
|
+
| 27 | **API-Doc-Smells** | Bloated (≥300 words) / Lazy (≤6 prose words) API documentation units, keyed on signature-headed sections (F1 0.90/0.95) | ✅ On |
|
|
389
394
|
|
|
390
395
|
**Per-validator controls** (in `.docguard.json`):
|
|
391
396
|
```json
|
|
@@ -454,7 +459,7 @@ DocGuard provides AI agent slash commands for integrated workflows. Installed au
|
|
|
454
459
|
| Command | What It Does |
|
|
455
460
|
|:--------|:-------------|
|
|
456
461
|
| `/docguard.init` | Initialize Canonical-Driven Development in a new or existing project |
|
|
457
|
-
| `/docguard.guard` | Run quality validation — check all
|
|
462
|
+
| `/docguard.guard` | Run quality validation — check all 27 validators |
|
|
458
463
|
| `/docguard.review` | Analyze doc quality and suggest improvements |
|
|
459
464
|
| `/docguard.fix` | Generate targeted fix prompts for specific issues |
|
|
460
465
|
| `/docguard.update` | Update canonical docs after code changes — detect drift and sync documentation |
|
|
@@ -613,7 +618,7 @@ Two ready-to-use templates ship with the Spec Kit extension and as standalone fi
|
|
|
613
618
|
Recent highlights across the v0.16 → v0.19 line:
|
|
614
619
|
|
|
615
620
|
- **`docguard explain <validator>`** — `docguard explain freshness` prints purpose, rules, common
|
|
616
|
-
failures, and fix recipes for any of the
|
|
621
|
+
failures, and fix recipes for any of the 27 validators. No need to dig into source.
|
|
617
622
|
- **`docguard memory --diff`** — surface what changed in your canonical docs between two refs
|
|
618
623
|
(`HEAD~10..HEAD` by default). Great for code review and changelog drafting.
|
|
619
624
|
- **`docguard score --diff`** — see exactly which validators moved the score up or down between
|
package/cli/commands/explain.mjs
CHANGED
|
@@ -178,6 +178,37 @@ const EXPLAINERS = {
|
|
|
178
178
|
example: '`cli/commands/demo.mjs` exists and `| `demo` | Zero-install preview |` appears in README.md\'s commands table',
|
|
179
179
|
standard: 'CDD principle: documented surfaces match implemented surfaces',
|
|
180
180
|
},
|
|
181
|
+
diffSuspicion: {
|
|
182
|
+
title: 'Diff-Suspicion — docs describing code that just changed (DSP001)',
|
|
183
|
+
what: 'Change-driven. For each code file changed since the ref (`config.changedSinceRef`, else HEAD~1), flags a canonical doc or agent-instruction file that BOTH references the file (path/`module`) AND shares domain tokens that were removed/replaced in the diff. Deterministic diff-overlap rule (arXiv 2010.01625, F1 74.7); confidence:low.',
|
|
184
|
+
why: 'A doc that talks about a symbol the code just deleted is the highest-signal, lowest-cost drift class — and it is exactly what whole-repo scans miss but a diff makes obvious.',
|
|
185
|
+
triggers: [
|
|
186
|
+
['describes X, which just had … removed/changed', 'Re-read the doc against the current file; the removed symbols may now be wrong. Suppress the pairing if it is a false positive.'],
|
|
187
|
+
],
|
|
188
|
+
example: 'AUTH.md references `src/auth.ts` and `validateToken` was renamed away in the last commit',
|
|
189
|
+
standard: 'Just-in-time comment-code inconsistency detection (AAAI 2021)',
|
|
190
|
+
},
|
|
191
|
+
referenceExistence: {
|
|
192
|
+
title: 'Reference-Existence — doc names a code symbol that no longer exists (REF001)',
|
|
193
|
+
what: 'Two-revision check. A compound code identifier backticked in a doc that existed in the source when the doc was last updated but has ZERO whole-word matches at HEAD is flagged as outdated. Deterministic (arXiv 2212.01479); confidence:low.',
|
|
194
|
+
why: 'References go stale silently for years; the two-revision gate distinguishes "renamed away" from "never existed", keeping false positives near zero.',
|
|
195
|
+
triggers: [
|
|
196
|
+
['references `X`, which existed … but has ZERO matches at HEAD', 'Update or remove the reference. Suppress with `<!-- docguard:ignore REF001 -->` if it is a still-relevant user-facing name.'],
|
|
197
|
+
],
|
|
198
|
+
example: 'API-REFERENCE.md backticks `getUserById` which was deleted three commits ago',
|
|
199
|
+
standard: 'Outdated code-element reference detection (EMSE 2022)',
|
|
200
|
+
},
|
|
201
|
+
apiDocSmells: {
|
|
202
|
+
title: 'API-Doc-Smells — Bloated / Lazy API documentation (APS001/APS002)',
|
|
203
|
+
what: 'Length signals on doc units whose heading is a code signature (HTTP endpoint / function / backticked symbol). Lazy = ≤6 prose words (documented in name only); Bloated = ≥300 words for one unit. Deterministic (F1 0.90/0.95); confidence:low. OFF by default — enable with `validators.apiDocSmells: true`.',
|
|
204
|
+
why: 'An endpoint documented in name only, or buried in 400 words, both fail the reader — and both are detectable without understanding the prose.',
|
|
205
|
+
triggers: [
|
|
206
|
+
['documented in name only', 'Describe what it does, its params, return, and errors — not just the signature.'],
|
|
207
|
+
['is N words for one unit — Bloated', 'Trim to the essential contract; move examples/edge-cases elsewhere.'],
|
|
208
|
+
],
|
|
209
|
+
example: '`#### GET /api/health` with an empty body → Lazy',
|
|
210
|
+
standard: 'API documentation smell taxonomy',
|
|
211
|
+
},
|
|
181
212
|
crossReference: {
|
|
182
213
|
title: 'Cross-Reference — internal markdown links resolve',
|
|
183
214
|
what: 'Scans canonical docs for `[text](./OTHER.md#anchor)` and `#anchor` links. Verifies the target file exists and the anchor matches a heading.',
|
|
@@ -351,6 +382,9 @@ const DISPLAY_NAMES = {
|
|
|
351
382
|
surfaceSync: 'Surface-Sync',
|
|
352
383
|
canonicalSync: 'Canonical-Sync',
|
|
353
384
|
metricsConsistency: 'Metrics-Consistency',
|
|
385
|
+
diffSuspicion: 'Diff-Suspicion',
|
|
386
|
+
referenceExistence: 'Reference-Existence',
|
|
387
|
+
apiDocSmells: 'API-Doc-Smells',
|
|
354
388
|
};
|
|
355
389
|
|
|
356
390
|
/** Collapse a key / display name to a comparable form: lowercase, alnum only. */
|
package/cli/commands/guard.mjs
CHANGED
|
@@ -147,6 +147,10 @@ import { validateSchemaSync } from '../validators/schema-sync.mjs';
|
|
|
147
147
|
import { validateSpecKitIntegration } from '../validators/spec-kit.mjs';
|
|
148
148
|
import { validateCanonicalSync } from '../validators/canonical-sync.mjs';
|
|
149
149
|
import { validateSurfaceSync } from '../validators/surface-sync.mjs';
|
|
150
|
+
// v0.31.0 change-driven + smell detectors
|
|
151
|
+
import { validateDiffSuspicion } from '../validators/diff-suspicion.mjs';
|
|
152
|
+
import { validateReferenceExistence } from '../validators/reference-existence.mjs';
|
|
153
|
+
import { validateApiDocSmells } from '../validators/api-doc-smells.mjs';
|
|
150
154
|
|
|
151
155
|
/**
|
|
152
156
|
* Internal guard — returns structured data, no console output, no process.exit.
|
|
@@ -313,6 +317,10 @@ export function runGuardInternal(projectDir, config) {
|
|
|
313
317
|
{ key: 'crossReference', name: 'Cross-Reference', fn: () => validateCrossReferences(projectDir, config) },
|
|
314
318
|
{ key: 'generatedStaleness', name: 'Generated-Staleness', fn: () => validateGeneratedStaleness(projectDir, config) },
|
|
315
319
|
{ key: 'surfaceSync', name: 'Surface-Sync', fn: () => validateSurfaceSync(projectDir, config) },
|
|
320
|
+
// v0.31.0 — change-driven + smell detectors (all confidence:low / soft).
|
|
321
|
+
{ key: 'diffSuspicion', name: 'Diff-Suspicion', fn: () => validateDiffSuspicion(projectDir, config) },
|
|
322
|
+
{ key: 'referenceExistence', name: 'Reference-Existence', fn: () => validateReferenceExistence(projectDir, config) },
|
|
323
|
+
{ key: 'apiDocSmells', name: 'API-Doc-Smells', fn: () => validateApiDocSmells(projectDir, config) },
|
|
316
324
|
// Metrics-Consistency runs post-loop (needs guard results)
|
|
317
325
|
];
|
|
318
326
|
|
package/cli/commands/impact.mjs
CHANGED
|
@@ -15,10 +15,21 @@
|
|
|
15
15
|
* JSON mode emits a structured `{ changedFiles, affectedDocs }` payload
|
|
16
16
|
* for CI integrations and PR-comment bots.
|
|
17
17
|
*
|
|
18
|
+
* v0.31.0 — blast radius (feat 1):
|
|
19
|
+
* - Agent-instruction files (AGENTS.md/CLAUDE.md/GEMINI.md) are indexed
|
|
20
|
+
* alongside canonical docs, so a changed code file they reference is
|
|
21
|
+
* surfaced too (agent instructions drift when the code they describe moves).
|
|
22
|
+
* - Doc→doc graph: when a DOC changes, the docs that reference it — INCLUDING
|
|
23
|
+
* agent-instruction files — are flagged as suspect ("blast radius"). This is
|
|
24
|
+
* the unclaimed slice: a change in ARCHITECTURE.md marks the AGENTS.md that
|
|
25
|
+
* points at it for review.
|
|
26
|
+
*
|
|
18
27
|
* @req SC-S11-001 — impact reports per-file → doc mappings
|
|
19
28
|
* @req SC-S11-002 — files with no doc references are listed as "no impact"
|
|
20
29
|
* @req SC-S11-003 — --format json emits parseable structured output
|
|
21
30
|
* @req SC-S11-004 — non-code files (.md, .json, etc.) are skipped from impact analysis
|
|
31
|
+
* @req SC-S11-007 — agent-instruction files participate in impact analysis
|
|
32
|
+
* @req SC-S11-008 — a changed doc flags the docs that reference it (blast radius)
|
|
22
33
|
*/
|
|
23
34
|
|
|
24
35
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
@@ -33,10 +44,28 @@ import { changedFilesSince, isGitRepo } from '../shared-git.mjs';
|
|
|
33
44
|
*/
|
|
34
45
|
const CODE_EXTENSIONS = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift)$/;
|
|
35
46
|
|
|
47
|
+
// Root agent-instruction files — documentation that names code and other docs.
|
|
48
|
+
const AGENT_FILES = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
|
|
49
|
+
|
|
36
50
|
function escapeRegex(s) {
|
|
37
51
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
38
52
|
}
|
|
39
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Doc→doc references: which indexed docs reference `changedDocPath` (by its
|
|
56
|
+
* basename — the form used in prose "see ARCHITECTURE.md" and markdown links
|
|
57
|
+
* `](ARCHITECTURE.md)`). Skips self. This is the blast-radius edge set.
|
|
58
|
+
*/
|
|
59
|
+
function docsReferencing(changedDocPath, index) {
|
|
60
|
+
const cbase = basename(changedDocPath);
|
|
61
|
+
const dependents = [];
|
|
62
|
+
for (const [docName, lines] of index) {
|
|
63
|
+
if (docName === cbase || docName === changedDocPath) continue; // not self
|
|
64
|
+
if (lines.some(l => l.includes(cbase))) dependents.push(docName);
|
|
65
|
+
}
|
|
66
|
+
return dependents;
|
|
67
|
+
}
|
|
68
|
+
|
|
40
69
|
/**
|
|
41
70
|
* Find canonical doc references for a single file. Reuses the same three
|
|
42
71
|
* match strategies as trace --reverse for consistency: direct path,
|
|
@@ -81,9 +110,11 @@ export function runImpact(projectDir, _config, flags) {
|
|
|
81
110
|
// impact" in the same sense; they ARE the docs (or config).
|
|
82
111
|
const codeChanged = changed.filter(f => CODE_EXTENSIONS.test(f));
|
|
83
112
|
|
|
84
|
-
// Index canonical docs once
|
|
113
|
+
// Index canonical docs once, PLUS root agent-instruction files (they name
|
|
114
|
+
// code and other docs, so they belong in both code→doc and doc→doc analysis).
|
|
85
115
|
const docsDir = resolve(projectDir, 'docs-canonical');
|
|
86
116
|
const docsIndex = new Map(); // docName → lines[]
|
|
117
|
+
const agentDocs = new Set(); // which indexed docs are agent-instruction files
|
|
87
118
|
if (existsSync(docsDir)) {
|
|
88
119
|
try {
|
|
89
120
|
for (const f of readdirSync(docsDir)) {
|
|
@@ -95,6 +126,11 @@ export function runImpact(projectDir, _config, flags) {
|
|
|
95
126
|
}
|
|
96
127
|
} catch { /* skip if dir unreadable */ }
|
|
97
128
|
}
|
|
129
|
+
for (const a of AGENT_FILES) {
|
|
130
|
+
const p = resolve(projectDir, a);
|
|
131
|
+
if (!existsSync(p)) continue;
|
|
132
|
+
try { docsIndex.set(a, readFileSync(p, 'utf-8').split('\n')); agentDocs.add(a); } catch { /* skip */ }
|
|
133
|
+
}
|
|
98
134
|
|
|
99
135
|
// Compute per-file references
|
|
100
136
|
const fileImpact = []; // { file, references: [{doc, line, kind}] }
|
|
@@ -113,15 +149,38 @@ export function runImpact(projectDir, _config, flags) {
|
|
|
113
149
|
const affectedDocs = Array.from(docMap.entries()).map(([doc, files]) => ({
|
|
114
150
|
doc,
|
|
115
151
|
files: Array.from(files),
|
|
152
|
+
isAgentFile: agentDocs.has(doc),
|
|
116
153
|
}));
|
|
117
154
|
|
|
155
|
+
// ── Doc→doc blast radius: a changed DOC flags the docs that reference it ──
|
|
156
|
+
// (including agent-instruction files that point at it). Only meaningful edges
|
|
157
|
+
// are emitted (changed doc with ≥1 dependent).
|
|
158
|
+
//
|
|
159
|
+
// A source must be an INDEXED canonical/agent doc — not any changed `.md`.
|
|
160
|
+
// Otherwise a CHANGELOG.md / README.md / .wolf/*.md change flags every doc
|
|
161
|
+
// that merely mentions it in passing (dogfooding false positives).
|
|
162
|
+
const indexBasenames = new Set(docsIndex.keys());
|
|
163
|
+
const changedDocs = changed.filter(f => f.endsWith('.md') && indexBasenames.has(basename(f)));
|
|
164
|
+
const blastRadius = [];
|
|
165
|
+
for (const cd of changedDocs) {
|
|
166
|
+
const dependents = docsReferencing(cd, docsIndex);
|
|
167
|
+
if (dependents.length > 0) {
|
|
168
|
+
blastRadius.push({
|
|
169
|
+
changedDoc: cd,
|
|
170
|
+
dependents: dependents.map(d => ({ doc: d, isAgentFile: agentDocs.has(d) })),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
118
175
|
// ── JSON output ──
|
|
119
176
|
if (isJson) {
|
|
120
177
|
console.log(JSON.stringify({
|
|
121
178
|
since,
|
|
122
179
|
changedFiles: codeChanged,
|
|
123
|
-
|
|
180
|
+
changedDocs,
|
|
181
|
+
ignoredFiles: changed.filter(f => !CODE_EXTENSIONS.test(f) && !f.endsWith('.md')),
|
|
124
182
|
affectedDocs,
|
|
183
|
+
blastRadius,
|
|
125
184
|
timestamp: new Date().toISOString(),
|
|
126
185
|
}, null, 2));
|
|
127
186
|
return;
|
|
@@ -134,8 +193,28 @@ export function runImpact(projectDir, _config, flags) {
|
|
|
134
193
|
console.log(` ${c.green}✅ No file changes since ${since}.${c.reset}`);
|
|
135
194
|
return;
|
|
136
195
|
}
|
|
196
|
+
|
|
197
|
+
// Doc→doc blast radius — shown whether or not code changed (a doc-only change
|
|
198
|
+
// can still ripple to the docs / agent files that reference it).
|
|
199
|
+
const printBlast = () => {
|
|
200
|
+
if (blastRadius.length === 0) return;
|
|
201
|
+
console.log(`\n ${c.bold}🌐 Doc blast radius${c.reset} ${c.dim}(${changedDocs.length} doc(s) changed)${c.reset}`);
|
|
202
|
+
for (const { changedDoc, dependents } of blastRadius) {
|
|
203
|
+
console.log(` ${c.cyan}${changedDoc}${c.reset} ${c.dim}changed → review ${dependents.length} dependent doc(s):${c.reset}`);
|
|
204
|
+
for (const dep of dependents.slice(0, 8)) {
|
|
205
|
+
const tag = dep.isAgentFile ? ` ${c.yellow}[agent-instruction]${c.reset}` : '';
|
|
206
|
+
console.log(` ${c.dim}↳${c.reset} ${dep.doc}${tag}`);
|
|
207
|
+
}
|
|
208
|
+
if (dependents.length > 8) console.log(` ${c.dim}... ${dependents.length - 8} more${c.reset}`);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
137
212
|
if (codeChanged.length === 0) {
|
|
138
|
-
console.log(` ${c.dim}No code files changed (${
|
|
213
|
+
console.log(` ${c.dim}No code files changed (${changedDocs.length} doc(s) + ${changed.length - changedDocs.length} other non-code file(s)).${c.reset}`);
|
|
214
|
+
if (blastRadius.length === 0 && changedDocs.length > 0) {
|
|
215
|
+
console.log(` ${c.green}✅ No other docs reference the changed doc(s).${c.reset}`);
|
|
216
|
+
}
|
|
217
|
+
printBlast();
|
|
139
218
|
return;
|
|
140
219
|
}
|
|
141
220
|
|
|
@@ -146,16 +225,16 @@ export function runImpact(projectDir, _config, flags) {
|
|
|
146
225
|
console.log(` ${c.dim}This often means the changed code is undocumented. Consider:${c.reset}`);
|
|
147
226
|
console.log(` ${c.dim} - Running ${c.cyan}docguard generate --plan${c.dim} to add doc skeletons${c.reset}`);
|
|
148
227
|
console.log(` ${c.dim} - Reviewing whether the change belongs in an existing doc${c.reset}`);
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
228
|
+
} else {
|
|
229
|
+
console.log(` ${c.green}${affectedDocs.length}${c.reset} canonical doc(s) reference the changed files:\n`);
|
|
230
|
+
for (const { doc, files, isAgentFile } of affectedDocs) {
|
|
231
|
+
const tag = isAgentFile ? ` ${c.yellow}[agent-instruction]${c.reset}` : '';
|
|
232
|
+
console.log(` ${c.cyan}${doc}${c.reset}${tag} ${c.dim}(${files.length} file${files.length > 1 ? 's' : ''})${c.reset}`);
|
|
233
|
+
for (const f of files.slice(0, 5)) {
|
|
234
|
+
console.log(` ${c.dim}via${c.reset} ${f}`);
|
|
235
|
+
}
|
|
236
|
+
if (files.length > 5) console.log(` ${c.dim}... ${files.length - 5} more${c.reset}`);
|
|
157
237
|
}
|
|
158
|
-
if (files.length > 5) console.log(` ${c.dim}... ${files.length - 5} more${c.reset}`);
|
|
159
238
|
}
|
|
160
239
|
|
|
161
240
|
// List code files with NO doc references — these may need new docs
|
|
@@ -166,4 +245,6 @@ export function runImpact(projectDir, _config, flags) {
|
|
|
166
245
|
if (orphaned.length > 5) console.log(` ${c.dim}... ${orphaned.length - 5} more${c.reset}`);
|
|
167
246
|
console.log(` ${c.dim}These may be undocumented — review whether they belong in an existing doc.${c.reset}`);
|
|
168
247
|
}
|
|
248
|
+
|
|
249
|
+
printBlast();
|
|
169
250
|
}
|
package/cli/commands/mcp.mjs
CHANGED
|
@@ -53,25 +53,40 @@ const PROJECT_DIR_PROP = {
|
|
|
53
53
|
},
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
// Every DocGuard MCP tool is READ-ONLY: it inspects local project files and
|
|
57
|
+
// never writes, mutates, or reaches the network. These MCP tool hints let
|
|
58
|
+
// clients (and directory scanners like Glama) surface that safety to users.
|
|
59
|
+
const READONLY_ANNOTATIONS = {
|
|
60
|
+
readOnlyHint: true,
|
|
61
|
+
destructiveHint: false,
|
|
62
|
+
idempotentHint: true,
|
|
63
|
+
openWorldHint: false,
|
|
64
|
+
};
|
|
65
|
+
|
|
56
66
|
const TOOLS = [
|
|
57
67
|
{
|
|
58
68
|
name: 'docguard_guard',
|
|
69
|
+
title: 'Guard docs against code',
|
|
59
70
|
description: 'Run every enabled DocGuard validator against the project\'s canonical docs. Returns the full guard JSON contract: status (PASS/WARN/FAIL), structured findings with stable codes and suggestions, nextStep, doc coverage map, semantic-claim count, and per-validator results.',
|
|
60
71
|
inputSchema: {
|
|
61
72
|
type: 'object',
|
|
62
73
|
properties: { ...PROJECT_DIR_PROP },
|
|
63
74
|
},
|
|
75
|
+
annotations: READONLY_ANNOTATIONS,
|
|
64
76
|
},
|
|
65
77
|
{
|
|
66
78
|
name: 'docguard_score',
|
|
79
|
+
title: 'CDD maturity score',
|
|
67
80
|
description: 'Compute the project\'s CDD maturity score (0-100) with letter grade and per-category breakdown.',
|
|
68
81
|
inputSchema: {
|
|
69
82
|
type: 'object',
|
|
70
83
|
properties: { ...PROJECT_DIR_PROP },
|
|
71
84
|
},
|
|
85
|
+
annotations: READONLY_ANNOTATIONS,
|
|
72
86
|
},
|
|
73
87
|
{
|
|
74
88
|
name: 'docguard_explain',
|
|
89
|
+
title: 'Explain a finding code',
|
|
75
90
|
description: 'Explain a stable DocGuard finding code (e.g. STR001, ENV003): what it means, which validator emits it, and the inline suppression to use if it\'s a confirmed false positive.',
|
|
76
91
|
inputSchema: {
|
|
77
92
|
type: 'object',
|
|
@@ -83,22 +98,27 @@ const TOOLS = [
|
|
|
83
98
|
},
|
|
84
99
|
required: ['code'],
|
|
85
100
|
},
|
|
101
|
+
annotations: READONLY_ANNOTATIONS,
|
|
86
102
|
},
|
|
87
103
|
{
|
|
88
104
|
name: 'docguard_verify_claims',
|
|
105
|
+
title: 'Extract claims to verify',
|
|
89
106
|
description: 'Extract the semantic claims in the project\'s canonical docs — documented numbers, limits, and enums — as a verification task list. Deterministic discovery, LLM judgment — the caller verifies each claim against the code.',
|
|
90
107
|
inputSchema: {
|
|
91
108
|
type: 'object',
|
|
92
109
|
properties: { ...PROJECT_DIR_PROP },
|
|
93
110
|
},
|
|
111
|
+
annotations: READONLY_ANNOTATIONS,
|
|
94
112
|
},
|
|
95
113
|
{
|
|
96
114
|
name: 'docguard_diagnose',
|
|
115
|
+
title: 'Diagnose what to fix',
|
|
97
116
|
description: 'Run guard and return only what needs fixing: failing/warning validators with their messages, structured findings, and suggested next actions — shaped for an agent to act on.',
|
|
98
117
|
inputSchema: {
|
|
99
118
|
type: 'object',
|
|
100
119
|
properties: { ...PROJECT_DIR_PROP },
|
|
101
120
|
},
|
|
121
|
+
annotations: READONLY_ANNOTATIONS,
|
|
102
122
|
},
|
|
103
123
|
];
|
|
104
124
|
|
package/cli/commands/score.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
7
7
|
import { resolve, join, extname } from 'node:path';
|
|
8
|
-
import {
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
9
|
import { c, docHasSection } from '../shared.mjs';
|
|
10
10
|
import { validateSecurity } from '../validators/security.mjs';
|
|
11
11
|
import { runGuardInternal } from './guard.mjs';
|
|
@@ -1039,11 +1039,16 @@ function estimateDocTax(projectDir, config, scores) {
|
|
|
1039
1039
|
// Estimate code churn (commits in last 30 days)
|
|
1040
1040
|
let recentCommits = 0;
|
|
1041
1041
|
try {
|
|
1042
|
-
|
|
1042
|
+
// execFileSync (argv array) + count in JS — avoids the shell `| wc -l` pipe,
|
|
1043
|
+
// which isn't portable to Windows (no `wc`) and needs a shell at all. Same
|
|
1044
|
+
// pattern freshness.mjs already uses for commit counting.
|
|
1045
|
+
const output = execFileSync('git', ['log', '--oneline', '--since=30 days ago'], {
|
|
1043
1046
|
cwd: projectDir,
|
|
1044
1047
|
encoding: 'utf-8',
|
|
1045
|
-
|
|
1046
|
-
|
|
1048
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
1049
|
+
maxBuffer: 1024 * 1024 * 5,
|
|
1050
|
+
});
|
|
1051
|
+
recentCommits = output.trim() ? output.trim().split('\n').length : 0;
|
|
1047
1052
|
} catch {
|
|
1048
1053
|
recentCommits = 10; // Default assumption
|
|
1049
1054
|
}
|
package/cli/commands/setup.mjs
CHANGED
|
@@ -21,7 +21,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from
|
|
|
21
21
|
import { resolve, dirname, basename } from 'node:path';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
23
|
import { createInterface } from 'node:readline';
|
|
24
|
-
import { execSync } from 'node:child_process';
|
|
25
24
|
import { c, CURRENT_SCHEMA_VERSION } from '../shared.mjs';
|
|
26
25
|
import { ensureSkills, detectAgentMode, isSpecKitInitialized, getDetectedAgent } from '../ensure-skills.mjs';
|
|
27
26
|
|
|
@@ -70,18 +69,6 @@ function detectProjectType(dir) {
|
|
|
70
69
|
return 'unknown';
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
// ── CLI Detection ───────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
function isCliAvailable(name) {
|
|
76
|
-
try {
|
|
77
|
-
const cmd = process.platform === 'win32' ? `where ${name}` : `which ${name}`;
|
|
78
|
-
execSync(`${cmd} 2>/dev/null`, { encoding: 'utf-8', timeout: 3000 });
|
|
79
|
-
return true;
|
|
80
|
-
} catch {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
72
|
function detectAgentDirs(projectDir) {
|
|
86
73
|
const agentDirs = [
|
|
87
74
|
{ name: 'GitHub Copilot', dir: '.github', commandsPath: '.github/commands' },
|
package/cli/commands/verify.mjs
CHANGED
|
@@ -22,10 +22,50 @@
|
|
|
22
22
|
* docguard verify [--semantic | --instructions] [--format json]
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import { basename } from 'node:path';
|
|
25
26
|
import { c } from '../shared.mjs';
|
|
26
27
|
import { detectAgentMode } from '../ensure-skills.mjs';
|
|
27
28
|
import { extractSemanticClaims, buildSemanticVerifyTasks } from '../scanners/semantic-claims.mjs';
|
|
28
29
|
import { auditInstructions } from '../scanners/instruction-audit.mjs';
|
|
30
|
+
import { isGitRepo, getDiffText } from '../shared-git.mjs';
|
|
31
|
+
import { parseUnifiedDiff, activityLabeledDiff } from '../shared-diff.mjs';
|
|
32
|
+
|
|
33
|
+
const CHANGE_CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|kt|rb|php|cs|swift|scala|dart)$/;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Structured change context for staged agent tasks (feat 6). When `--since` is
|
|
37
|
+
* given, decompose the code diff into activity-labeled spans (ordered
|
|
38
|
+
* replace/delete/add) — the CARL-CCI representation shown to beat raw-text
|
|
39
|
+
* diffs (arXiv 2512.19883). The agent judging a claim then sees WHAT changed,
|
|
40
|
+
* not just the claim. Returns null when there's no ref / git / diff.
|
|
41
|
+
*
|
|
42
|
+
* Bounded: caps files and per-activity lines so the JSON stays agent-sized.
|
|
43
|
+
*/
|
|
44
|
+
function buildChangeContext(projectDir, since, { maxFiles = 40, maxLines = 6 } = {}) {
|
|
45
|
+
if (!since || !isGitRepo(projectDir)) return null;
|
|
46
|
+
const files = parseUnifiedDiff(getDiffText(projectDir, since))
|
|
47
|
+
.filter(f => f.newPath && CHANGE_CODE_EXT.test(f.newPath) && f.status !== 'deleted');
|
|
48
|
+
if (files.length === 0) return null;
|
|
49
|
+
const clip = (arr) => arr.slice(0, maxLines).map(s => s.length > 200 ? s.slice(0, 200) + '…' : s);
|
|
50
|
+
const activities = files.slice(0, maxFiles).map(f => ({
|
|
51
|
+
file: f.newPath,
|
|
52
|
+
activities: activityLabeledDiff(f).map(a => ({
|
|
53
|
+
type: a.type,
|
|
54
|
+
...(a.del ? { del: clip(a.del) } : {}),
|
|
55
|
+
...(a.add ? { add: clip(a.add) } : {}),
|
|
56
|
+
})),
|
|
57
|
+
}));
|
|
58
|
+
return { since, changedFiles: files.map(f => f.newPath), activities };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Best-effort: is this semantic-verify task about code that just changed?
|
|
62
|
+
function taskTouchesChange(task, changedSet, changedBasenames) {
|
|
63
|
+
const cite = task.citedCode || '';
|
|
64
|
+
if (!cite) return false;
|
|
65
|
+
if (changedSet.has(cite)) return true;
|
|
66
|
+
const b = basename(cite);
|
|
67
|
+
return changedBasenames.has(b) || [...changedSet].some(p => cite.includes(p) || p.includes(cite));
|
|
68
|
+
}
|
|
29
69
|
|
|
30
70
|
export function runVerify(projectDir, config, flags) {
|
|
31
71
|
if (flags.instructions) {
|
|
@@ -37,13 +77,27 @@ export function runVerify(projectDir, config, flags) {
|
|
|
37
77
|
const claims = extractSemanticClaims(projectDir, config);
|
|
38
78
|
const tasks = buildSemanticVerifyTasks(claims);
|
|
39
79
|
|
|
80
|
+
// Change-aware staging (feat 6): if --since given, attach the structured diff
|
|
81
|
+
// and flag which claims are about just-changed code (verify those first).
|
|
82
|
+
const changeContext = buildChangeContext(projectDir, flags.since);
|
|
83
|
+
if (changeContext) {
|
|
84
|
+
const changedSet = new Set(changeContext.changedFiles);
|
|
85
|
+
const changedBasenames = new Set(changeContext.changedFiles.map(f => basename(f)));
|
|
86
|
+
for (const t of tasks) t.aboutChangedCode = taskTouchesChange(t, changedSet, changedBasenames);
|
|
87
|
+
// Prioritize changed-code claims first.
|
|
88
|
+
tasks.sort((a, b) => (b.aboutChangedCode ? 1 : 0) - (a.aboutChangedCode ? 1 : 0));
|
|
89
|
+
}
|
|
90
|
+
|
|
40
91
|
if (isJson) {
|
|
41
92
|
console.log(JSON.stringify({
|
|
42
93
|
command: 'verify --semantic',
|
|
43
94
|
project: config.projectName,
|
|
44
95
|
claimCount: tasks.length,
|
|
45
96
|
// How to act on this: each task is a claim to confirm against the code.
|
|
46
|
-
howToVerify:
|
|
97
|
+
howToVerify: changeContext
|
|
98
|
+
? 'Claims flagged aboutChangedCode are about code that changed since the ref — verify those FIRST using changeContext.activities (the ordered replace/delete/add spans show exactly what changed). For each task, read the cited code, compare to the documented value, report mismatches with both values.'
|
|
99
|
+
: 'For each task, read the cited code (or grep for the constant/config), compare it to the documented value, and report any mismatch with both values. DocGuard cannot judge these — they require reading the code.',
|
|
100
|
+
...(changeContext ? { changeContext } : {}),
|
|
47
101
|
tasks,
|
|
48
102
|
}, null, 2));
|
|
49
103
|
return;
|
|
@@ -66,6 +120,10 @@ export function runVerify(projectDir, config, flags) {
|
|
|
66
120
|
}
|
|
67
121
|
|
|
68
122
|
console.log(` ${c.yellow}${tasks.length} claim(s) to verify against the code:${c.reset}\n`);
|
|
123
|
+
if (changeContext) {
|
|
124
|
+
const nChanged = tasks.filter(t => t.aboutChangedCode).length;
|
|
125
|
+
console.log(` ${c.cyan}⚡ ${nChanged} claim(s) are about code changed since ${flags.since}${c.reset} ${c.dim}— verify these first (structured diff in --format json).${c.reset}\n`);
|
|
126
|
+
}
|
|
69
127
|
for (const [doc, ts] of byDoc) {
|
|
70
128
|
console.log(` ${c.bold}${doc}${c.reset}`);
|
|
71
129
|
for (const t of ts) {
|
|
@@ -89,6 +147,9 @@ function runInstructionAudit(projectDir, config, flags) {
|
|
|
89
147
|
const { rules, deterministic, tasks } = auditInstructions(projectDir, config);
|
|
90
148
|
const { duplicates, negations, stalePointers, staleCommands } = deterministic;
|
|
91
149
|
const findingCount = duplicates.length + negations.length + stalePointers.length + staleCommands.length;
|
|
150
|
+
// Structured change context helps the agent judge whether a rule about code
|
|
151
|
+
// has been invalidated by a recent change (feat 6).
|
|
152
|
+
const changeContext = buildChangeContext(projectDir, flags.since);
|
|
92
153
|
|
|
93
154
|
if (isJson) {
|
|
94
155
|
console.log(JSON.stringify({
|
|
@@ -100,6 +161,7 @@ function runInstructionAudit(projectDir, config, flags) {
|
|
|
100
161
|
taskCount: tasks.length,
|
|
101
162
|
// How to act on this: findings are proven; tasks need judgment.
|
|
102
163
|
howToVerify: 'The findings are deterministic — fix them directly (delete the duplicate copy, resolve the negation in favour of one rule, repoint or remove stale paths/commands). For each task, read both rules in context and judge whether they contradict in practice; if so, report which should win, why, and which file to edit. DocGuard cannot judge the tasks — they require understanding intent.',
|
|
164
|
+
...(changeContext ? { changeContext } : {}),
|
|
103
165
|
tasks,
|
|
104
166
|
}, null, 2));
|
|
105
167
|
return;
|
package/cli/config.mjs
CHANGED
|
@@ -77,6 +77,13 @@ export function loadConfig(projectDir) {
|
|
|
77
77
|
security: false,
|
|
78
78
|
environment: true,
|
|
79
79
|
freshness: true,
|
|
80
|
+
// v0.31.0 — all three default ON. Soft (confidence:low, never break CI),
|
|
81
|
+
// precise (zero false positives across the 6-repo corpus), and quiet when
|
|
82
|
+
// not applicable (no diff / no API-reference doc). api-doc-smells is
|
|
83
|
+
// low-yield but zero-FP, so on-by-default beats a self-counting split.
|
|
84
|
+
diffSuspicion: true,
|
|
85
|
+
referenceExistence: true,
|
|
86
|
+
apiDocSmells: true,
|
|
80
87
|
},
|
|
81
88
|
};
|
|
82
89
|
|
package/cli/findings.mjs
CHANGED
|
@@ -600,6 +600,32 @@ export const CODES = {
|
|
|
600
600
|
help: 'Over 30% of sentences are conditional (if/unless/when…). Split conditionals into separate, unconditional requirements.',
|
|
601
601
|
suppress: null,
|
|
602
602
|
},
|
|
603
|
+
|
|
604
|
+
// ── v0.31.0 change-driven + IR detectors (all confidence:'low' / soft) ──
|
|
605
|
+
DSP001: {
|
|
606
|
+
validator: 'diff-suspicion',
|
|
607
|
+
title: 'Doc describes code that just changed',
|
|
608
|
+
help: 'A canonical doc (or agent-instruction file) references a code file AND shares wording with symbols removed/changed in that file since the compared revision. Deterministic diff-overlap rule (arXiv 2010.01625, F1 74.7). Low-confidence by design — re-read the doc against the current code; suppress the pairing if it is a false positive.',
|
|
609
|
+
suppress: null,
|
|
610
|
+
},
|
|
611
|
+
REF001: {
|
|
612
|
+
validator: 'reference-existence',
|
|
613
|
+
title: 'Doc references a code symbol that no longer exists',
|
|
614
|
+
help: 'A code-element reference in the doc matched source when the doc was last updated, but matches ZERO source instances at HEAD (two-revision check, arXiv 2212.01479). Excludes the two documented false-positive modes (removed-but-config-relevant flags, and symbols whose literal string was deleted while logic remains). Verify and update the reference.',
|
|
615
|
+
suppress: '<!-- docguard:ignore REF001 — still relevant, e.g. user-facing flag -->',
|
|
616
|
+
},
|
|
617
|
+
APS001: {
|
|
618
|
+
validator: 'api-doc-smells',
|
|
619
|
+
title: 'Bloated API documentation',
|
|
620
|
+
help: 'An API doc unit is excessively long / over-structured relative to the surface it documents (smell taxonomy, arXiv API-doc-smells; deterministic Bloated detector F1 0.90). Trim to the essential contract.',
|
|
621
|
+
suppress: '<!-- docguard:quality api-smell off — your reason -->',
|
|
622
|
+
},
|
|
623
|
+
APS002: {
|
|
624
|
+
validator: 'api-doc-smells',
|
|
625
|
+
title: 'Lazy API documentation',
|
|
626
|
+
help: 'An API doc unit is vague/generic or barely exceeds the signature it documents (deterministic Lazy detector F1 0.95). Document parameters, return, and errors concretely.',
|
|
627
|
+
suppress: '<!-- docguard:quality api-smell off — your reason -->',
|
|
628
|
+
},
|
|
603
629
|
};
|
|
604
630
|
|
|
605
631
|
/**
|