docguard-cli 0.28.0 → 0.29.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.
Files changed (65) hide show
  1. package/README.es.md +102 -0
  2. package/README.md +64 -31
  3. package/README.pt-BR.md +101 -0
  4. package/STANDARD.md +20 -10
  5. package/cli/commands/agents.mjs +149 -0
  6. package/cli/commands/diff.mjs +6 -15
  7. package/cli/commands/generate.mjs +14 -1001
  8. package/cli/commands/guard.mjs +136 -8
  9. package/cli/commands/llms.mjs +67 -5
  10. package/cli/commands/mcp.mjs +263 -0
  11. package/cli/commands/memory.mjs +115 -0
  12. package/cli/commands/score.mjs +76 -12
  13. package/cli/docguard.mjs +31 -2
  14. package/cli/findings.mjs +499 -0
  15. package/cli/scanners/agent-readability.mjs +202 -0
  16. package/cli/scanners/semantic-claims.mjs +7 -1
  17. package/cli/scanners/speckit.mjs +98 -28
  18. package/cli/shared-ignore.mjs +148 -16
  19. package/cli/shared.mjs +45 -1
  20. package/cli/validators/api-surface.mjs +113 -26
  21. package/cli/validators/architecture.mjs +66 -43
  22. package/cli/validators/canonical-sync.mjs +59 -28
  23. package/cli/validators/changelog.mjs +41 -17
  24. package/cli/validators/cross-reference.mjs +28 -11
  25. package/cli/validators/doc-quality.mjs +78 -44
  26. package/cli/validators/docs-coverage.mjs +90 -63
  27. package/cli/validators/docs-diff.mjs +63 -64
  28. package/cli/validators/docs-sync.mjs +48 -33
  29. package/cli/validators/drift.mjs +40 -34
  30. package/cli/validators/environment.mjs +67 -27
  31. package/cli/validators/freshness.mjs +12 -5
  32. package/cli/validators/generated-staleness.mjs +26 -10
  33. package/cli/validators/metadata-sync.mjs +28 -25
  34. package/cli/validators/metrics-consistency.mjs +89 -47
  35. package/cli/validators/schema-sync.mjs +37 -32
  36. package/cli/validators/security.mjs +7 -20
  37. package/cli/validators/spec-kit.mjs +3 -0
  38. package/cli/validators/structure.mjs +58 -23
  39. package/cli/validators/surface-sync.mjs +34 -15
  40. package/cli/validators/test-spec.mjs +87 -29
  41. package/cli/validators/todo-tracking.mjs +83 -74
  42. package/cli/validators/traceability.mjs +67 -39
  43. package/cli/writers/doc-generators.mjs +853 -0
  44. package/cli/writers/generate-io.mjs +142 -0
  45. package/cli/writers/sarif.mjs +129 -0
  46. package/commands/docguard.fix.md +56 -53
  47. package/commands/docguard.guard.md +53 -47
  48. package/commands/docguard.review.md +49 -31
  49. package/docs/ai-integration.md +133 -134
  50. package/docs/commands.md +49 -3
  51. package/docs/configuration.md +38 -0
  52. package/docs/faq.md +15 -0
  53. package/extensions/spec-kit-docguard/extension.yml +1 -1
  54. package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
  55. package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
  56. package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
  57. package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
  58. package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +2 -2
  59. package/package.json +1 -1
  60. package/schemas/docguard-config.schema.json +17 -0
  61. package/templates/commands/docguard.fix.md +33 -10
  62. package/templates/commands/docguard.guard.md +40 -26
  63. package/templates/commands/docguard.init.md +23 -11
  64. package/templates/commands/docguard.review.md +25 -8
  65. package/templates/commands/docguard.update.md +14 -4
@@ -21,8 +21,12 @@
21
21
  * Zero NPM dependencies. Pure orchestration of existing diff helpers.
22
22
  */
23
23
 
