docguard-cli 0.17.1 → 0.18.1

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.
@@ -8,6 +8,63 @@ import { resolve, join, extname } from 'node:path';
8
8
  import { execSync } from 'node:child_process';
9
9
  import { c } from '../shared.mjs';
10
10
  import { validateSecurity } from '../validators/security.mjs';
11
+ import { runGuardInternal } from './guard.mjs';
12
+
13
+ /**
14
+ * v0.18-P3: map score categories to the validator keys that contribute.
15
+ * One category can roll up multiple validators (e.g. "environment" pulls
16
+ * from Environment validator findings). When --diff fires, we use this
17
+ * to surface the underlying warnings.
18
+ */
19
+ const _SCORE_TO_VALIDATORS = {
20
+ structure: ['structure'],
21
+ docQuality: ['docQuality', 'docsCoverage', 'docsSync'],
22
+ testing: ['testSpec', 'todoTracking'],
23
+ security: ['security'],
24
+ environment: ['environment'],
25
+ drift: ['drift'],
26
+ changelog: ['changelog'],
27
+ architecture: ['architecture'],
28
+ };
29
+
30
+ function _showScoreDiff(projectDir, config, scores) {
31
+ console.log(` ${c.bold}── Drill-down (--diff) ──${c.reset}\n`);
32
+ // Pull live guard data; reuses the in-process plan cache so this is
33
+ // cheap when run right after the score calc.
34
+ const guard = runGuardInternal(projectDir, config);
35
+ const byKey = new Map(guard.validators.map(v => [v.key, v]));
36
+
37
+ let anyShown = false;
38
+ for (const [category, score] of Object.entries(scores)) {
39
+ if (score === 100) continue;
40
+ const validatorKeys = _SCORE_TO_VALIDATORS[category];
41
+ if (!validatorKeys) continue;
42
+ const warnings = [];
43
+ const errors = [];
44
+ for (const k of validatorKeys) {
45
+ const v = byKey.get(k);
46
+ if (!v) continue;
47
+ warnings.push(...(v.warnings || []));
48
+ errors.push(...(v.errors || []));
49
+ }
50
+ if (warnings.length === 0 && errors.length === 0) continue;
51
+ anyShown = true;
52
+ console.log(` ${c.yellow}${category}${c.reset} ${c.dim}(${score}/100)${c.reset}`);
53
+ for (const e of errors.slice(0, 5)) console.log(` ${c.red}✗${c.reset} ${e}`);
54
+ for (const w of warnings.slice(0, 5)) console.log(` ${c.yellow}⚠${c.reset} ${w}`);
55
+ const totalIssues = errors.length + warnings.length;
56
+ if (totalIssues > 5) console.log(` ${c.dim}... ${totalIssues - 5} more${c.reset}`);
57
+ console.log('');
58
+ }
59
+
60
+ if (!anyShown) {
61
+ console.log(` ${c.dim}No specific findings available for the weakest categories. They may be scoring below 100 due to structural/quality heuristics rather than discrete check failures.${c.reset}\n`);
62
+ } else {
63
+ console.log(` ${c.dim}Fix options:${c.reset}`);
64
+ console.log(` ${c.dim}• Run ${c.cyan}docguard explain "<warning>"${c.dim} for the full validator help on any line above${c.reset}`);
65
+ console.log(` ${c.dim}• Run ${c.cyan}docguard fix --write${c.dim} for the mechanical fixes${c.reset}`);
66
+ }
67
+ }
11
68
 
