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.
package/README.md CHANGED
@@ -76,7 +76,7 @@ graph TD
76
76
  Commands --> setup["setup wizard"]
77
77
  Commands --> other["diff · init · fix · trace · impact · sync<br/>explain · memory · upgrade · agents · hooks · badge · ci · watch"]
78
78
 
79
- guard --> Validators["Validators (24)"]
79
+ guard --> Validators["Validators (27)"]
80
80
  generate --> Scanners["Scanners (4)<br/>routes · schemas · doc-tools · speckit"]
81
81
  score --> Scoring["Weighted Scoring<br/>8 categories"]
82
82
  diagnose --> Validators
@@ -254,7 +254,7 @@ DocGuard ships **18 commands** (the "Daily 5" + 13 situational tools, including
254
254
  | Command | What It Does |
255
255
  |:--------|:-------------|
256
256
  | `init` | Bootstrap a project (`--wizard` for interactive · `--with <name>` for scaffolders) |
257
- | `guard` | Validate against canonical docs — 24 validators |
257
+ | `guard` | Validate against canonical docs — 27 validators |
258
258
  | `diff` | Show gaps between docs and code (`--since <ref>` for impact mode) |
259
259
  | `sync` | Refresh code-truth doc sections — keeps memory always up to date |
260
260
  | `score` | CDD maturity score (0-100; `--diff` for delta between refs) |
@@ -360,7 +360,7 @@ $ npx docguard-cli generate
360
360
 
361
361
  ## 🔍 Validators
362
362
 
363
- DocGuard runs **24 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.
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.
364
364
 
365
365
  | # | Validator | What It Checks | Default |
366
366
  |:--|:----------|:--------------|:--------|
@@ -387,7 +387,10 @@ DocGuard runs **24 automated validators** on every `guard` check. Every one is *
387
387
  | 21 | **Generated-Staleness** | `source=code` sections match scanner output; `status: draft` doc age | ✅ On |
388
388
  | 22 | **Canonical-Sync** | DocGuard's own README count claims match code-truth (DocGuard repo only — N/A elsewhere) | ✅ On |
389
389
  | 23 | **Metrics-Consistency** | Hardcoded numbers match actual counts | ✅ On |
390
- | 24 | **Surface-Sync** | Item-level enumerable drift — names in doc tables/lists (commands, validators, etc.) match code-truth (opt-in via `surfaceSync.surfaces`; N/A unless configured) | ✅ On |
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 |
391
394
 
392
395
  **Per-validator controls** (in `.docguard.json`):
393
396
  ```json
@@ -456,7 +459,7 @@ DocGuard provides AI agent slash commands for integrated workflows. Installed au
456
459
  | Command | What It Does |
457
460
  |:--------|:-------------|
458
461
  | `/docguard.init` | Initialize Canonical-Driven Development in a new or existing project |
459
- | `/docguard.guard` | Run quality validation — check all 24 validators |
462
+ | `/docguard.guard` | Run quality validation — check all 27 validators |
460
463
  | `/docguard.review` | Analyze doc quality and suggest improvements |
461
464
  | `/docguard.fix` | Generate targeted fix prompts for specific issues |
462
465
  | `/docguard.update` | Update canonical docs after code changes — detect drift and sync documentation |
@@ -615,7 +618,7 @@ Two ready-to-use templates ship with the Spec Kit extension and as standalone fi
615
618
  Recent highlights across the v0.16 → v0.19 line:
616
619
 
617
620
  - **`docguard explain <validator>`** — `docguard explain freshness` prints purpose, rules, common
618
- failures, and fix recipes for any of the 24 validators. No need to dig into source.
621
+ failures, and fix recipes for any of the 27 validators. No need to dig into source.
619
622
  - **`docguard memory --diff`** — surface what changed in your canonical docs between two refs
620
623
  (`HEAD~10..HEAD` by default). Great for code review and changelog drafting.
621
624
  - **`docguard score --diff`** — see exactly which validators moved the score up or down between
@@ -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. */
@@ -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
 
@@ -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
- ignoredFiles: changed.filter(f => !CODE_EXTENSIONS.test(f)),
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 (${changed.length} non-code files: ${changed.slice(0, 3).join(', ')}${changed.length > 3 ? '…' : ''}).${c.reset}`);
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
- 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}`);
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
  }
@@ -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: '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.',
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
  /**