docguard-cli 0.15.3 → 0.17.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.
@@ -25,8 +25,12 @@ const CODE_EXTENSIONS = new Set([
25
25
  ]);
26
26
 
27
27
  export function runDiff(projectDir, config, flags) {
28
- console.log(`${c.bold}🔍 DocGuard Diff ${config.projectName}${c.reset}`);
29
- console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
28
+ // v0.16-P1: headless mode for JSON output (matches guard/score/trace fix).
29
+ const isJson = flags.format === 'json';
30
+ if (!isJson) {
31
+ console.log(`${c.bold}🔍 DocGuard Diff — ${config.projectName}${c.reset}`);
32
+ console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
33
+ }
30
34
 
31
35
  const results = [];
32
36
 
@@ -97,7 +101,7 @@ export function runDiff(projectDir, config, flags) {
97
101
 
98
102
  // ── Diff Functions ─────────────────────────────────────────────────────────
99
103
 
100
- function diffRoutes(dir, config = {}) {
104
+ export function diffRoutes(dir, config = {}) {
101
105
  // Documented surface: prefer the dedicated API reference, fall back to ARCHITECTURE.md.
102
106
  const apiRefPath = resolve(dir, 'docs-canonical/API-REFERENCE.md');
103
107
  const archPath = resolve(dir, 'docs-canonical/ARCHITECTURE.md');
@@ -129,7 +133,7 @@ const CODE_ENTITY_NOISE = new Set([
129
133
  'models', 'model', 'utils', 'helpers', 'constants', 'config', 'common', 'base',
130
134
  ]);
131
135
 
132
- function diffEntities(dir, config = {}) {
136
+ export function diffEntities(dir, config = {}) {
133
137
  const dataModelPath = resolve(dir, 'docs-canonical/DATA-MODEL.md');
134
138
  if (!existsSync(dataModelPath)) return null;
135
139
 
@@ -196,7 +200,26 @@ function diffEntities(dir, config = {}) {
196
200
  };
197
201
  }
198
202
 
199
- function diffEnvVars(dir, config = {}) {
203
+ // v0.16-P4: common system environment variables that get backticked in
204
+ // prose ("the venv `PATH`", "your `HOME` directory") but are NEVER user-set
205
+ // application env vars. Excluding them from the docVars set kills the
206
+ // false-positive class reported by the Python user where `PATH` was flagged
207
+ // as "documented-but-not-implemented".
208
+ //
209
+ // Conservative list — only the names that are unambiguously OS/shell vars.
210
+ // Application names like `DATABASE_URL`, `API_KEY` etc. still count.
211
+ const SYSTEM_ENV_VARS = new Set([
212
+ 'PATH', 'HOME', 'USER', 'USERNAME', 'SHELL', 'PWD', 'OLDPWD', 'TMPDIR', 'TEMP', 'TMP',
213
+ 'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES', 'TZ',
214
+ 'EDITOR', 'VISUAL', 'PAGER', 'TERM', 'COLORTERM',
215
+ 'DISPLAY', 'SSH_AUTH_SOCK', 'SSH_CONNECTION', 'SSH_TTY',
216
+ 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_RUNTIME_DIR',
217
+ // CI/build platform vars (set by the platform, not by the app)
218
+ 'CI', 'GITHUB_TOKEN', 'GITHUB_ACTIONS', 'GITHUB_REF', 'GITHUB_SHA',
219
+ 'NODE_ENV', // could be app-set but more often platform-set; conservative skip
220
+ ]);
221
+
222
+ export function diffEnvVars(dir, config = {}) {
200
223
  const envDocPath = resolve(dir, 'docs-canonical/ENVIRONMENT.md');
201
224
  if (!existsSync(envDocPath)) return null;
202
225
 
@@ -208,6 +231,9 @@ function diffEnvVars(dir, config = {}) {
208
231
  const varRegex = /`([A-Z][A-Z0-9_]*[A-Z0-9])`/g;
209
232
  let match;
210
233
  while ((match = varRegex.exec(content)) !== null) {
234
+ // v0.16-P4: skip backticked system vars that appear in prose. They're
235
+ // never user-set application env vars; flagging them produces noise.
236
+ if (SYSTEM_ENV_VARS.has(match[1])) continue;
211
237
  docVars.add(match[1]);
212
238
  }
213
239
 
@@ -237,7 +263,7 @@ function diffEnvVars(dir, config = {}) {
237
263
  };
238
264
  }
239
265
 
240
- function diffTechStack(dir, config = {}) {
266
+ export function diffTechStack(dir, config = {}) {
241
267
  const archPath = resolve(dir, 'docs-canonical/ARCHITECTURE.md');
242
268
  if (!existsSync(archPath)) return null;
243
269
 
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Explain Command — v0.16-P6.
3
+ *
4
+ * Asked for by a user who'd spent 5-10 minutes per warning spelunking
5
+ * through validators/*.mjs source to understand what the validator wanted.
6
+ * `docguard explain "<warning text>"` matches the warning back to its
7
+ * validator and prints:
8
+ * - Which validator emitted it
9
+ * - What pattern triggered it
10
+ * - A passing example
11
+ * - The doc / spec / standard it's checking against
12
+ *
13
+ * Also supports `docguard explain <validator-key>` to show the whole
14
+ * validator's purpose without needing a specific warning.
15
+ *
16
+ * Zero NPM dependencies. Pure lookup table.
17
+ */
18
+
19
+ import { c } from '../shared.mjs';
20
+
21
+ /**
22
+ * Validator-key → human-readable explainer. Keyed by the same key DocGuard
23
+ * uses internally for severity overrides + lite-mode selection.
24
+ *
25
+ * Each entry has:
26
+ * - title: one-line summary
27
+ * - what: what the validator checks (declarative)
28
+ * - why: why it matters (motivation)
29
+ * - triggers: array of common warning fragments and what each means
30
+ * - example: a tiny passing snippet
31
+ * - standard: the spec/practice the validator references
32
+ */
33
+ const EXPLAINERS = {
34
+ structure: {
35
+ title: 'Structure — required CDD files exist',
36
+ what: 'Verifies the canonical files declared in .docguard.json `requiredFiles.canonical` are present, plus AGENTS.md/CLAUDE.md, CHANGELOG.md, DRIFT-LOG.md.',
37
+ why: 'A documentation memory needs known anchor points. Missing files = broken memory.',
38
+ triggers: [
39
+ ['Missing required file', 'A canonical doc declared in your config doesn\'t exist on disk. Create it or remove it from `requiredFiles.canonical`.'],
40
+ ['Missing agent file', 'No AGENTS.md or CLAUDE.md found. Create one — even a stub establishes the agent contract.'],
41
+ ],
42
+ example: 'docs-canonical/ARCHITECTURE.md, AGENTS.md, CHANGELOG.md all present',
43
+ standard: 'CDD STANDARD (this project\'s STANDARD.md)',
44
+ },
45
+ docsSync: {
46
+ title: 'Docs-Sync — code files are referenced in canonical docs',
47
+ what: 'Walks route/service files in your source tree. For each, checks that the file path or basename appears in any canonical doc.',
48
+ why: 'Code that exists but isn\'t mentioned in any doc is invisible to future contributors and AI agents.',
49
+ triggers: [
50
+ ['not referenced in any canonical doc', 'A route or service file has no mention anywhere in docs-canonical/. Add a one-line reference (path or filename) to ARCHITECTURE.md or DATA-MODEL.md.'],
51
+ ],
52
+ example: '`src/services/auth.ts` mentioned in ARCHITECTURE.md\'s Components table',
53
+ standard: 'arc42 Component Map',
54
+ },
55
+ drift: {
56
+ title: 'Drift-Comments — every `// DRIFT:` has a DRIFT-LOG entry',
57
+ what: 'Scans code for `// DRIFT: reason` comments (also # / /* / -- variants). Each must have a row in DRIFT-LOG.md.',
58
+ why: 'DRIFT comments document conscious deviations from canonical specs. Without log entries, the deviation is invisible.',
59
+ triggers: [
60
+ ['DRIFT comment but DRIFT-LOG.md doesn\'t exist', 'Create DRIFT-LOG.md or remove the DRIFT comment.'],
61
+ ['no matching DRIFT-LOG.md entry', 'Add a row to DRIFT-LOG.md documenting the deviation, OR remove the // DRIFT: comment if the deviation is no longer current.'],
62
+ ],
63
+ example: '// DRIFT: using S3 SDK v2 here for compatibility — DRIFT-LOG.md has a row dated and explaining why',
64
+ standard: 'CDD principle: log every intentional deviation',
65
+ },
66
+ changelog: {
67
+ title: 'Changelog — Keep a Changelog format',
68
+ what: 'CHANGELOG.md must have a top-level `# Changelog` heading and an `## [Unreleased]` section.',
69
+ why: 'Standard format makes changelogs machine-readable. The Unreleased section is where new work accumulates between releases.',
70
+ triggers: [
71
+ ['Missing # Changelog heading', 'Start CHANGELOG.md with `# Changelog`.'],
72
+ ['Missing ## [Unreleased] section', 'Add `## [Unreleased]` between `# Changelog` and your first dated release.'],
73
+ ],
74
+ example: '# Changelog\\n\\n## [Unreleased]\\n\\n## [1.0.0] - 2026-01-01',
75
+ standard: 'Keep a Changelog v1.1.0 (https://keepachangelog.com)',
76
+ },
77
+ testSpec: {
78
+ title: 'Test-Spec — declared tests exist',
79
+ what: 'Reads TEST-SPEC.md\'s test mapping (rows linking sources to test files) and verifies each referenced test file exists.',
80
+ why: 'A spec that claims test coverage for X but the test file is missing is a stale promise.',
81
+ triggers: [
82
+ ['no service-to-test mappings', 'TEST-SPEC.md has no recognized mapping table. Add a table with `| Source | Test file | Status |` columns.'],
83
+ ['referenced test file does not exist', 'A path in TEST-SPEC.md\'s mapping doesn\'t exist. Update the path or remove the row.'],
84
+ ],
85
+ example: '| `src/auth.ts` | `tests/auth.test.ts` | ✅ |',
86
+ standard: 'ISO/IEC/IEEE 29119-3 (test specification)',
87
+ },
88
+ environment: {
89
+ title: 'Environment — env vars used in code are documented',
90
+ what: 'Greps `process.env.X` and `import.meta.env.X` (plus `os.environ` for Python) across source. Each name must appear in ENVIRONMENT.md or .env.example/.env.template.',
91
+ why: 'Undocumented env vars are runtime surprises waiting to happen.',
92
+ triggers: [
93
+ ['used but not documented', 'Code reads an env var that ENVIRONMENT.md doesn\'t list. Add it to the table.'],
94
+ ['VITE_API_URL or similar prefix', 'Naked prefixes like `VITE_` (no suffix) get filtered out — they\'re convention markers, not real var names.'],
95
+ ],
96
+ example: '`DATABASE_URL` listed in ENVIRONMENT.md\'s Environment Variables table AND read via `process.env.DATABASE_URL` in code',
97
+ standard: '12-Factor App III. Config',
98
+ },
99
+ security: {
100
+ title: 'Security — secrets handling + auth presence',
101
+ what: 'Checks SECURITY.md for required sections, and scans code for committed secrets / unsafe patterns.',
102
+ why: 'OWASP ASVS baseline.',
103
+ triggers: [
104
+ ['Missing "Authentication" section', 'Add `## Authentication` to SECURITY.md. If the project genuinely has no auth (CLI, library), use the v0.16-P7 N/A marker: `<!-- docguard:section authentication n/a — reason -->`.'],
105
+ ['Possible secret', 'A pattern matching common secret formats (API keys, JWT secrets) was found in committed code. Move to env var or .env.example.'],
106
+ ],
107
+ example: 'SECURITY.md has `## Authentication` describing JWT flow; no `sk_live_*` strings in code',
108
+ standard: 'OWASP ASVS v4.0',
109
+ },
110
+ freshness: {
111
+ title: 'Freshness — docs updated alongside code',
112
+ what: 'For each canonical doc, counts code commits since the doc\'s last commit. >10 commits = stale.',
113
+ why: 'Docs drift silently. This validator surfaces the drift before it becomes invisible.',
114
+ triggers: [
115
+ ['code commits since last doc update', 'Run `docguard sync --write` to refresh code-truth sections, then review the prose for accuracy.'],
116
+ ['DRIFT-LOG.md may be stale', 'DRIFT comments in code outpaced log entries. Add the entries.'],
117
+ ],
118
+ example: 'ARCHITECTURE.md last committed within 10 code commits',
119
+ standard: 'CDD principle: docs and code commit together',
120
+ },
121
+ traceability: {
122
+ title: 'Traceability — every FR/SC ID has test coverage',
123
+ what: 'Scans specs/ for FR-### and SC-### requirement IDs. Each must appear in a test file as `@req FR-###`.',
124
+ why: 'Untraceable requirements drift from implementation.',
125
+ triggers: [
126
+ ['has no test coverage', 'Add `// @req FR-012` (or similar) as a comment in the test that verifies the requirement.'],
127
+ ['orphaned test reference', 'A `@req` comment references an ID that doesn\'t exist in any spec. Update the ID or remove the marker.'],
128
+ ],
129
+ example: 'spec.md defines `**FR-012**: ...` and test file has `// @req FR-012` near the test that verifies it',
130
+ standard: 'ISO/IEC/IEEE 29148 (requirements traceability)',
131
+ },
132
+ apiSurface: {
133
+ title: 'API-Surface — endpoints in code match API-REFERENCE.md',
134
+ what: 'Compares routes scanned from code (Express, Next, FastAPI, Spring, etc.) against endpoints listed in API-REFERENCE.md and OpenAPI specs.',
135
+ why: 'Documented but missing endpoints are dead links. Endpoints in code that aren\'t documented are invisible.',
136
+ triggers: [
137
+ ['documented but absent', 'API-REFERENCE.md lists an endpoint that scanRoutes() can\'t find. Remove or fix the doc; `fix --write` removes when marked.'],
138
+ ['present but undocumented', 'A route exists in code but API-REFERENCE.md doesn\'t list it. Add it.'],
139
+ ],
140
+ example: 'GET /api/users in src/routes/users.ts AND in API-REFERENCE.md\'s Endpoints table',
141
+ standard: 'OpenAPI 3.1',
142
+ },
143
+ metricsConsistency: {
144
+ title: 'Metrics-Consistency — quoted numbers match reality',
145
+ what: 'Greps canonical + root docs for "N validators" / "N checks" claims and compares against the actual runtime count.',
146
+ why: 'Stale numeric claims ("19 validators" when it\'s now 22) erode credibility.',
147
+ triggers: [
148
+ ['says "N validators" but actual count is M', 'Run `docguard fix --write` — this is auto-fixable.'],
149
+ ],
150
+ example: 'AGENTS.md says "22 validators" and `docguard guard` shows 22 active validators',
151
+ standard: 'CDD principle: documented metrics match reality',
152
+ },
153
+ crossReference: {
154
+ title: 'Cross-Reference — internal markdown links resolve',
155
+ what: 'Scans canonical docs for `[text](./OTHER.md#anchor)` and `#anchor` links. Verifies the target file exists and the anchor matches a heading.',
156
+ why: 'Broken doc-to-doc links are the most-clicked dead ends in onboarding.',
157
+ triggers: [
158
+ ['broken link: target file not found', 'The file path doesn\'t exist. Fix the path or remove the link.'],
159
+ ['broken anchor', 'Anchor doesn\'t match any heading. Hint: `(did you mean #X?)` is appended for near-misses; if marked `[auto-fixable]`, run `docguard fix --write`.'],
160
+ ],
161
+ example: '`[Setup](#prerequisites)` in ENVIRONMENT.md AND `## Prerequisites` heading present',
162
+ standard: 'GitHub Flavored Markdown anchor rules',
163
+ },
164
+ generatedStaleness: {
165
+ title: 'Generated-Staleness — source=code sections match scanner output',
166
+ what: 'For each `<!-- docguard:section source=code -->` block, re-runs the memory plan scanner and compares against on-disk content. Also flags status: draft docs unmodified for > 14 days.',
167
+ why: 'Code-truth sections must reflect what the code actually says. Forgotten drafts rot.',
168
+ triggers: [
169
+ ['is stale', 'A code-truth section drifted. Run `docguard sync --write` (or `docguard fix --write` since v0.14-P3 — the validator now emits a regenerate-section fix).'],
170
+ ['status: draft for', 'A doc has been in draft for too long. Promote to `status: current` or remove. Threshold via `config.draftStalenessDays`.'],
171
+ ],
172
+ example: 'All source=code sections match what the scanner would produce right now',
173
+ standard: 'CDD principle: code-truth sections are machine-owned',
174
+ },
175
+ todoTracking: {
176
+ title: 'TODO-Tracking — TODOs are tracked + skipped tests explained',
177
+ what: 'Finds TODO/FIXME/HACK comments in source. Each must be referenced in tracking docs (ROADMAP.md, GitHub issues, etc.). Also flags `it.skip()` / `test.skip()` without an adjacent `// REASON:` comment.',
178
+ why: 'TODOs in code that no one tracks are silent debt.',
179
+ triggers: [
180
+ ['Skipped test without explanation', 'Add `// REASON: <why>` immediately above the skip.'],
181
+ ['Untracked TODO', 'Reference the TODO from ROADMAP.md by file:line, OR add it to a GitHub issue and link the issue ID in the comment.'],
182
+ ],
183
+ example: '// REASON: waiting on upstream fix in libfoo v2.5\\ntest.skip("foo", () => {})',
184
+ standard: 'Pragmatic Programmer (debt visibility)',
185
+ },
186
+ specKit: {
187
+ title: 'Spec-Kit — spec.md/plan.md/tasks.md have required sections',
188
+ what: 'For projects using Spec Kit, validates each spec/*.md against the spec-kit-template required sections.',
189
+ why: 'Spec Kit\'s value comes from consistent shape across specs.',
190
+ triggers: [
191
+ ['Missing mandatory section', 'Add the section listed in the warning. Reference the template at .specify/templates/'],
192
+ ],
193
+ example: 'plan.md has Summary, Technical Context, Constitution Check, Project Structure',
194
+ standard: 'GitHub Spec Kit',
195
+ },
196
+ };
197
+
198
+ /**
199
+ * Match a warning text fragment against the explainer table. Returns the
200
+ * matching entry's key + the trigger entry that best matches, or null when
201
+ * no match is confident enough.
202
+ */
203
+ function matchWarning(query) {
204
+ const q = query.toLowerCase();
205
+
206
+ // Exact validator-key lookup (e.g. `docguard explain freshness`)
207
+ if (EXPLAINERS[query]) return { key: query, trigger: null };
208
+ // Also try kebab-case (e.g. `cross-reference` → `crossReference`)
209
+ const camelized = query.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
210
+ if (EXPLAINERS[camelized]) return { key: camelized, trigger: null };
211
+
212
+ // Search trigger phrases
213
+ let best = null;
214
+ let bestScore = 0;
215
+ for (const [key, e] of Object.entries(EXPLAINERS)) {
216
+ for (const [phrase, _hint] of e.triggers) {
217
+ if (q.includes(phrase.toLowerCase())) {
218
+ const score = phrase.length; // prefer the more-specific phrase
219
+ if (score > bestScore) {
220
+ best = { key, trigger: [phrase, _hint] };
221
+ bestScore = score;
222
+ }
223
+ }
224
+ }
225
+ }
226
+ return best;
227
+ }
228
+
229
+ export function runExplain(projectDir, _config, flags) {
230
+ const query = (flags.args || []).join(' ').trim();
231
+ const isJson = flags.format === 'json';
232
+
233
+ if (!query) {
234
+ if (isJson) {
235
+ console.log(JSON.stringify({ validators: Object.keys(EXPLAINERS) }, null, 2));
236
+ return;
237
+ }
238
+ console.log(`${c.bold}🧭 docguard explain${c.reset} ${c.dim}— usage:${c.reset}`);
239
+ console.log(` ${c.cyan}docguard explain <validator-key>${c.reset} e.g. docguard explain freshness`);
240
+ console.log(` ${c.cyan}docguard explain "<warning text>"${c.reset} e.g. docguard explain "no service-to-test mappings"`);
241
+ console.log(`\n${c.dim}Known validators:${c.reset}`);
242
+ for (const [k, e] of Object.entries(EXPLAINERS)) {
243
+ console.log(` ${c.cyan}${k.padEnd(22)}${c.reset} ${c.dim}${e.title}${c.reset}`);
244
+ }
245
+ return;
246
+ }
247
+
248
+ const match = matchWarning(query);
249
+ if (!match) {
250
+ if (isJson) {
251
+ console.log(JSON.stringify({ query, match: null }, null, 2));
252
+ return;
253
+ }
254
+ console.log(`${c.yellow}No matching validator or warning found for: "${query}"${c.reset}`);
255
+ console.log(`${c.dim}Try: ${c.cyan}docguard explain${c.dim} (no args) to list all validators.${c.reset}`);
256
+ process.exit(1);
257
+ }
258
+
259
+ const e = EXPLAINERS[match.key];
260
+ if (isJson) {
261
+ console.log(JSON.stringify({ query, match: { key: match.key, ...e, matchedTrigger: match.trigger } }, null, 2));
262
+ return;
263
+ }
264
+
265
+ console.log(`${c.bold}🧭 ${e.title}${c.reset}`);
266
+ console.log(`${c.dim} validator key: ${match.key}${c.reset}\n`);
267
+
268
+ console.log(`${c.bold}What it checks:${c.reset}\n ${e.what}\n`);
269
+ console.log(`${c.bold}Why:${c.reset}\n ${e.why}\n`);
270
+
271
+ if (match.trigger) {
272
+ console.log(`${c.bold}Your warning ("${query}") matches:${c.reset}`);
273
+ console.log(` ${c.yellow}${match.trigger[0]}${c.reset}`);
274
+ console.log(` ${match.trigger[1]}\n`);
275
+ } else {
276
+ console.log(`${c.bold}Common warnings:${c.reset}`);
277
+ for (const [phrase, hint] of e.triggers) {
278
+ console.log(` ${c.yellow}${phrase}${c.reset}`);
279
+ console.log(` ${c.dim}${hint}${c.reset}`);
280
+ }
281
+ console.log('');
282
+ }
283
+
284
+ console.log(`${c.bold}Passing example:${c.reset}\n ${c.dim}${e.example}${c.reset}\n`);
285
+ console.log(`${c.bold}Standard:${c.reset} ${c.dim}${e.standard}${c.reset}`);
286
+ }
@@ -11,6 +11,80 @@ import { c, resolveSeverity } from '../shared.mjs';
11
11
  import { detectAgentMode, isSpecKitInitialized } from '../ensure-skills.mjs';
12
12
  import { checkUpgradeStatus } from './upgrade.mjs';
13
13
  import { changedFilesSince, isGitRepo } from '../shared-git.mjs';
14
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
15
+ import { resolve as resolvePath } from 'node:path';
16
+ import { fileURLToPath as fp } from 'node:url';
17
+ import { dirname as dn } from 'node:path';
18
+
19
+ // v0.17-P1: CLI version for version-pin checks (F8). Reproducibility for CDD —
20
+ // users can pin the docguard version their config was last validated against.
21
+ const _PKG = JSON.parse(readFileSync(resolvePath(dn(fp(import.meta.url)), '..', '..', 'package.json'), 'utf-8'));
22
+ const CLI_VERSION = _PKG.version;
23
+
24
+ /**
25
+ * v0.17-P1: parse a semver-ish version string into a comparable tuple.
26
+ * Tolerates trailing pre-release tags (`0.16.0-rc.1`). Returns null on garbage.
27
+ */
28
+ function _parseSemver(v) {
29
+ if (!v || typeof v !== 'string') return null;
30
+ const m = v.match(/^(\d+)\.(\d+)\.(\d+)/);
31
+ if (!m) return null;
32
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
33
+ }
34
+
35
+ /**
36
+ * v0.17-P1: returns +1 if a > b, 0 if equal, -1 if a < b. Unparseable
37
+ * input sorts as equal (silent — never blocks a guard run).
38
+ */
39
+ function _semverCompare(a, b) {
40
+ const pa = _parseSemver(a);
41
+ const pb = _parseSemver(b);
42
+ if (!pa || !pb) return 0;
43
+ for (let i = 0; i < 3; i++) {
44
+ if (pa[i] > pb[i]) return 1;
45
+ if (pa[i] < pb[i]) return -1;
46
+ }
47
+ return 0;
48
+ }
49
+
50
+ /**
51
+ * v0.17-P1: emit a "you're running a newer CLI than the config was pinned
52
+ * against" nudge. Cheap, file-local check. Returns the nudge text or null.
53
+ */
54
+ function _checkVersionPin(config) {
55
+ const pinned = config.docguardVersion;
56
+ if (!pinned) return null;
57
+ const cmp = _semverCompare(CLI_VERSION, pinned);
58
+ if (cmp > 0) {
59
+ return `Running CLI v${CLI_VERSION} but config pins v${pinned}. ` +
60
+ `New validators/rules may have appeared. Run \`docguard guard --pin\` to update the pin once you've reviewed any new findings.`;
61
+ }
62
+ if (cmp < 0) {
63
+ return `Running CLI v${CLI_VERSION} but config pins v${pinned} (newer). ` +
64
+ `Older CLI may be missing checks the config expects. Upgrade with \`npm i -g docguard-cli@latest\`.`;
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * v0.17-P1: update the docguardVersion field in .docguard.json after a
71
+ * successful guard run. Triggered by `docguard guard --pin`. Idempotent.
72
+ */
73
+ function _updateVersionPin(projectDir) {
74
+ const cfgPath = resolvePath(projectDir, '.docguard.json');
75
+ if (!existsSync(cfgPath)) return { written: false, reason: '.docguard.json not found — run `docguard init` first' };
76
+ let raw, cfg;
77
+ try { raw = readFileSync(cfgPath, 'utf-8'); cfg = JSON.parse(raw); } catch (e) {
78
+ return { written: false, reason: `could not parse .docguard.json: ${e.message}` };
79
+ }
80
+ if (cfg.docguardVersion === CLI_VERSION) {
81
+ return { written: false, reason: `already pinned at v${CLI_VERSION}` };
82
+ }
83
+ const prev = cfg.docguardVersion || '(unset)';
84
+ cfg.docguardVersion = CLI_VERSION;
85
+ writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
86
+ return { written: true, from: prev, to: CLI_VERSION };
87
+ }
14
88
  import { validateStructure, validateDocSections } from '../validators/structure.mjs';
15
89
  import { validateDrift } from '../validators/drift.mjs';
16
90
  import { validateChangelog } from '../validators/changelog.mjs';
@@ -364,6 +438,15 @@ export function runGuard(projectDir, config, flags) {
364
438
  console.log(`\n ${c.yellow}↑ ${upgradeHint}${c.reset}`);
365
439
  }
366
440
 
441
+ // v0.17-P1: version-pin nudge. When .docguard.json carries a
442
+ // docguardVersion field and the running CLI doesn't match, emit a
443
+ // one-line note. Keeps CDD reproducibility honest — "same project,
444
+ // same docs, different score across versions" no longer silent.
445
+ const pinHint = _checkVersionPin(config);
446
+ if (pinHint) {
447
+ console.log(`\n ${c.yellow}📌 ${pinHint}${c.reset}`);
448
+ }
449
+
367
450
  // K-6 / S-2: sweep-needed nudge. Aggregates freshness warnings — if 2+
368
451
  // canonical docs are stale (matching the "X code commits since last doc
369
452
  // update" pattern), suggest a single `docguard sync --write` pass that
@@ -402,6 +485,24 @@ export function runGuard(projectDir, config, flags) {
402
485
 
403
486
  console.log('');
404
487
 
488
+ // v0.17-P1: --pin updates docguardVersion in .docguard.json to the running
489
+ // CLI version. Only meaningful AFTER a clean (or near-clean) guard run —
490
+ // pinning to a version that just failed defeats the reproducibility goal.
491
+ // We allow pinning when status is PASS or WARN; refuse on FAIL.
492
+ if (flags.pin) {
493
+ if (data.status === 'FAIL') {
494
+ console.log(` ${c.red}✗ Cannot --pin after a FAIL run.${c.reset} Fix the errors first, then retry.`);
495
+ } else {
496
+ const r = _updateVersionPin(projectDir);
497
+ if (r.written) {
498
+ console.log(` ${c.green}📌 docguardVersion pinned: ${r.from} → ${r.to}${c.reset}`);
499
+ } else {
500
+ console.log(` ${c.dim}📌 ${r.reason}${c.reset}`);
501
+ }
502
+ }
503
+ console.log('');
504
+ }
505
+
405
506
  // v0.5: severity-aware exit codes (see runGuardInternal for the rollup).
406
507
  if (data.effectiveErrors > 0) process.exit(1);
407
508
  if (data.effectiveWarnings > 0) process.exit(2);
@@ -4,6 +4,56 @@
4
4
  */
5
5
 
6
6
  import { existsSync, writeFileSync, mkdirSync, chmodSync, readFileSync, unlinkSync } from 'node:fs';
7
+
8
+ // v0.16-P3: managed-block markers. Letting users extend the hook with their
9
+ // own commands (data-file guards, lint checks, etc.) without us clobbering
10
+ // them on re-install. Format:
11
+ //
12
+ // #!/bin/sh
13
+ // # ... user's prelude ...
14
+ //
15
+ // # BEGIN DOCGUARD MANAGED — do not edit between these markers
16
+ // ... DocGuard's content ...
17
+ // # END DOCGUARD MANAGED
18
+ //
19
+ // # ... user's postlude ...
20
+ //
21
+ // On re-install, we splice ONLY the content between the markers, preserving
22
+ // everything else verbatim. Without markers (legacy hooks or third-party
23
+ // pre-existing hooks), behavior falls back to the existing --force flow.
24
+ const BEGIN_MARKER = '# BEGIN DOCGUARD MANAGED — do not edit between these markers';
25
+ const END_MARKER = '# END DOCGUARD MANAGED';
26
+
27
+ /**
28
+ * Wrap a hook body in BEGIN/END markers so future re-installs can splice
29
+ * just the managed portion. The shebang stays at the top, outside the block.
30
+ */
31
+ function wrapManaged(body) {
32
+ // Pull shebang off the front if present so it stays at the top.
33
+ const lines = body.split('\n');
34
+ let shebang = '';
35
+ if (lines[0] && lines[0].startsWith('#!')) {
36
+ shebang = lines.shift() + '\n';
37
+ }
38
+ return `${shebang}${BEGIN_MARKER}\n${lines.join('\n').replace(/\n+$/, '')}\n${END_MARKER}\n`;
39
+ }
40
+
41
+ /**
42
+ * Splice DocGuard's managed content into an existing hook file that has
43
+ * the BEGIN/END markers. Returns the new file content (string) or null
44
+ * when the markers aren't found (caller falls back to legacy behavior).
45
+ */
46
+ function spliceManagedBlock(existing, newBody) {
47
+ const startIdx = existing.indexOf(BEGIN_MARKER);
48
+ const endIdx = existing.indexOf(END_MARKER);
49
+ if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) return null;
50
+ const before = existing.slice(0, startIdx);
51
+ const after = existing.slice(endIdx + END_MARKER.length);
52
+ // newBody has its own shebang — strip it since we're splicing into the
53
+ // middle of an existing file (which already has one).
54
+ const bodyNoShebang = newBody.replace(/^#!.*\n/, '');
55
+ return `${before}${BEGIN_MARKER}\n${bodyNoShebang.replace(/\n+$/, '')}\n${END_MARKER}${after}`;
56
+ }
7
57
  import { resolve } from 'node:path';
8
58
  import { c } from '../shared.mjs';
9
59
 
@@ -228,26 +278,45 @@ export function runHooks(projectDir, config, flags) {
228
278
 
229
279
  for (const name of hookTypes) {
230
280
  const hookPath = resolve(hooksDir, name);
281
+ const useAutofix = name === 'pre-commit' && flags.autoFix;
282
+ const newContent = wrapManaged(useAutofix ? PRE_COMMIT_AUTOFIX : HOOKS[name].content);
283
+ const desc = useAutofix ? 'Apply mechanical fixes (fix --write) then guard' : HOOKS[name].description;
231
284
 
232
- if (existsSync(hookPath) && !flags.force) {
233
- // Check if it's already a DocGuard hook
285
+ if (existsSync(hookPath)) {
234
286
  const existing = readFileSync(hookPath, 'utf-8');
235
- if (existing.includes('DocGuard')) {
236
- console.log(` ${c.dim}⏭️ ${name} (DocGuard hook already installed)${c.reset}`);
287
+
288
+ // v0.16-P3: managed-block path splice just the DocGuard portion,
289
+ // preserve everything outside it. The user can extend the hook with
290
+ // their own commands above/below the markers without losing them on
291
+ // re-install.
292
+ const spliced = spliceManagedBlock(existing, newContent);
293
+ if (spliced !== null) {
294
+ writeFileSync(hookPath, spliced, 'utf-8');
295
+ chmodSync(hookPath, 0o755);
296
+ console.log(` ${c.green}↻ ${name}${c.reset}: updated DocGuard managed block (preserved user content around it)`);
297
+ installed++;
298
+ continue;
299
+ }
300
+
301
+ // No markers found. Two sub-cases:
302
+ // (a) Legacy DocGuard hook (pre-v0.16, no markers, contains "DocGuard")
303
+ // → upgrade in place when --force is set
304
+ // (b) Third-party hook the user wrote themselves
305
+ // → refuse without --force; warn about clobber risk
306
+ if (!flags.force) {
307
+ if (existing.includes('DocGuard')) {
308
+ console.log(` ${c.yellow}⚠️ ${name}: legacy DocGuard hook (pre-v0.16) without managed markers. Re-run with --force to upgrade it to the managed-block format.${c.reset}`);
309
+ } else {
310
+ console.log(` ${c.yellow}⚠️ ${name}: an existing hook is present and has no DocGuard markers. Re-run with --force to overwrite (your hook will be replaced — back it up first!).${c.reset}`);
311
+ }
237
312
  skipped++;
238
313
  continue;
239
314
  }
240
- console.log(` ${c.yellow}⚠️ ${name}: existing hook found (use --force to overwrite)${c.reset}`);
241
- skipped++;
242
- continue;
315
+ // --force path: write fresh managed-block version
243
316
  }
244
317
 
245
- // pre-commit supports an auto-fix variant (applies mechanical fixes first).
246
- const useAutofix = name === 'pre-commit' && flags.autoFix;
247
- const content = useAutofix ? PRE_COMMIT_AUTOFIX : HOOKS[name].content;
248
- writeFileSync(hookPath, content, 'utf-8');
249
- chmodSync(hookPath, 0o755); // Make executable
250
- const desc = useAutofix ? 'Apply mechanical fixes (fix --write) then guard' : HOOKS[name].description;
318
+ writeFileSync(hookPath, newContent, 'utf-8');
319
+ chmodSync(hookPath, 0o755);
251
320
  console.log(` ${c.green}✅ ${name}${c.reset}: ${desc}`);
252
321
  installed++;
253
322
  }
@@ -253,10 +253,14 @@ poetry.lock
253
253
 
254
254
  // ── Spec-Kit Integration (Extension-First) ────────────────────────────
255
255
  // Delegate LLM/IDE detection and spec-kit skill install to `specify init`
256
+ // v0.16-P8: --no-spec-kit lets users skip the .specify/.agent/commands
257
+ // scaffolding (minimalist library projects, CI containers, etc.).
256
258
  const specKitAvailable = isSpecKitAvailable();
257
259
  const specKitInitialized = isSpecKitInitialized(projectDir);
258
260
 
259
- if (specKitAvailable && !specKitInitialized) {
261
+ if (flags.noSpecKit) {
262
+ console.log(`\n ${c.dim}⏭️ Spec Kit init skipped (--no-spec-kit).${c.reset}`);
263
+ } else if (specKitAvailable && !specKitInitialized) {
260
264
  console.log(`\n ${c.bold}🌱 Spec Kit Integration${c.reset}`);
261
265
 
262
266
  // Detect which AI agent is in use (matches spec-kit's --ai flag)
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Memory Command — v0.17-P2.
3
+ *
4
+ * `docguard memory` shows the documentation-memory accuracy headline that
5
+ * already appears in `docguard score`, but adds a `--diff` mode that drills
6
+ * into WHICH claims don't match code. Reported by a Python user:
7
+ *
8
+ * "Memory accuracy 83% with no drill-down. The headline number was the
9
+ * only signal — there's no `docguard memory --diff` to show which doc
10
+ * claim doesn't match the code."
11
+ *
12
+ * The numbers are the same ones `score` shows; this command's value is
13
+ * making them inspectable per-domain.
14
+ *
15
+ * Domains drilled into:
16
+ * - Endpoints: API-REFERENCE.md vs scanned routes
17
+ * - Entities: DATA-MODEL.md vs scanned schemas
18
+ * - Env vars: ENVIRONMENT.md vs process.env / import.meta.env usage
19
+ * - Tech: ARCHITECTURE.md vs detected stack
20
+ *
21
+ * Zero NPM dependencies. Pure orchestration of existing diff helpers.
22
+ */
23
+
24
+ import { c } from '../shared.mjs';
25
+ import { diffRoutes, diffEntities, diffEnvVars, diffTechStack } from './diff.mjs';
26
+
27
+ /**
28
+ * Compute an accuracy score for a single domain. Returns:
29
+ * { matched, total, accuracy: 0..100, onlyInDocs, onlyInCode }
30
+ * `null` when the domain isn't applicable (e.g. no API-REFERENCE.md).
31
+ */
32
+ function _domainAccuracy(d) {
33
+ if (!d) return null;
34
+ const matched = (d.matched || []).length;
35
+ const onlyDocs = (d.onlyInDocs || []).length;
36
+ const onlyCode = (d.onlyInCode || []).length;
37
+ const total = matched + onlyDocs + onlyCode;
38
+ if (total === 0) return null;
39
+ return {
40
+ title: d.title,
41
+ icon: d.icon,
42
+ matched,
43
+ onlyInDocs: d.onlyInDocs || [],
44
+ onlyInCode: d.onlyInCode || [],
45
+ total,
46
+ accuracy: Math.round((matched / total) * 100),
47
+ };
48
+ }
49
+
50
+ export function runMemory(projectDir, config, flags) {
51
+ const isJson = flags.format === 'json';
52
+ const wantsDiff = flags.diff || (flags.args || []).includes('--diff');
53
+
54
+ const domains = {
55
+ endpoints: _domainAccuracy(diffRoutes(projectDir, config)),
56
+ entities: _domainAccuracy(diffEntities(projectDir, config)),
57
+ envVars: _domainAccuracy(diffEnvVars(projectDir, config)),
58
+ techStack: _domainAccuracy(diffTechStack(projectDir, config)),
59
+ };
60
+
61
+ // Roll up across applicable domains.
62
+ let totalMatched = 0;
63
+ let totalChecks = 0;
64
+ for (const d of Object.values(domains)) {
65
+ if (!d) continue;
66
+ totalMatched += d.matched;
67
+ totalChecks += d.total;
68
+ }
69
+ const overallAccuracy = totalChecks > 0
70
+ ? Math.round((totalMatched / totalChecks) * 100)
71
+ : 0;
72
+
73
+ if (isJson) {
74
+ console.log(JSON.stringify({
75
+ project: config.projectName,
76
+ accuracy: overallAccuracy,
77
+ domains,
78
+ totals: { matched: totalMatched, checks: totalChecks },
79
+ timestamp: new Date().toISOString(),
80
+ }, null, 2));
81
+ return;
82
+ }
83
+
84
+ // ── Text output ──
85
+ console.log(`${c.bold}🧠 DocGuard Memory${c.reset} ${c.dim}— ${config.projectName}${c.reset}\n`);
86
+
87
+ const accColor = overallAccuracy >= 90 ? c.green : overallAccuracy >= 70 ? c.yellow : c.red;
88
+ console.log(` ${c.bold}Accuracy:${c.reset} ${accColor}${overallAccuracy}%${c.reset} ${c.dim}(${totalMatched}/${totalChecks} doc claims match code)${c.reset}\n`);
89
+
90
+ if (totalChecks === 0) {
91
+ console.log(` ${c.dim}No applicable domains found — add canonical docs (API-REFERENCE.md, DATA-MODEL.md, ENVIRONMENT.md) and rerun.${c.reset}`);
92
+ return;
93
+ }
94
+
95
+ // Per-domain breakdown
96
+ console.log(` ${c.bold}By domain:${c.reset}`);
97
+ for (const [name, d] of Object.entries(domains)) {
98
+ if (!d) continue;
99
+ const domainColor = d.accuracy >= 90 ? c.green : d.accuracy >= 70 ? c.yellow : c.red;
100
+ console.log(` ${d.icon} ${c.cyan}${d.title.padEnd(22)}${c.reset} ${domainColor}${String(d.accuracy).padStart(3)}%${c.reset} ${c.dim}${d.matched}/${d.total} matched${c.reset}`);
101
+ }
102
+
103
+ if (!wantsDiff) {
104
+ if (overallAccuracy < 100) {
105
+ console.log(`\n ${c.dim}Run ${c.cyan}docguard memory --diff${c.dim} to see WHICH claims don't match.${c.reset}`);
106
+ }
107
+ return;
108
+ }
109
+
110
+ // --diff mode: detail per domain
111
+ console.log(`\n ${c.bold}── Drill-down ──${c.reset}`);
112
+ let anyShown = false;
113
+ for (const [_, d] of Object.entries(domains)) {
114
+ if (!d) continue;
115
+ if (d.onlyInDocs.length === 0 && d.onlyInCode.length === 0) continue;
116
+ anyShown = true;
117
+ console.log(`\n ${d.icon} ${c.bold}${d.title}${c.reset} ${c.dim}(${d.accuracy}%)${c.reset}`);
118
+
119
+ if (d.onlyInDocs.length > 0) {
120
+ console.log(` ${c.red}✗ In docs but missing from code${c.reset} ${c.dim}(${d.onlyInDocs.length}):${c.reset}`);
121
+ for (const item of d.onlyInDocs.slice(0, 10)) {
122
+ console.log(` ${c.red}-${c.reset} ${item}`);
123
+ }
124
+ if (d.onlyInDocs.length > 10) console.log(` ${c.dim}... ${d.onlyInDocs.length - 10} more${c.reset}`);
125
+ }
126
+
127
+ if (d.onlyInCode.length > 0) {
128
+ console.log(` ${c.yellow}⚠ In code but missing from docs${c.reset} ${c.dim}(${d.onlyInCode.length}):${c.reset}`);
129
+ for (const item of d.onlyInCode.slice(0, 10)) {
130
+ console.log(` ${c.yellow}+${c.reset} ${item}`);
131
+ }
132
+ if (d.onlyInCode.length > 10) console.log(` ${c.dim}... ${d.onlyInCode.length - 10} more${c.reset}`);
133
+ }
134
+ }
135
+
136
+ if (!anyShown) {
137
+ console.log(`\n ${c.green}✅ All claims match — nothing to drill into.${c.reset}`);
138
+ } else {
139
+ console.log(`\n ${c.dim}Fix options:${c.reset}`);
140
+ console.log(` ${c.dim}• Removed-from-code items: ${c.cyan}docguard fix --write${c.dim} (deletes documented-but-absent endpoints)${c.reset}`);
141
+ console.log(` ${c.dim}• Missing-from-docs items: ${c.cyan}/docguard.fix --doc <name>${c.dim} (AI fills in the gap)${c.reset}`);
142
+ }
143
+ }
@@ -21,8 +21,15 @@ const WEIGHTS = {
21
21
  };
22
22
 
23
23
  export function runScore(projectDir, config, flags) {
24
- console.log(`${c.bold}📊 DocGuard Score ${config.projectName}${c.reset}`);
25
- console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
24
+ // v0.16-P1: suppress banner in JSON mode so stdout stays parseable.
25
+ // Was already fixed for guard/diagnose in v0.12; score/trace/diff missed
26
+ // the pattern. Reported on a Python project where `score --format json`
27
+ // mixed ANSI escapes with JSON.
28
+ const isJson = flags.format === 'json';
29
+ if (!isJson) {
30
+ console.log(`${c.bold}📊 DocGuard Score — ${config.projectName}${c.reset}`);
31
+ console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
32
+ }
26
33
 
27
34
  const { scores, totalScore, grade, details } = calcAllScores(projectDir, config);
28
35
 
@@ -21,65 +21,107 @@ const CODE_EXTENSIONS = new Set([
21
21
  '.py', '.java', '.go', '.rs', '.rb', '.php', '.cs',
22
22
  ]);
23
23
 
24
+ // v0.16-P2: language-aware patterns. The original JS/TS-only sets created
25
+ // false-negative warnings on Python/Rust/Go/Java projects (reported by the
26
+ // quick-recon-tool Python user: TEST-SPEC.md was flagged unlinked even
27
+ // though Python tests existed because `.test.mjs` didn't match `test_*.py`).
28
+ // Each `glob` is now a single regex that ALSO matches the equivalent
29
+ // patterns in other ecosystems we care about.
24
30
  const TEST_PATTERNS = [
25
- /\.test\.[jt]sx?$/,
26
- /\.spec\.[jt]sx?$/,
27
- /test_.*\.py$/,
31
+ // JS/TS
32
+ /\.test\.[jt]sx?$/, /\.spec\.[jt]sx?$/, /\.test\.(mjs|cjs)$/,
33
+ // Python — pytest conventions
34
+ /(^|\/)test_[^/]+\.py$/, /[^/]+_test\.py$/, /(^|\/)tests?\/[^/]+\.py$/,
35
+ // Go
28
36
  /_test\.go$/,
29
- /Test\.java$/,
37
+ // Java/Kotlin — JUnit/TestNG conventions
38
+ /(?:Test|Tests|Spec|IT)\.(?:java|kt)$/,
39
+ // Rust — tests live in tests/ or as #[cfg(test)] modules; pattern below covers integration tests
40
+ /(^|\/)tests\/[^/]+\.rs$/,
41
+ // Ruby/RSpec
42
+ /_spec\.rb$/, /_test\.rb$/,
43
+ // PHP/PHPUnit
44
+ /Test\.php$/, /(^|\/)tests?\/[^/]+\.php$/,
30
45
  ];
31
46
 
32
47
  /**
33
48
  * Mapping of canonical documents to the code/config artifacts they trace to.
34
49
  * Each entry defines what source patterns prove coverage of that canonical doc.
50
+ *
51
+ * v0.16-P2: every glob is now multi-language. JS/TS patterns are preserved
52
+ * (the most common case); Python/Rust/Go/Java/Ruby/PHP equivalents are
53
+ * appended so non-JS projects don't false-negative.
35
54
  */
36
55
  const TRACE_MAP = {
37
56
  'ARCHITECTURE.md': {
38
57
  standard: 'arc42 / C4 Model',
39
58
  sourcePatterns: [
40
- { label: 'Entry points', glob: /^(index|main|app|server)\.[jt]sx?$/ },
41
- { label: 'Config files', glob: /^(package\.json|tsconfig.*|next\.config|vite\.config)/ },
42
- { label: 'Route handlers', glob: /(routes?|api|pages|app)\// },
59
+ // Entry points: JS (index/main/app/server.[jt]sx?), Python (__main__.py, main.py, app.py, cli.py),
60
+ // Go (main.go, cmd/), Rust (main.rs, lib.rs), Java (Application.java, Main.java)
61
+ { label: 'Entry points', glob: /(?:^|\/)(?:index|main|app|server|cli|__main__|Application|Main)\.(?:[jt]sx?|mjs|cjs|py|go|rs|java|kt|rb)$|(?:^|\/)cmd\// },
62
+ // Config files: JS (package.json/tsconfig/next.config/vite.config), Python (pyproject.toml/setup.py/setup.cfg),
63
+ // Rust (Cargo.toml), Go (go.mod), Java/Kotlin (pom.xml/build.gradle), Ruby (Gemfile), PHP (composer.json)
64
+ { label: 'Config files', glob: /(?:^|\/)(?:package\.json|tsconfig|next\.config|vite\.config|pyproject\.toml|setup\.(?:py|cfg)|Cargo\.toml|go\.mod|pom\.xml|build\.gradle|Gemfile|composer\.json)/ },
65
+ // Route handlers + module dirs
66
+ { label: 'Route handlers / modules', glob: /(?:^|\/)(?:routes?|api|pages|app|controllers?|handlers?|views?|services?)\// },
43
67
  ],
44
68
  },
45
69
  'DATA-MODEL.md': {
46
70
  standard: 'C4 Component / ER (Chen)',
47
71
  sourcePatterns: [
48
- { label: 'Schema definitions', glob: /(schema|model|entity|migration|prisma)/i },
49
- { label: 'Type definitions', glob: /types?\.[jt]sx?$/ },
50
- { label: 'Database configs', glob: /(drizzle|knex|sequelize|typeorm)/i },
72
+ // Schema/model files: JS (schema/model/entity/migration/prisma), Python (models.py/schema.py/Pydantic/SQLAlchemy),
73
+ // Go (models/), Rust (struct definitions in models/), Java (entities/)
74
+ { label: 'Schema definitions', glob: /(?:schema|model|entity|migration|prisma)/i },
75
+ // Type definitions: JS types.ts, Python types.py, Rust types.rs
76
+ { label: 'Type definitions', glob: /(?:^|\/)types?\.(?:[jt]sx?|mjs|py|rs|go|java|kt)$/ },
77
+ // ORM/database libs (any language)
78
+ { label: 'Database configs', glob: /(?:drizzle|knex|sequelize|typeorm|sqlalchemy|alembic|django|diesel|sqlx|gorm|hibernate|active.?record)/i },
51
79
  ],
52
80
  },
53
81
  'TEST-SPEC.md': {
54
82
  standard: 'ISO/IEC/IEEE 29119-3',
55
83
  sourcePatterns: [
56
- { label: 'Test files', glob: /\.(test|spec)\.(mjs|cjs|[jt]sx?)$/ },
57
- { label: 'Test config', glob: /(jest|vitest|playwright|cypress)\.config/ },
58
- { label: 'E2E tests', glob: /(e2e|integration)\// },
84
+ // Test files in any ecosystem (mirrors TEST_PATTERNS above)
85
+ { label: 'Test files', glob: /\.(?:test|spec)\.(?:mjs|cjs|[jt]sx?)$|(?:^|\/)test_[^/]+\.py$|[^/]+_test\.py$|_test\.go$|(?:Test|Spec|IT)\.(?:java|kt)$|(?:^|\/)tests?\/[^/]+\.(?:rs|py|rb|php)$|_(?:spec|test)\.rb$|Test\.php$/ },
86
+ // Test runner configs: JS (jest/vitest/playwright/cypress), Python (pytest.ini/tox.ini), Rust (Cargo.toml has [[test]]),
87
+ // Java (pom.xml/build.gradle), Go (no config file typically)
88
+ { label: 'Test config', glob: /(?:jest|vitest|playwright|cypress|pytest|tox|phpunit)\.config|(?:^|\/)pytest\.ini$|(?:^|\/)tox\.ini$|(?:^|\/)phpunit\.xml$/ },
89
+ { label: 'E2E / integration tests', glob: /(?:^|\/)(?:e2e|integration|tests?\/integration)\// },
59
90
  ],
60
91
  },
61
92
  'SECURITY.md': {
62
93
  standard: 'OWASP ASVS v4.0',
63
94
  sourcePatterns: [
64
- { label: 'Auth modules', glob: /(auth|login|session|jwt|oauth|middleware)/i },
65
- { label: 'Secret configs', glob: /\.(env|env\.example|env\.local)$/ },
66
- { label: 'Gitignore', glob: /^\.gitignore$/ },
95
+ // Auth modules semantic, language-agnostic
96
+ { label: 'Auth modules', glob: /(?:auth|login|session|jwt|oauth|middleware|guard|csrf|cors|permissions?|policy)/i },
97
+ // Secret configs .env family + secrets.* / keyring patterns
98
+ { label: 'Secret configs', glob: /\.env(?:\.|$)|(?:^|\/)secrets?\.(?:py|js|ts|yaml|yml|json)$|keyring/i },
99
+ // Gitignore + ignore files
100
+ { label: 'Ignore files', glob: /^\.(?:git|docker|npm)ignore$/ },
67
101
  ],
68
102
  },
69
103
  'ENVIRONMENT.md': {
70
104
  standard: '12-Factor App',
71
105
  sourcePatterns: [
72
- { label: 'Env files', glob: /\.env/ },
73
- { label: 'Docker configs', glob: /(Dockerfile|docker-compose|\.dockerignore)/ },
74
- { label: 'CI/CD configs', glob: /\.(github|gitlab-ci|circleci)/ },
106
+ // .env family across all ecosystems
107
+ { label: 'Env files', glob: /\.env(?:\.|$)|(?:^|\/)\.envrc$/ },
108
+ // Containerization
109
+ { label: 'Container configs', glob: /(?:^|\/)(?:Dockerfile|docker-compose|\.dockerignore|Containerfile)/ },
110
+ // Python venv / requirements / lock files
111
+ { label: 'Python env', glob: /(?:^|\/)(?:requirements[^/]*\.txt|Pipfile|poetry\.lock|uv\.lock|pyproject\.toml)$/ },
112
+ // CI/CD configs
113
+ { label: 'CI/CD configs', glob: /(?:^|\/)\.(?:github|gitlab-ci|circleci|drone|gitea)/ },
75
114
  ],
76
115
  },
77
116
  'API-REFERENCE.md': {
78
117
  standard: 'OpenAPI 3.1',
79
118
  sourcePatterns: [
80
- { label: 'Route handlers', glob: /(routes?|controllers?|handlers?)\// },
81
- { label: 'OpenAPI spec', glob: /(openapi|swagger)\.(json|ya?ml)/ },
82
- { label: 'API middleware', glob: /middleware\// },
119
+ // Route handlers + Python views/urls + Java/Spring controllers
120
+ { label: 'Route handlers', glob: /(?:^|\/)(?:routes?|controllers?|handlers?|views?|urls?\.py)/ },
121
+ // OpenAPI / API specs
122
+ { label: 'API spec', glob: /(?:openapi|swagger|asyncapi)\.(?:json|ya?ml)/ },
123
+ // Middleware / decorators
124
+ { label: 'API middleware', glob: /(?:^|\/)middleware\/|decorators?\.py$/ },
83
125
  ],
84
126
  },
85
127
  };
@@ -190,9 +232,14 @@ export function runTrace(projectDir, config, flags) {
190
232
  return runTraceReverse(projectDir, config, flags);
191
233
  }
192
234
 
193
- console.log(`${c.bold}🔗 DocGuard Trace ${config.projectName}${c.reset}`);
194
- console.log(`${c.dim} Directory: ${projectDir}${c.reset}`);
195
- console.log(`${c.dim} Generating requirements traceability matrix...${c.reset}\n`);
235
+ // v0.16-P1: same headless-mode pattern as guard/score. Reported by Python
236
+ // user — trace --format json was leaking ANSI escapes before the body.
237
+ const isJson = flags.format === 'json';
238
+ if (!isJson) {
239
+ console.log(`${c.bold}🔗 DocGuard Trace — ${config.projectName}${c.reset}`);
240
+ console.log(`${c.dim} Directory: ${projectDir}${c.reset}`);
241
+ console.log(`${c.dim} Generating requirements traceability matrix...${c.reset}\n`);
242
+ }
196
243
 
197
244
  // ── 1. Build set of required doc basenames from config ──
198
245
  const requiredDocs = new Set(
package/cli/docguard.mjs CHANGED
@@ -42,6 +42,8 @@ import { runLlms } from './commands/llms.mjs';
42
42
  import { runSetup } from './commands/setup.mjs';
43
43
  import { runUpgrade } from './commands/upgrade.mjs';
44
44
  import { runImpact } from './commands/impact.mjs';
45
+ import { runExplain } from './commands/explain.mjs';
46
+ import { runMemory } from './commands/memory.mjs';
45
47
  import { ensureSkills } from './ensure-skills.mjs';
46
48
 
47
49
  // ── Shared constants (imported to break circular dependencies) ──────────
@@ -120,7 +122,10 @@ export function loadConfig(projectDir) {
120
122
  ? deepMerge(defaults, profilePreset)
121
123
  : defaults;
122
124
 
123
- const merged = deepMerge(withProfile, userConfig);
125
+ // v0.17-P4: normalize validator/severity keys before merging so the
126
+ // user can write either kebab-case (`test-spec`) or camelCase (`testSpec`)
127
+ // and the internal lookups (always camelCase) still hit.
128
+ const merged = deepMerge(withProfile, normalizeConfig(userConfig));
124
129
  merged.profile = profileName;
125
130
 
126
131
  // Auto-detect project type if not set
@@ -209,6 +214,46 @@ function getProjectTypeDefaults(type) {
209
214
  return defaults[type] || defaults.unknown;
210
215
  }
211
216
 
217
+ /**
218
+ * v0.17-P4: normalize validator-key naming so users can write either
219
+ * `validators: { "test-spec": true }` (kebab-case, matches CLI display)
220
+ * or `validators: { testSpec: true }` (camelCase, matches JSON internals)
221
+ * in `.docguard.json`. We normalize the WHOLE config tree's known validator
222
+ * keys to camelCase before merging. Same treatment applied to `severity`.
223
+ *
224
+ * Non-validator keys are left alone. Unknown keys (forward-compat) are
225
+ * normalized blindly: kebab-case→camelCase always.
226
+ */
227
+ const _KNOWN_VALIDATORS = [
228
+ 'structure', 'docsSync', 'drift', 'changelog', 'testSpec', 'environment',
229
+ 'security', 'architecture', 'freshness', 'traceability', 'docsDiff',
230
+ 'apiSurface', 'metadataSync', 'docsCoverage', 'docQuality', 'todoTracking',
231
+ 'schemaSync', 'specKit', 'crossReference', 'generatedStaleness',
232
+ 'metricsConsistency',
233
+ ];
234
+
235
+ function _kebabToCamel(k) {
236
+ return k.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
237
+ }
238
+
239
+ function _normalizeValidatorKeys(map) {
240
+ if (!map || typeof map !== 'object' || Array.isArray(map)) return map;
241
+ const out = {};
242
+ for (const [k, v] of Object.entries(map)) {
243
+ const normalized = k.includes('-') ? _kebabToCamel(k) : k;
244
+ out[normalized] = v;
245
+ }
246
+ return out;
247
+ }
248
+
249
+ function normalizeConfig(cfg) {
250
+ if (!cfg || typeof cfg !== 'object') return cfg;
251
+ const out = { ...cfg };
252
+ if (out.validators) out.validators = _normalizeValidatorKeys(out.validators);
253
+ if (out.severity) out.severity = _normalizeValidatorKeys(out.severity);
254
+ return out;
255
+ }
256
+
212
257
  function deepMerge(target, source) {
213
258
  const result = { ...target };
214
259
  for (const key of Object.keys(source)) {
@@ -395,6 +440,25 @@ async function main() {
395
440
  // avoid collision with `docguard init --profile <name>`. `--show-timings`
396
441
  // is the long form for users who prefer explicit verbs.
397
442
  flags.timings = true;
443
+ } else if (args[i] === '--quiet' || args[i] === '-q') {
444
+ // v0.16-P5: suppress the banner + ensureSkills decorative line.
445
+ // Useful inside git hooks (every commit prints the banner otherwise)
446
+ // and any CI/script that pipes docguard's output.
447
+ flags.quiet = true;
448
+ } else if (args[i] === '--no-spec-kit') {
449
+ // v0.16-P8: opt-out of automatic Spec Kit init during `docguard init`.
450
+ // Default stays on (discoverability), but lets minimalist library
451
+ // projects skip the .specify/.agent/commands scaffolding.
452
+ flags.noSpecKit = true;
453
+ } else if (args[i] === '--pin') {
454
+ // v0.17-P1: `docguard guard --pin` records the running CLI version
455
+ // into .docguard.json (`docguardVersion` field) after a successful run.
456
+ // Different from `--pr` (used by upgrade) — this is for guard.
457
+ flags.pin = true;
458
+ } else if (args[i] === '--diff') {
459
+ // v0.17-P2: `docguard memory --diff` drills into accuracy mismatches.
460
+ // Distinct from the `diff` command itself (which is a top-level cmd).
461
+ flags.diff = true;
398
462
  } else if (!args[i].startsWith('--') && i > 0) {
399
463
  // Positional args go into flags.args for commands that take them (e.g.
400
464
  // `docguard trace --reverse <path>`). Skip the command itself (i === 0).
@@ -442,10 +506,12 @@ async function main() {
442
506
  // ensureSkills' install message would corrupt the output for any
443
507
  // programmatic consumer (CI, dashboards, the Score-on-PR Action recipe).
444
508
  // Headless flags (`--write`, `--check-only`, `--auto`) also suppress chrome.
509
+ // v0.16-P5: --quiet (-q) joins the headless club for users who want
510
+ // banner-free output without committing to a specific machine format.
445
511
  const jsonMode = flags.format === 'json';
446
- const headless = jsonMode || flags.write || flags.checkOnly || flags.changedOnly;
512
+ const headless = jsonMode || flags.write || flags.checkOnly || flags.changedOnly || flags.quiet;
447
513
 
448
- if (!jsonMode) printBanner();
514
+ if (!headless) printBanner();
449
515
 
450
516
  const config = loadConfig(projectDir);
451
517
 
@@ -527,6 +593,13 @@ async function main() {
527
593
  case 'impact':
528
594
  runImpact(projectDir, config, flags);
529
595
  break;
596
+ case 'explain':
597
+ case 'help-warning':
598
+ runExplain(projectDir, config, flags);
599
+ break;
600
+ case 'memory':
601
+ runMemory(projectDir, config, flags);
602
+ break;
530
603
  default:
531
604
  console.error(`${c.red}Unknown command: ${command}${c.reset}`);
532
605
  console.log(`Run ${c.cyan}docguard --help${c.reset} for usage.`);
@@ -45,9 +45,21 @@ export function validateEnvironment(projectDir, config) {
45
45
  // tokens like `VITE_` (the convention prefix) from being treated as a real
46
46
  // variable name.
47
47
  const varRe = /`([A-Z][A-Z0-9_]*[A-Z0-9])`/g;
48
+ // v0.16-P4: skip backticked SYSTEM env vars (PATH, HOME, USER, etc.).
49
+ // They appear in ENVIRONMENT.md prose ("the venv `PATH`") but aren't
50
+ // user-set application vars. Mirrors the same skip in diff.mjs.
51
+ const SYSTEM = new Set([
52
+ 'PATH','HOME','USER','USERNAME','SHELL','PWD','OLDPWD','TMPDIR','TEMP','TMP',
53
+ 'LANG','LC_ALL','LC_CTYPE','LC_MESSAGES','TZ',
54
+ 'EDITOR','VISUAL','PAGER','TERM','COLORTERM',
55
+ 'DISPLAY','SSH_AUTH_SOCK','SSH_CONNECTION','SSH_TTY',
56
+ 'XDG_CONFIG_HOME','XDG_DATA_HOME','XDG_CACHE_HOME','XDG_RUNTIME_DIR',
57
+ 'CI','GITHUB_TOKEN','GITHUB_ACTIONS','GITHUB_REF','GITHUB_SHA','NODE_ENV',
58
+ ]);
48
59
  let m;
49
60
  while ((m = varRe.exec(content)) !== null) {
50
61
  if (m[1].length < 3) continue; // 'OK' / 'ID' etc. are too short to be env var refs
62
+ if (SYSTEM.has(m[1])) continue; // v0.16-P4: prose mentions of system vars are not docs
51
63
  documented.add(m[1]);
52
64
  }
53
65
  for (const envFile of ['.env.example', '.env.template']) {
@@ -89,14 +89,36 @@ export function validateDocSections(projectDir, config) {
89
89
  // Match an actual heading at line start (any level), not a substring that
90
90
  // could appear in a table-of-contents link or a code block.
91
91
  const headingText = section.replace(/^#+\s*/, '');
92
- const headingRe = new RegExp(
93
- '^#{2,6}\\s+' + headingText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b',
94
- 'm'
92
+ const escapedHeading = headingText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
93
+ const headingRe = new RegExp('^#{2,6}\\s+' + escapedHeading + '\\b', 'm');
94
+ // v0.16-P7: N/A marker. A project can declare a required section as
95
+ // "not applicable" via an HTML comment instead of writing boilerplate
96
+ // "Absent by design" prose. Format:
97
+ // <!-- docguard:section authentication n/a — JWT not used; we're a CLI -->
98
+ // The section name in the marker is matched case-insensitively against
99
+ // the heading slug (lowercase, hyphenated). Requires a reason after `—`
100
+ // or `--` so it's not a silent opt-out.
101
+ const slug = headingText.toLowerCase()
102
+ .replace(/[^a-z0-9\s-]/g, '')
103
+ .replace(/\s+/g, '-');
104
+ // Reason must start with an actual letter or digit (not `>` from `-->`
105
+ // and not whitespace). This makes sure `<!-- ... n/a -->` (no reason)
106
+ // is rejected, while `<!-- ... n/a — CLI tool -->` is accepted.
107
+ const naRe = new RegExp(
108
+ '<!--\\s*docguard:section\\s+' + slug.replace(/-/g, '[-_]') + '\\s+n/a\\s*[—-]+\\s*[A-Za-z0-9]',
109
+ 'i'
95
110
  );
96
111
  if (headingRe.test(content)) {
97
112
  results.passed++;
113
+ } else if (naRe.test(content)) {
114
+ // v0.16-P7: explicit N/A — counts as passed (the project has owned
115
+ // the absence) and doesn't pollute the warnings list.
116
+ results.passed++;
98
117
  } else {
99
- results.warnings.push(`${file}: missing section "${section}"`);
118
+ results.warnings.push(
119
+ `${file}: missing section "${section}". ` +
120
+ `If genuinely not applicable, add: <!-- docguard:section ${slug} n/a — your reason -->`
121
+ );
100
122
  }
101
123
  }
102
124
  }
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Run DocGuard guard validation — check project documentation against CDD standards with 22 validators
2
+ description: Run DocGuard guard validation — check project documentation against CDD standards with all validators
3
3
  handoffs:
4
4
  - label: Fix All Issues
5
5
  agent: docguard.fix
@@ -23,7 +23,7 @@ Run the DocGuard CLI to validate all documentation against Canonical-Driven Deve
23
23
  npx docguard-cli guard
24
24
  ```
25
25
 
26
- 2. **Parse the output**. Each of the 22 validators reports ✅ (pass), ⚠️ (warning), ❌ (fail), or ➖ (N/A — nothing to validate). **A ➖ N/A is NOT a pass**: it means the validator found nothing to check (e.g. no API-REFERENCE.md, no DB schema, no layer boundaries declared). Don't read N/A as "healthy" — read it as "not assessed".
26
+ 2. **Parse the output**. Each of the validators reports ✅ (pass), ⚠️ (warning), ❌ (fail), or ➖ (N/A — nothing to validate). **A ➖ N/A is NOT a pass**: it means the validator found nothing to check (e.g. no API-REFERENCE.md, no DB schema, no layer boundaries declared). Don't read N/A as "healthy" — read it as "not assessed".
27
27
 
28
28
  | Validator | What It Checks |
29
29
  |-----------|---------------|
@@ -14,7 +14,7 @@ handoffs:
14
14
 
15
15
  # DocGuard Guard
16
16
 
17
- Validate your project against its canonical documentation. Runs 160+ automated checks across 22 validators.
17
+ Validate your project against its canonical documentation. Runs 160+ automated checks across validators.
18
18
 
19
19
  ## User Input
20
20
 
@@ -3,7 +3,7 @@ schema_version: "1.0"
3
3
  extension:
4
4
  id: "docguard"
5
5
  name: "DocGuard — CDD Enforcement"
6
- version: "0.15.3"
6
+ version: "0.17.0"
7
7
  description: "Canonical-Driven Development enforcement as a true spec-kit extension. LLM-first design with 19 automated validators, 4 AI behavior skills, spec-kit skill chaining, and workflow hooks. Zero NPM runtime dependencies."
8
8
  author: "Ricardo Accioly"
9
9
  repository: "https://github.com/raccioly/docguard"
@@ -58,7 +58,7 @@ provides:
58
58
  workflows:
59
59
  - name: "docguard-guard"
60
60
  file: "templates/github-workflows/docguard-guard.yml"
61
- description: "Mandatory CI gate — runs all 20 validators on PR + main push"
61
+ description: "Mandatory CI gate — runs all validators on PR + main push"
62
62
  - name: "docguard-autofix"
63
63
  file: "templates/github-workflows/docguard-autofix.yml"
64
64
  description: "PR-time auto-fix — applies mechanical doc fixes + comments summary"
@@ -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.15.3
9
+ version: 0.17.0
10
10
  source: extensions/spec-kit-docguard/skills/docguard-fix
11
11
  ---
12
- <!-- docguard:version: 0.15.3 -->
12
+ <!-- docguard:version: 0.17.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.15.3
10
+ version: 0.17.0
11
11
  source: extensions/spec-kit-docguard/skills/docguard-guard
12
12
  ---
13
- <!-- docguard:version: 0.15.3 -->
13
+ <!-- docguard:version: 0.17.0 -->
14
14
 
15
15
  # DocGuard Guard Skill
16
16
 
@@ -139,7 +139,7 @@ For each finding, provide a **specific, actionable fix** — not "fix the issue"
139
139
 
140
140
  Based on the triage results:
141
141
 
142
- - **If all PASS**: "All 22 validators passed. Project is CDD-compliant. Ready to commit."
142
+ - **If all PASS**: "All validators passed. Project is CDD-compliant. Ready to commit."
143
143
  - **If only MEDIUM/LOW warnings**: "Non-blocking warnings found. Safe to commit, but consider running `/docguard.fix` for automated remediation."
144
144
  - **If HIGH or CRITICAL failures**: "Blocking issues found. Fix these before committing. Suggest running `/docguard.fix --doc [most impactful doc]` next."
145
145
 
@@ -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.15.3
9
+ version: 0.17.0
10
10
  source: extensions/spec-kit-docguard/skills/docguard-review
11
11
  ---
12
- <!-- docguard:version: 0.15.3 -->
12
+ <!-- docguard:version: 0.17.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.15.3
9
+ version: 0.17.0
10
10
  source: extensions/spec-kit-docguard/skills/docguard-score
11
11
  ---
12
- <!-- docguard:version: 0.15.3 -->
12
+ <!-- docguard:version: 0.17.0 -->
13
13
 
14
14
  # DocGuard Score Skill
15
15
 
@@ -4,7 +4,7 @@ description: Keep canonical documentation ALWAYS UP TO DATE. Refreshes code-trut
4
4
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
5
5
  metadata:
6
6
  author: docguard
7
- version: 0.15.3
7
+ version: 0.17.0
8
8
  source: extensions/spec-kit-docguard/skills/docguard-sync
9
9
  ---
10
10
 
@@ -1,4 +1,4 @@
1
- # DocGuard Guard — runs all 20 validators on every PR and main push.
1
+ # DocGuard Guard — runs all validators on every PR and main push.
2
2
  #
3
3
  # This is the canonical CI gate. It does NOT modify your repo — it only
4
4
  # reports. Pair with `docguard-autofix.yml` if you want mechanical fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.15.3",
3
+ "version": "0.17.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": {
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Run DocGuard guard validation — check all 22 validators and fix any issues
2
+ description: Run DocGuard guard validation — check all validators and fix any issues
3
3
  handoffs:
4
4
  - label: Fix Issues
5
5
  agent: docguard.fix
@@ -19,7 +19,7 @@ You are an AI agent enforcing Canonical-Driven Development (CDD) compliance usin
19
19
  npx docguard-cli guard
20
20
  ```
21
21
 
22
- Read the output. It shows pass (✅), warn (⚠️), or fail (❌) for each of the 22 validators:
22
+ Read the output. It shows pass (✅), warn (⚠️), or fail (❌) for each of the validators:
23
23
 
24
24
  | Priority | Validators |
25
25
  |----------|-----------|