docguard-cli 0.17.0 → 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.
- package/cli/commands/diff.mjs +19 -11
- package/cli/commands/guard.mjs +46 -0
- package/cli/commands/score.mjs +65 -0
- package/cli/scanners/memory-plan.mjs +127 -15
- package/cli/validators/environment.mjs +6 -4
- package/cli/validators/generated-staleness.mjs +56 -2
- package/extensions/spec-kit-docguard/extension.yml +1 -1
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +1 -1
- package/package.json +1 -1
package/cli/commands/diff.mjs
CHANGED
|
@@ -200,23 +200,31 @@ export function diffEntities(dir, config = {}) {
|
|
|
200
200
|
};
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
// v0.16-P4
|
|
204
|
-
// prose ("the venv `PATH`"
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
// as
|
|
203
|
+
// v0.16-P4 (revised in v0.17.1): conservative denylist of system env vars
|
|
204
|
+
// that appear in prose ("the venv `PATH`") but are never user-set app env
|
|
205
|
+
// vars. v0.17.1-B7: trimmed to TRULY-system-only after wu feedback —
|
|
206
|
+
// NODE_ENV / CI / GITHUB_* are legitimately app env vars when read via
|
|
207
|
+
// process.env. Including them caused diff to falsely flag `NODE_ENV` as
|
|
208
|
+
// "in code but not docs" even when ENVIRONMENT.md documented it.
|
|
208
209
|
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
210
|
+
// Rule of thumb for inclusion: would a sane Node/Python/Go app ever
|
|
211
|
+
// `process.env.X` this name and treat it as app config? If yes → NOT a
|
|
212
|
+
// system var. PATH/HOME/SHELL/TERM never satisfy that bar.
|
|
211
213
|
const SYSTEM_ENV_VARS = new Set([
|
|
212
|
-
|
|
214
|
+
// POSIX shell / OS
|
|
215
|
+
'PATH', 'HOME', 'USER', 'USERNAME', 'SHELL', 'PWD', 'OLDPWD',
|
|
216
|
+
'TMPDIR', 'TEMP', 'TMP',
|
|
217
|
+
// Locale
|
|
213
218
|
'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES', 'TZ',
|
|
219
|
+
// Terminal / interactive
|
|
214
220
|
'EDITOR', 'VISUAL', 'PAGER', 'TERM', 'COLORTERM',
|
|
221
|
+
// SSH / Display
|
|
215
222
|
'DISPLAY', 'SSH_AUTH_SOCK', 'SSH_CONNECTION', 'SSH_TTY',
|
|
223
|
+
// XDG base directory spec
|
|
216
224
|
'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_RUNTIME_DIR',
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
225
|
+
// NOTE: NODE_ENV / CI / GITHUB_* used to be here. Removed in v0.17.1
|
|
226
|
+
// because apps DO read them as app config (e.g. NODE_ENV=production
|
|
227
|
+
// gates branching in nearly every Node.js app).
|
|
220
228
|
]);
|
|
221
229
|
|
|
222
230
|
export function diffEnvVars(dir, config = {}) {
|
package/cli/commands/guard.mjs
CHANGED
|
@@ -66,6 +66,40 @@ function _checkVersionPin(config) {
|
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* v0.17.1: small in-code highlight reel surfaced when a project's pinned
|
|
71
|
+
* version is behind the running CLI. The biggest recurring user pattern is
|
|
72
|
+
* "I asked for feature X" → "X shipped two releases ago". This eliminates
|
|
73
|
+
* the need to grep the CHANGELOG. Keep entries short and command-oriented.
|
|
74
|
+
*
|
|
75
|
+
* Add to this table on every release. Format: [introducedIn, oneLineFeature].
|
|
76
|
+
*/
|
|
77
|
+
const _RELEASE_HIGHLIGHTS = [
|
|
78
|
+
['0.13.0', '`docguard sync --since <ref>` — surgical refresh of code-truth doc sections'],
|
|
79
|
+
['0.13.1', '`docguard impact --since <ref>` — changed files → affected canonical docs map'],
|
|
80
|
+
['0.13.1', '`Cross-Reference` validator + "did you mean #X?" hints for broken anchors'],
|
|
81
|
+
['0.14.1', '`docguard fix --write` auto-fixes high-confidence anchor matches'],
|
|
82
|
+
['0.15.0', '`docguard guard --timings` — per-validator wall-time profile'],
|
|
83
|
+
['0.15.0', '`.docguard.json` JSON Schema for VS Code autocomplete'],
|
|
84
|
+
['0.16.0', '`docguard explain "<warning>"` — paste any warning, get the validator help'],
|
|
85
|
+
['0.16.0', '`docguard guard --quiet` — suppress banner in hooks/CI'],
|
|
86
|
+
['0.16.0', '`docguard init --no-spec-kit` — opt out of Spec Kit scaffolding'],
|
|
87
|
+
['0.16.0', 'Language-aware test patterns (Python `test_*.py`, Rust `tests/*.rs`, Go `*_test.go`, ...)'],
|
|
88
|
+
['0.17.0', '`docguard memory --diff` — drill into accuracy mismatches (which claim ≠ code)'],
|
|
89
|
+
['0.17.0', '`docguard guard --pin` — record running CLI version into .docguard.json'],
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
function _whatsNewSince(pinnedVersion) {
|
|
93
|
+
if (!pinnedVersion) return [];
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const [introducedIn, feature] of _RELEASE_HIGHLIGHTS) {
|
|
96
|
+
if (_semverCompare(introducedIn, pinnedVersion) > 0) {
|
|
97
|
+
out.push(`v${introducedIn}: ${feature}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
69
103
|
/**
|
|
70
104
|
* v0.17-P1: update the docguardVersion field in .docguard.json after a
|
|
71
105
|
* successful guard run. Triggered by `docguard guard --pin`. Idempotent.
|
|
@@ -445,6 +479,18 @@ export function runGuard(projectDir, config, flags) {
|
|
|
445
479
|
const pinHint = _checkVersionPin(config);
|
|
446
480
|
if (pinHint) {
|
|
447
481
|
console.log(`\n ${c.yellow}📌 ${pinHint}${c.reset}`);
|
|
482
|
+
// v0.17.1: surface features added since the pinned version so users
|
|
483
|
+
// who pinned at v0.12 and just upgraded actually KNOW about sync,
|
|
484
|
+
// impact, explain, memory --diff, etc. The biggest user complaint
|
|
485
|
+
// pattern is "I asked for X but X already shipped two releases ago."
|
|
486
|
+
const whatsNew = _whatsNewSince(config.docguardVersion);
|
|
487
|
+
if (whatsNew.length > 0) {
|
|
488
|
+
console.log(` ${c.dim}New since v${config.docguardVersion}:${c.reset}`);
|
|
489
|
+
for (const item of whatsNew.slice(0, 5)) {
|
|
490
|
+
console.log(` ${c.dim}• ${item}${c.reset}`);
|
|
491
|
+
}
|
|
492
|
+
if (whatsNew.length > 5) console.log(` ${c.dim}... ${whatsNew.length - 5} more in CHANGELOG.md${c.reset}`);
|
|
493
|
+
}
|
|
448
494
|
}
|
|
449
495
|
|
|
450
496
|
// K-6 / S-2: sweep-needed nudge. Aggregates freshness warnings — if 2+
|
package/cli/commands/score.mjs
CHANGED
|
@@ -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
|
|
35
|
-
*
|
|
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
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
-
*
|
|
45
|
-
*
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
}
|
|
@@ -45,16 +45,18 @@ 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
|
|
49
|
-
//
|
|
50
|
-
//
|
|
48
|
+
// v0.16-P4 (revised in v0.17.1-B7): skip backticked SYSTEM env vars
|
|
49
|
+
// (PATH, HOME, USER, etc.) that appear in ENVIRONMENT.md prose. Trimmed
|
|
50
|
+
// to TRULY-system-only after wu feedback — NODE_ENV / CI / GITHUB_* were
|
|
51
|
+
// causing asymmetric flagging between diff and this validator. Apps
|
|
52
|
+
// legitimately treat NODE_ENV as app config; keep the list to vars that
|
|
53
|
+
// no sane application would read as runtime config.
|
|
51
54
|
const SYSTEM = new Set([
|
|
52
55
|
'PATH','HOME','USER','USERNAME','SHELL','PWD','OLDPWD','TMPDIR','TEMP','TMP',
|
|
53
56
|
'LANG','LC_ALL','LC_CTYPE','LC_MESSAGES','TZ',
|
|
54
57
|
'EDITOR','VISUAL','PAGER','TERM','COLORTERM',
|
|
55
58
|
'DISPLAY','SSH_AUTH_SOCK','SSH_CONNECTION','SSH_TTY',
|
|
56
59
|
'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
60
|
]);
|
|
59
61
|
let m;
|
|
60
62
|
while ((m = varRe.exec(content)) !== null) {
|
|
@@ -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.
|
|
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.
|
|
9
|
+
version: 0.18.1
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-fix
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
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.
|
|
10
|
+
version: 0.18.1
|
|
11
11
|
source: extensions/spec-kit-docguard/skills/docguard-guard
|
|
12
12
|
---
|
|
13
|
-
<!-- docguard:version: 0.
|
|
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.
|
|
9
|
+
version: 0.18.1
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-review
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
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.
|
|
9
|
+
version: 0.18.1
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-score
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
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.
|
|
7
|
+
version: 0.18.1
|
|
8
8
|
source: extensions/spec-kit-docguard/skills/docguard-sync
|
|
9
9
|
---
|
|
10
10
|
|
package/package.json
CHANGED