12
69
  const WEIGHTS = {
13
70
  structure: 25, // Required files exist
@@ -116,6 +173,14 @@ export function runScore(projectDir, config, flags) {
116
173
  console.log('');
117
174
  }
118
175
 
176
+ // v0.18-P3: --diff drill-down. Symmetric to v0.17 memory --diff.
177
+ // Shows WHICH specific checks dragged each weak category down by joining
178
+ // the guard validator warnings to score categories. Cheap: we already
179
+ // import runGuardInternal; one extra guard run on `--diff` is acceptable.
180
+ if (flags.diff) {
181
+ _showScoreDiff(projectDir, config, scores);
182
+ }
183
+
119
184
  // ── Tax Estimate (--tax flag) ──
120
185
  if (flags.tax) {
121
186
  const tax = estimateDocTax(projectDir, config, scores);
@@ -30,21 +30,112 @@ const md = {
30
30
  };
31
31
 
32
32
  /**
33
- * v0.15-P1: in-process cache. buildMemoryPlan is expensive (~400ms on
34
- * an enterprise client project, 33% of total guard validator time) because it triggers
35
- * routes/schemas/screens/frontend scanners — all of which walk the source
36
- * tree. Within a single guard run, sync, generate, and the Generated-
37
- * Staleness validator all ask for the SAME plan; without caching they each
38
- * re-pay the cost.
33
+ * v0.15-P1: in-process cache (Map). buildMemoryPlan is expensive (~400ms on
34
+ * an enterprise client project) because it triggers routes/schemas/screens/
35
+ * frontend scanners — all of which walk the source tree.
39
36
  *
40
- * Cache key: projectDir + a config fingerprint that captures the fields the
41
- * scanners actually consume (sourceRoot, ignore, projectType). Other config
42
- * mutations (e.g. changedFiles per-validator) don't invalidate the plan.
37
+ * v0.18-P2: cross-process cache (`.docguard/plan.cache.json`). CI flows that
38
+ * run guard sync fix as separate processes each pay the build cost.
39
+ * The disk cache shares the plan across processes, keyed by a tree-state
40
+ * hash so we invalidate when the source tree changes.
43
41
  *
44
- * Bypass with `_skipCache: true` in opts used by tests and any caller that
45
- * wants a fresh scan.
42
+ * Cache key: projectDir + a config fingerprint (sourceRoot, ignore,
43
+ * projectType, profile). Other config mutations (e.g. changedFiles
44
+ * per-validator) don't invalidate the plan.
45
+ *
46
+ * Bypass with `_skipCache: true` in opts — used by tests.
46
47
  */
48
+ import { createHash } from 'node:crypto';
49
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'node:fs';
50
+ import { resolve as resolvePath, join as joinPath } from 'node:path';
51
+ import { execFileSync } from 'node:child_process';
52
+
47
53
  const _memoryPlanCache = new Map(); // key → plan
54
+ const _DISK_CACHE_PATH = '.docguard/plan.cache.json';
55
+ const _DISK_CACHE_VERSION = '1'; // bump if cache shape changes
56
+
57
+ /**
58
+ * v0.18-P2: tree-state hash. Cheap signature of the source tree that
59
+ * changes whenever something a scanner would care about changes. We use:
60
+ * - git HEAD commit SHA (when in a git repo) — captures committed state
61
+ * - mtime sum of top-level config files (package.json, pyproject.toml,
62
+ * Cargo.toml, etc.) — captures uncommitted bumps to deps
63
+ * Combined into a 12-char hex fingerprint.
64
+ *
65
+ * NOT a perfect cache key — a user editing src/foo.ts without bumping a
66
+ * config file won't invalidate. But guard/sync/fix all run in quick
67
+ * succession within a CI step, and the user's flow IS bump + commit + run.
68
+ * The tradeoff favors speed: the worst case is one stale plan per CI run,
69
+ * recoverable with `--no-plan-cache` or a tree change.
70
+ */
71
+ function _treeStateHash(projectDir) {
72
+ let signal = '';
73
+ // git HEAD
74
+ try {
75
+ const sha = execFileSync('git', ['rev-parse', 'HEAD'], {
76
+ cwd: projectDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
77
+ }).trim();
78
+ signal += `git:${sha};`;
79
+ } catch { /* not a git repo, or no commits */ }
80
+ // mtime of common manifest files
81
+ const manifests = [
82
+ 'package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod',
83
+ 'pom.xml', 'build.gradle', 'Gemfile', 'composer.json',
84
+ '.docguard.json',
85
+ ];
86
+ for (const m of manifests) {
87
+ try {
88
+ const s = statSync(resolvePath(projectDir, m));
89
+ signal += `${m}:${s.mtimeMs};`;
90
+ } catch { /* not present */ }
91
+ }
92
+ return createHash('sha256').update(signal).digest('hex').slice(0, 12);
93
+ }
94
+
95
+ /**
96
+ * v0.18-P2: read the disk cache. Returns null when the file is missing,
97
+ * the schema version mismatches, the tree hash doesn't match, or anything
98
+ * about the load is suspicious. Never throws — cache miss is silent.
99
+ */
100
+ function _readDiskCache(projectDir, configKey) {
101
+ try {
102
+ const p = resolvePath(projectDir, _DISK_CACHE_PATH);
103
+ if (!existsSync(p)) return null;
104
+ const data = JSON.parse(readFileSync(p, 'utf-8'));
105
+ if (data.v !== _DISK_CACHE_VERSION) return null;
106
+ if (data.configKey !== configKey) return null;
107
+ const currentHash = _treeStateHash(projectDir);
108
+ if (data.treeHash !== currentHash) return null;
109
+ return data.plan || null;
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * v0.18-P2: write the disk cache. Best-effort — failures are silent (the
117
+ * in-process cache still works). `.docguard/` directory created if needed.
118
+ */
119
+ function _writeDiskCache(projectDir, configKey, plan) {
120
+ try {
121
+ const fullDir = resolvePath(projectDir, '.docguard');
122
+ if (!existsSync(fullDir)) mkdirSync(fullDir, { recursive: true });
123
+ const payload = {
124
+ v: _DISK_CACHE_VERSION,
125
+ configKey,
126
+ treeHash: _treeStateHash(projectDir),
127
+ plan,
128
+ writtenAt: new Date().toISOString(),
129
+ };
130
+ writeFileSync(
131
+ resolvePath(projectDir, _DISK_CACHE_PATH),
132
+ JSON.stringify(payload), // compact — this file isn't human-edited
133
+ 'utf-8'
134
+ );
135
+ } catch {
136
+ // swallow — the cache is auxiliary
137
+ }
138
+ }
48
139
 
49
140
  export function clearMemoryPlanCache() {
50
141
  _memoryPlanCache.clear();
@@ -67,14 +158,35 @@ function _cacheKey(projectDir, config) {
67
158
  * agentTasks: flattened prose tasks the AI must write.
68
159
  */
69
160
  export function buildMemoryPlan(projectDir, config = {}, opts = {}) {
70
- if (!opts._skipCache) {
71
- const key = _cacheKey(projectDir, config);
161
+ const useCache = !opts._skipCache;
162
+ const key = useCache ? _cacheKey(projectDir, config) : null;
163
+
164
+ // L1: in-process Map (same-run guard → sync → fix).
165
+ if (useCache) {
72
166
  const cached = _memoryPlanCache.get(key);
73
167
  if (cached) return cached;
74
168
  }
169
+
170
+ // L2: cross-process disk cache (CI guard → CI sync → CI fix).
171
+ // v0.18-P2: opt-in via config.diskCache !== false (default ON).
172
+ // Tree-state hash invalidates when source files change.
173
+ const diskCacheEnabled = useCache && config.diskCache !== false;
174
+ if (diskCacheEnabled) {
175
+ const onDisk = _readDiskCache(projectDir, key);
176
+ if (onDisk) {
177
+ _memoryPlanCache.set(key, onDisk); // promote to L1
178
+ return onDisk;
179
+ }
180
+ }
181
+
182
+ // Miss — build fresh.
75
183
  const result = _buildMemoryPlanUncached(projectDir, config);
76
- if (!opts._skipCache) {
77
- _memoryPlanCache.set(_cacheKey(projectDir, config), result);
184
+
185
+ if (useCache) {
186
+ _memoryPlanCache.set(key, result);
187
+ }
188
+ if (diskCacheEnabled) {
189
+ _writeDiskCache(projectDir, key, result);
78
190
  }
79
191
  return result;
80
192
  }
@@ -23,12 +23,55 @@
23
23
  * @req SC-M1-004 — N/A when no source=code sections present in any doc
24
24
  */
25
25
 
26
- import { existsSync, readFileSync, statSync } from 'node:fs';
27
- import { resolve, basename } from 'node:path';
26
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
27
+ import { resolve, basename, join } from 'node:path';
28
28
 
29
29
  import { buildMemoryPlan } from '../scanners/memory-plan.mjs';
30
30
  import { getSection } from '../writers/sections.mjs';
31
31
 
32
+ /**
33
+ * v0.18-P1 fast-path: cheap pre-flight to detect whether ANY canonical doc
34
+ * has a `<!-- docguard:section ... source=code -->` marker OR a `status:
35
+ * draft` frontmatter. If neither exists anywhere, this validator has
36
+ * nothing to do — skip the expensive buildMemoryPlan call (~400ms on
37
+ * mid-sized repos, was 26-33% of total guard validator time).
38
+ *
39
+ * Returns { hasMarkers, hasDrafts }.
40
+ */
41
+ function _quickScan(projectDir) {
42
+ const out = { hasMarkers: false, hasDrafts: false };
43
+ const candidateDirs = [
44
+ resolve(projectDir, 'docs-canonical'),
45
+ projectDir, // for README.md, AGENTS.md, etc.
46
+ ];
47
+ // We only need a single match in any file to know the validator has work.
48
+ // Short-circuit aggressively: stop the moment we find either signal.
49
+ for (const dir of candidateDirs) {
50
+ if (!existsSync(dir)) continue;
51
+ let entries;
52
+ try { entries = readdirSync(dir); } catch { continue; }
53
+ for (const entry of entries) {
54
+ if (!entry.endsWith('.md')) continue;
55
+ // Skip very large files quickly — for canonical docs, > 200 KB is unusual
56
+ // and almost certainly not the marker-heavy file we're looking for.
57
+ let stat;
58
+ try { stat = statSync(join(dir, entry)); } catch { continue; }
59
+ if (!stat.isFile()) continue;
60
+ if (stat.size > 200_000) continue;
61
+ let content;
62
+ try { content = readFileSync(join(dir, entry), 'utf-8'); } catch { continue; }
63
+ if (!out.hasMarkers && /<!--\s*docguard:section\s+[^>]*source=code/i.test(content)) {
64
+ out.hasMarkers = true;
65
+ }
66
+ if (!out.hasDrafts && /(?:^---\s*\n[\s\S]*?\bstatus:\s*draft\b[\s\S]*?\n---|<!--\s*status:\s*draft\s*-->)/im.test(content)) {
67
+ out.hasDrafts = true;
68
+ }
69
+ if (out.hasMarkers && out.hasDrafts) return out;
70
+ }
71
+ }
72
+ return out;
73
+ }
74
+
32
75
  /**
33
76
  * S-7: how long a generated doc may sit in `status: draft` before we warn.
34
77
  * 14 days is the v0.13.1 default — long enough to absorb a typical sprint,
@@ -62,6 +105,17 @@ export function validateGeneratedStaleness(projectDir, config = {}) {
62
105
  // of just warning. No AI needed — the scanner already knows the right body.
63
106
  const result = { errors: [], warnings: [], passed: 0, total: 0, fixes: [] };
64
107
 
108
+ // v0.18-P1: cheap pre-flight. If no canonical doc has a source=code marker
109
+ // AND no doc is in status:draft, this validator has nothing to do — skip
110
+ // the expensive buildMemoryPlan call. Generated-Staleness used to be
111
+ // 26-33% of total validator time on projects with NO markers, all
112
+ // wasted. The fast-path scans markdown files for the marker substring
113
+ // only — no parsing, no tree walk.
114
+ const quick = _quickScan(projectDir);
115
+ if (!quick.hasMarkers && !quick.hasDrafts) {
116
+ return { ...result, applicable: false, note: 'no docguard:section markers and no status:draft docs' };
117
+ }
118
+
65
119
  // Build the canonical memory plan (what the docs SHOULD contain). If this
66
120
  // fails or produces no docs, the validator is N/A.
67
121
  let plan;
@@ -3,7 +3,7 @@ schema_version: "1.0"
3
3
  extension:
4
4
  id: "docguard"
5
5
  name: "DocGuard — CDD Enforcement"
6
- version: "0.17.1"
6
+ version: "0.18.1"
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"
@@ -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.17.1
9
+ version: 0.18.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-fix
11
11
  ---
12
- <!-- docguard:version: 0.17.1 -->
12
+ <!-- docguard:version: 0.18.1 -->
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.17.1
10
+ version: 0.18.1
11
11
  source: extensions/spec-kit-docguard/skills/docguard-guard
12
12
  ---
13
- <!-- docguard:version: 0.17.1 -->
13
+ <!-- docguard:version: 0.18.1 -->
14
14
 
15
15
  # DocGuard Guard Skill
16
16
 
@@ -6,10 +6,10 @@ description: Cross-document consistency analysis and quality assessment. Perform
6
6
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
7
7
  metadata:
8
8
  author: docguard
9
- version: 0.17.1
9
+ version: 0.18.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-review
11
11
  ---
12
- <!-- docguard:version: 0.17.1 -->
12
+ <!-- docguard:version: 0.18.1 -->
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.17.1
9
+ version: 0.18.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-score
11
11
  ---
12
- <!-- docguard:version: 0.17.1 -->
12
+ <!-- docguard:version: 0.18.1 -->
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.17.1
7
+ version: 0.18.1
8
8
  source: extensions/spec-kit-docguard/skills/docguard-sync
9
9
  ---
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.17.1",
3
+ "version": "0.18.1",
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": {