24
+ import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from 'node:fs';
25
+ import { resolve } from 'node:path';
24
26
  import { c } from '../shared.mjs';
25
27
  import { diffRoutes, diffEntities, diffEnvVars, diffTechStack } from './diff.mjs';
28
+ import { buildMemoryPlan } from '../scanners/memory-plan.mjs';
29
+ import { runGuardInternal } from './guard.mjs';
26
30
 
27
31
  /**
28
32
  * Compute an accuracy score for a single domain. Returns:
@@ -47,7 +51,118 @@ function _domainAccuracy(d) {
47
51
  };
48
52
  }
49
53
 
54
+ // ── Context pack (v0.29) ─────────────────────────────────────────────────────
55
+
56
+ /** H2 sections of AGENTS.md whose heading reads like rules/conventions/workflow. */
57
+ function extractConventions(agentsMd, capLines = 60) {
58
+ const out = [];
59
+ const lines = agentsMd.split('\n');
60
+ let taking = false;
61
+ for (const line of lines) {
62
+ const h2 = line.match(/^##\s+(.+)$/);
63
+ if (h2) taking = /rules|conventions|workflow/i.test(h2[1]);
64
+ if (taking) {
65
+ out.push(line);
66
+ if (out.length >= capLines) {
67
+ out.push('<!-- truncated — read AGENTS.md for the full rules -->');
68
+ break;
69
+ }
70
+ }
71
+ }
72
+ return out;
73
+ }
74
+
75
+ /**
76
+ * `docguard memory --pack` — write .docguard/context-pack.md: a compact,
77
+ * code-truth-stamped session-start context for an AI agent. Everything in it
78
+ * is derived from scanners (buildMemoryPlan) and guard — numbers, not prose —
79
+ * so it can't hallucinate and is always regenerable.
80
+ */
81
+ function runMemoryPack(projectDir, config, flags) {
82
+ const plan = buildMemoryPlan(projectDir, config);
83
+ const guard = runGuardInternal(projectDir, config);
84
+ const lines = [];
85
+
86
+ lines.push(`# Context Pack — ${config.projectName}`);
87
+ lines.push('');
88
+ lines.push(`<!-- Generated by \`docguard memory --pack\` ${new Date().toISOString()} — regenerate, don't edit -->`);
89
+ lines.push('');
90
+ lines.push(`**Guard:** ${guard.status} — ${guard.passed}/${guard.total} checks (${guard.errors} error(s), ${guard.warnings} warning(s))`);
91
+ lines.push('');
92
+
93
+ lines.push('## Code-truth surface');
94
+ lines.push('');
95
+ lines.push(`- Stack: ${plan.profile.languages.join(', ') || 'unknown'}${plan.profile.frameworks.length ? ` · ${plan.profile.frameworks.join(', ')}` : ''} · kind: ${plan.profile.kind}`);
96
+ lines.push(`- Modules: ${plan.surface.modules.length} · Endpoints: ${plan.surface.endpoints.length} · Entities: ${plan.surface.entities.length} · Env vars: ${plan.surface.envVars.length}`);
97
+ lines.push(`- Tests: ${plan.surface.tests.totalFiles} files, ${plan.surface.tests.totalCases} cases`);
98
+ lines.push('');
99
+
100
+ const docsDir = resolve(projectDir, 'docs-canonical');
101
+ if (existsSync(docsDir)) {
102
+ lines.push('## Canonical docs');
103
+ lines.push('');
104
+ let entries = [];
105
+ try { entries = readdirSync(docsDir).filter(f => f.endsWith('.md')).sort(); } catch { /* ignore */ }
106
+ for (const doc of entries) {
107
+ let reviewed = '';
108
+ try {
109
+ const m = readFileSync(resolve(docsDir, doc), 'utf-8').match(/docguard:last-reviewed\s+(\d{4}-\d{2}-\d{2})/);
110
+ if (m) reviewed = ` (last-reviewed ${m[1]})`;
111
+ } catch { /* ignore */ }
112
+ lines.push(`- docs-canonical/${doc}${reviewed}`);
113
+ }
114
+ lines.push('');
115
+ }
116
+
117
+ const agentsPath = resolve(projectDir, 'AGENTS.md');
118
+ if (existsSync(agentsPath)) {
119
+ let conventions = [];
120
+ try { conventions = extractConventions(readFileSync(agentsPath, 'utf-8')); } catch { /* ignore */ }
121
+ if (conventions.length > 0) {
122
+ lines.push('## Project rules (from AGENTS.md)');
123
+ lines.push('');
124
+ lines.push(...conventions);
125
+ lines.push('');
126
+ }
127
+ }
128
+
129
+ const driftPath = resolve(projectDir, 'DRIFT-LOG.md');
130
+ if (existsSync(driftPath)) {
131
+ try {
132
+ const drift = readFileSync(driftPath, 'utf-8');
133
+ const entries = drift.match(/^##\s+.+$/gm) || [];
134
+ if (entries.length > 0) {
135
+ lines.push('## Known drift');
136
+ lines.push('');
137
+ lines.push(`- ${entries.length} logged deviation(s); latest: ${entries[entries.length - 1].replace(/^##\s+/, '')}`);
138
+ lines.push('');
139
+ }
140
+ } catch { /* ignore */ }
141
+ }
142
+
143
+ lines.push('---');
144
+ lines.push('Verify claims: `docguard verify --semantic` · Full docs: `llms-full.txt`');
145
+ lines.push('');
146
+ const content = lines.join('\n');
147
+
148
+ if (flags.stdout) {
149
+ console.log(content);
150
+ return;
151
+ }
152
+ const outDir = resolve(projectDir, '.docguard');
153
+ if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
154
+ const outPath = resolve(outDir, 'context-pack.md');
155
+ writeFileSync(outPath, content, 'utf-8');
156
+ console.log(`${c.bold}🧠 DocGuard Context Pack${c.reset}`);
157
+ console.log(`${c.green}✅ Wrote ${outPath}${c.reset} ${c.dim}(${lines.length} lines — load at agent session start)${c.reset}`);
158
+ console.log('');
159
+ }
160
+
50
161
  export function runMemory(projectDir, config, flags) {
162
+ // v0.29: --pack writes the agent context pack and exits — a separate output
163
+ // artifact, not a display mode of the accuracy drill-down below.
164
+ if (flags.pack) return runMemoryPack(projectDir, config, flags);
165
+
51
166
  const isJson = flags.format === 'json';
52
167
  const wantsDiff = flags.diff || (flags.args || []).includes('--diff');
53
168
 
@@ -9,6 +9,8 @@ import { execSync } 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';
12
+ import { extractSemanticClaims } from '../scanners/semantic-claims.mjs';
13
+ import { assessAgentReadability } from '../scanners/agent-readability.mjs';
12
14
 
13
15
  /**
14
16
  * Detect whether the project configures a test runner (the "Check 3" of the
@@ -299,11 +301,17 @@ export function runScore(projectDir, config, flags) {
299
301
  console.log(` ${c.dim}─────────────────────────────────${c.reset}`);
300
302
 
301
303
  for (const attr of alcoa.attributes) {
302
- const icon = attr.met ? `${c.green}✅` : `${c.yellow}⚠️`;
303
- const status = attr.met ? `${c.green}${attr.evidence}` : `${c.yellow}${attr.gap}`;
304
- console.log(` ${icon} ${attr.name.padEnd(16)}${c.reset} ${status}${c.reset}`);
304
+ // `unverified` is a third state: not a green pass, but not a red gap either —
305
+ // "checked the structure, can't confirm the facts." Render it neutrally (🔍,
306
+ // cyan) so it reads as a to-do, not a failure.
307
+ const unverified = attr.status === 'unverified';
308
+ const icon = attr.met ? `${c.green}✅` : unverified ? `${c.cyan}🔍` : `${c.yellow}⚠️`;
309
+ const tone = attr.met ? c.green : unverified ? c.cyan : c.yellow;
310
+ const body = attr.met ? attr.evidence : attr.gap;
311
+ console.log(` ${icon} ${attr.name.padEnd(16)}${c.reset} — ${tone}${body}${c.reset}`);
305
312
  if (!attr.met && attr.fix) {
306
- console.log(` ${c.dim} Fix: ${attr.fix}${c.reset}`);
313
+ const verb = unverified ? 'Verify' : 'Fix';
314
+ console.log(` ${c.dim} ${verb}: ${attr.fix}${c.reset}`);
307
315
  }
308
316
  }
309
317
 
@@ -314,6 +322,22 @@ export function runScore(projectDir, config, flags) {
314
322
  }
315
323
  console.log('');
316
324
 
325
+ // ── Agent Readability (v0.29) ──
326
+ // Display-only, like ALCOA+ — never feeds the gating CDD grade. Answers the
327
+ // 2026 question: can an AI consumer FIND, QUOTE, and TRUST these docs?
328
+ const agentRead = assessAgentReadability(projectDir, config);
329
+ console.log(` ${c.bold}🤖 Agent Readability${c.reset} ${c.dim}(how well AI consumers can read this repo)${c.reset}`);
330
+ console.log(` ${c.dim}─────────────────────────────────${c.reset}`);
331
+ for (const m of agentRead.metrics) {
332
+ const icon = m.score >= 60 ? `${c.green}✅` : `${c.yellow}⚠️`;
333
+ const tone = m.score >= 60 ? c.green : c.yellow;
334
+ console.log(` ${icon} ${m.label.padEnd(28)}${c.reset} — ${tone}${m.detail}${c.reset}`);
335
+ if (m.fix) console.log(` ${c.dim} Fix: ${m.fix}${c.reset}`);
336
+ }
337
+ const arColor = agentRead.score >= 75 ? c.green : agentRead.score >= 40 ? c.yellow : c.red;
338
+ console.log(`\n ${arColor}${c.bold}Agent Readability: ${agentRead.score}% (${agentRead.grade})${c.reset}`);
339
+ console.log('');
340
+
317
341
  // Badge snippet
318
342
  const bColor = totalScore >= 90 ? 'brightgreen' : totalScore >= 80 ? 'green' : totalScore >= 70 ? 'yellowgreen' : totalScore >= 60 ? 'yellow' : totalScore >= 50 ? 'orange' : 'red';
319
343
  const badgeUrl = `https://img.shields.io/badge/CDD_Score-${totalScore}%2F100_(${grade})-${bColor}`;
@@ -411,14 +435,54 @@ function computeAlcoaCompliance(projectDir, config, scores) {
411
435
  });
412
436
 
413
437
  // 5. Accurate — Do docs match the code?
414
- const accurate = scores.drift >= 80 && scores.docQuality >= 50;
415
- attributes.push({
416
- name: 'Accurate',
417
- met: accurate,
418
- evidence: accurate ? `Drift: ${scores.drift}%, doc quality: ${scores.docQuality}%` : null,
419
- gap: !accurate ? `Drift: ${scores.drift}%, doc quality: ${scores.docQuality}% docs may be inaccurate` : null,
420
- fix: !accurate ? 'Run docguard diagnose to find doc/code mismatches' : null,
421
- });
438
+ //
439
+ // Field report #6: this attribute used to read `met` purely from structural
440
+ // signals (drift markers + prose quality). That let it show ✅ "100%" while a
441
+ // watched doc stated a factually wrong number — the confidence-inverting false
442
+ // negative the field report is about. Structure passing is necessary but NOT
443
+ // sufficient for "accurate"; the factual claims (counts/limits/enums) have to be
444
+ // verified against code, and DocGuard's deterministic core can't do that only
445
+ // an agent via `verify --semantic` can. So we add a third, honest state:
446
+ // met — structure sound AND no unverified factual claims exist
447
+ // unverified — structure sound BUT documented claims remain unchecked vs code
448
+ // unmet — structural drift/quality below bar
449
+ // `unverified` counts as not-met for the ALCOA percentage (so the score stops
450
+ // overclaiming), but renders as a neutral 🔍 (not a ⚠️ failure) — "I haven't
451
+ // confirmed this," not "this is wrong." This is display-only: it never touches
452
+ // the gating CDD grade (totalScore), which CI thresholds read.
453
+ const structurallyAccurate = scores.drift >= 80 && scores.docQuality >= 50;
454
+ let unverifiedClaims = 0;
455
+ if (structurallyAccurate) {
456
+ try { unverifiedClaims = extractSemanticClaims(projectDir, config).length; } catch { /* extractor best-effort */ }
457
+ }
458
+ if (!structurallyAccurate) {
459
+ attributes.push({
460
+ name: 'Accurate',
461
+ met: false,
462
+ status: 'unmet',
463
+ evidence: null,
464
+ gap: `Drift: ${scores.drift}%, doc quality: ${scores.docQuality}% — docs may be inaccurate`,
465
+ fix: 'Run docguard diagnose to find doc/code mismatches',
466
+ });
467
+ } else if (unverifiedClaims > 0) {
468
+ attributes.push({
469
+ name: 'Accurate',
470
+ met: false,
471
+ status: 'unverified',
472
+ evidence: null,
473
+ gap: `Structure sound (drift ${scores.drift}%, quality ${scores.docQuality}%), but ${unverifiedClaims} documented claim(s) (counts/limits/enums) are unverified against code`,
474
+ fix: 'Run docguard verify --semantic to check the documented values against the code',
475
+ });
476
+ } else {
477
+ attributes.push({
478
+ name: 'Accurate',
479
+ met: true,
480
+ status: 'met',
481
+ evidence: `Drift: ${scores.drift}%, doc quality: ${scores.docQuality}%, no unverified factual claims`,
482
+ gap: null,
483
+ fix: null,
484
+ });
485
+ }
422
486
 
423
487
  // 6. Complete — Are all required docs present?
424
488
  const complete = scores.structure >= 80;
package/cli/docguard.mjs CHANGED
@@ -48,6 +48,7 @@ import { runVerify } from './commands/verify.mjs';
48
48
  import { runMemory } from './commands/memory.mjs';
49
49
  import { runDemo } from './commands/demo.mjs';
50
50
  import { runAgent } from './commands/agent.mjs';
51
+ import { runMcp } from './commands/mcp.mjs';
51
52
  import { ensureSkills } from './ensure-skills.mjs';
52
53
 
53
54
  // ── Shared constants (imported to break circular dependencies) ──────────
@@ -90,6 +91,7 @@ ${c.bold}Tools (situational, but day-to-day useful)${c.reset}
90
91
  ${c.green}explain${c.reset} Explain a validator key, warning text, or finding code (${c.cyan}docguard explain SEC001${c.reset})
91
92
  ${c.green}verify${c.reset} Extract documented numbers/limits/enums for an agent to check vs code (${c.cyan}--semantic${c.reset})
92
93
  ${c.green}feedback${c.reset} Report likely false positives back to DocGuard (local-first + 1-click prefilled issue)
94
+ ${c.green}mcp${c.reset} MCP server over stdio — guard/score/explain/verify/diagnose as agent tools
93
95
  ${c.green}memory${c.reset} Show what DocGuard remembers (${c.cyan}--diff${c.reset} drills into drift)
94
96
  ${c.green}trace${c.reset} Requirements traceability matrix (${c.cyan}--reverse${c.reset} for code→doc map)
95
97
  ${c.green}upgrade${c.reset} Migrate ${c.cyan}.docguard.json${c.reset} schema + CLI (${c.cyan}--apply --pr${c.reset} for team-wide PR)
@@ -375,6 +377,23 @@ async function main() {
375
377
  // v0.28 (field report #5): `docguard verify --semantic` extracts
376
378
  // documented numbers/enums/limits for the agent to check against code.
377
379
  flags.semantic = true;
380
+ } else if (args[i] === '--full') {
381
+ // v0.29: `docguard llms --full` emits llms-full.txt (inline doc bodies,
382
+ // the Mintlify-popularized companion to the llms.txt index).
383
+ flags.full = true;
384
+ } else if (args[i] === '--pack') {
385
+ // v0.29: `docguard memory --pack` writes .docguard/context-pack.md — a
386
+ // compact code-truth-stamped session-start context for AI agents.
387
+ flags.pack = true;
388
+ } else if (args[i] === '--sync') {
389
+ // v0.29: `docguard agents --sync` regenerates the agent-file family
390
+ // (CLAUDE.md, GEMINI.md, copilot-instructions, .cursor rules) from
391
+ // AGENTS.md — the canonical source. Kills hand-duplication drift.
392
+ flags.sync = true;
393
+ } else if (args[i] === '--check') {
394
+ // v0.29: `docguard agents --check` — CI staleness gate for the synced
395
+ // agent-file family (exit 2 when a variant is missing or stale).
396
+ flags.check = true;
378
397
  } else if (args[i] === '--plan') {
379
398
  flags.plan = true;
380
399
  } else if (args[i] === '--since' && args[i + 1]) {
@@ -512,10 +531,13 @@ async function main() {
512
531
  // touch" — so it joins the club to suppress the banner AND ensureSkills'
513
532
  // .agent/.specify writes, which were a surprising side effect of a bare
514
533
  // `generate --plan` (and were already suppressed for `--plan --write`).
515
- const jsonMode = flags.format === 'json';
534
+ // v0.29: 'sarif' joins 'json' — any machine format where stdout IS the
535
+ // artifact belongs here, or the banner corrupts the payload.
536
+ const jsonMode = flags.format === 'json' || flags.format === 'sarif';
516
537
  // `agent` emits a machine task graph (JSON by default) — it must be banner-
517
538
  // free and side-effect-free like the other read-only commands.
518
- const headless = jsonMode || flags.write || flags.checkOnly || flags.changedOnly || flags.quiet || flags.plan || command === 'agent';
539
+ // `mcp`: stdout IS the JSON-RPC transport any banner byte corrupts the stream.
540
+ const headless = jsonMode || flags.write || flags.checkOnly || flags.changedOnly || flags.quiet || flags.plan || command === 'agent' || command === 'mcp';
519
541
 
520
542
  if (!headless) printBanner();
521
543
 
@@ -540,6 +562,8 @@ async function main() {
540
562
  'feedback',
541
563
  // verify only reads docs and emits a task list — pure report.
542
564
  'verify',
565
+ // mcp serves read-only tools over stdio — scaffolding writes are off-limits.
566
+ 'mcp',
543
567
  ]);
544
568
 
545
569
  // Silent auto-check: install skills/commands if missing. Skip entirely in
@@ -691,6 +715,11 @@ async function main() {
691
715
  // drift — the class regex/AST can't see). Read-only.
692
716
  runVerify(projectDir, config, flags);
693
717
  break;
718
+ case 'mcp':
719
+ // MCP stdio server — guard/score/explain/verify-claims/diagnose as tools
720
+ // for MCP clients. Long-lived; resolves when stdin closes.
721
+ await runMcp(projectDir, config, flags);
722
+ break;
694
723
  case 'memory':
695
724
  runMemory(projectDir, config, flags);
696
725
  break;