docguard-cli 0.25.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/cli/commands/agent.mjs +135 -0
- package/cli/commands/explain.mjs +23 -1
- package/cli/commands/feedback.mjs +163 -0
- package/cli/commands/generate.mjs +12 -1
- package/cli/commands/guard.mjs +77 -15
- package/cli/commands/score.mjs +65 -32
- package/cli/config.mjs +6 -1
- package/cli/docguard.mjs +61 -5
- package/cli/findings.mjs +194 -0
- package/cli/scanners/inventory.mjs +140 -0
- package/cli/scanners/memory-plan.mjs +84 -14
- package/cli/scanners/project-type.mjs +60 -4
- package/cli/scanners/routes.mjs +11 -5
- package/cli/shared-ignore.mjs +40 -0
- package/cli/shared-source.mjs +93 -3
- package/cli/validators/doc-quality.mjs +14 -3
- package/cli/validators/freshness.mjs +31 -2
- package/cli/validators/metrics-consistency.mjs +26 -2
- package/cli/validators/security.mjs +117 -31
- package/cli/validators/todo-tracking.mjs +4 -0
- package/cli/writers/mechanical.mjs +21 -3
- 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 +2 -2
- package/package.json +1 -1
|
@@ -8,6 +8,18 @@
|
|
|
8
8
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
9
9
|
import { resolve, join, extname } from 'node:path';
|
|
10
10
|
import { shouldIgnore, relPosix } from '../shared-ignore.mjs';
|
|
11
|
+
import { mkFinding, resultFromFindings, lineSuppresses } from '../findings.mjs';
|
|
12
|
+
|
|
13
|
+
// Each secret pattern maps to a stable finding code (see cli/findings.mjs CODES)
|
|
14
|
+
// so it is `explain`-able and inline-suppressible (`// docguard:ignore SEC00x`).
|
|
15
|
+
const LABEL_TO_CODE = {
|
|
16
|
+
'hardcoded password': 'SEC001',
|
|
17
|
+
'hardcoded API key': 'SEC002',
|
|
18
|
+
'hardcoded secret key': 'SEC003',
|
|
19
|
+
'hardcoded access token': 'SEC004',
|
|
20
|
+
'AWS Access Key ID': 'SEC005',
|
|
21
|
+
'API secret key (Stripe/OpenAI pattern)': 'SEC006',
|
|
22
|
+
};
|
|
11
23
|
|
|
12
24
|
const CODE_EXTENSIONS = new Set([
|
|
13
25
|
'.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
|
|
@@ -54,11 +66,46 @@ function isSafePlaceholder(line, matchStr) {
|
|
|
54
66
|
return SAFE_PATTERNS.some(p => p.test(line));
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
/**
|
|
70
|
+
* v0.27 (field report #1): a password-style key whose VALUE is natural
|
|
71
|
+
* language — an error message, validation copy, UI string — is almost never a
|
|
72
|
+
* credential. e.g. a "New password must differ from recent passwords"
|
|
73
|
+
* validation message assigned to such a key.
|
|
74
|
+
*
|
|
75
|
+
* We don't drop these (a real secret that happens to read like prose must still
|
|
76
|
+
* surface — false-green is the failure mode this tool exists to prevent); we
|
|
77
|
+
* downgrade them to a LOW-CONFIDENCE warning the agent can suppress inline,
|
|
78
|
+
* instead of a blocking error. Heuristic per the field report: ≥3 words, OR
|
|
79
|
+
* ≥2 internal spaces, OR ends in sentence punctuation.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} value - the literal inside the quotes
|
|
82
|
+
*/
|
|
83
|
+
function looksLikeProse(value) {
|
|
84
|
+
if (!value) return false;
|
|
85
|
+
const v = value.trim();
|
|
86
|
+
const words = v.split(/\s+/).filter(Boolean);
|
|
87
|
+
// Multi-word natural language (validation messages, UI copy, sentences).
|
|
88
|
+
if (words.length >= 3) return true;
|
|
89
|
+
// A 2-word sentence fragment ending in terminal punctuation — but NOT a
|
|
90
|
+
// single token like "SuperSecretPassword!" (strong passwords end in !/? too,
|
|
91
|
+
// so terminal punctuation ALONE must never reclassify a one-word value).
|
|
92
|
+
if (words.length >= 2 && /[.!?]$/.test(v)) return true;
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Pull the first quoted literal out of a matched secret expression. */
|
|
97
|
+
function quotedValue(matchStr) {
|
|
98
|
+
const m = matchStr.match(/['"]([^'"]*)['"]/);
|
|
99
|
+
return m ? m[1] : '';
|
|
100
|
+
}
|
|
59
101
|
|
|
102
|
+
export function validateSecurity(projectDir, config) {
|
|
103
|
+
/** @type {import('../findings.mjs').Finding[]} */
|
|
60
104
|
const findings = [];
|
|
105
|
+
let passed = 0;
|
|
106
|
+
let total = 0;
|
|
61
107
|
let scanned = 0;
|
|
108
|
+
let realSecretCount = 0;
|
|
62
109
|
|
|
63
110
|
walkDir(projectDir, (filePath) => {
|
|
64
111
|
const ext = extname(filePath);
|
|
@@ -89,25 +136,58 @@ export function validateSecurity(projectDir, config) {
|
|
|
89
136
|
// Lazily initialize lines only when a match is found
|
|
90
137
|
if (!lines) lines = content.split('\n');
|
|
91
138
|
|
|
92
|
-
//
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
for (const line of lines) {
|
|
97
|
-
charCount += line.length + 1; // +1 for newline
|
|
98
|
-
if (charCount > matchPos) {
|
|
99
|
-
matchLine = line;
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
139
|
+
// 1-based line number + the line above (for inline-pragma suppression).
|
|
140
|
+
const lineNo = content.slice(0, match.index).split('\n').length;
|
|
141
|
+
const matchLine = lines[lineNo - 1] || '';
|
|
142
|
+
const prevLine = lines[lineNo - 2] || '';
|
|
103
143
|
|
|
104
144
|
// Skip known-safe placeholder/example values, but keep scanning for a
|
|
105
145
|
// real one further down the file.
|
|
106
146
|
if (isSafePlaceholder(matchLine, match[0])) continue;
|
|
107
147
|
|
|
108
|
-
|
|
148
|
+
const code = LABEL_TO_CODE[label];
|
|
149
|
+
|
|
150
|
+
// v0.27 (#8): honour an inline `// docguard:ignore SEC00x` pragma on the
|
|
151
|
+
// line or the line above — per-line suppression instead of blinding the
|
|
152
|
+
// whole file via `securityIgnore`.
|
|
153
|
+
if (code && lineSuppresses(code, matchLine, prevLine)) break;
|
|
154
|
+
|
|
155
|
+
const location = `${relPath}:${lineNo}`;
|
|
156
|
+
const value = quotedValue(match[0]);
|
|
157
|
+
const isProse = looksLikeProse(value);
|
|
158
|
+
|
|
159
|
+
if (isProse) {
|
|
160
|
+
// v0.27 (#1): natural-language value → low-confidence warning, not a
|
|
161
|
+
// blocking error. Still surfaced (no false-green), still suppressible,
|
|
162
|
+
// and now reportable via `docguard feedback`.
|
|
163
|
+
findings.push(mkFinding({
|
|
164
|
+
code, validator: 'security', severity: 'warn', confidence: 'low',
|
|
165
|
+
message: `${location}: possible ${label} — but the value reads like natural-language text (likely UI copy / a validation message, not a credential)`,
|
|
166
|
+
location,
|
|
167
|
+
suggestion: {
|
|
168
|
+
kind: 'suppress',
|
|
169
|
+
text: 'If this is UI copy or a message and not a real secret, suppress it inline.',
|
|
170
|
+
pragma: `// docguard:ignore ${code} — UI copy, not a credential`,
|
|
171
|
+
},
|
|
172
|
+
reportable: true,
|
|
173
|
+
redactedContext: `${label} pattern fired on a value that is natural-language text (~${value.trim().split(/\s+/).filter(Boolean).length} words). Literal omitted.`,
|
|
174
|
+
}));
|
|
175
|
+
} else {
|
|
176
|
+
realSecretCount++;
|
|
177
|
+
findings.push(mkFinding({
|
|
178
|
+
code, validator: 'security', severity: 'error', confidence: 'high',
|
|
179
|
+
message: `${location}: possible ${label} found`,
|
|
180
|
+
location,
|
|
181
|
+
suggestion: {
|
|
182
|
+
kind: 'fix',
|
|
183
|
+
text: 'Move the secret to an environment variable and read it via process.env / the platform secret store. Never commit credentials.',
|
|
184
|
+
command: code ? `docguard explain ${code}` : undefined,
|
|
185
|
+
pragma: code ? `// docguard:ignore ${code} — reason (only if a confirmed false positive)` : undefined,
|
|
186
|
+
},
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
109
189
|
// One finding per (file, label) is enough — the reported message is
|
|
110
|
-
// identical for repeats and we've already proven a
|
|
190
|
+
// identical for repeats and we've already proven a match exists.
|
|
111
191
|
break;
|
|
112
192
|
}
|
|
113
193
|
}
|
|
@@ -116,35 +196,41 @@ export function validateSecurity(projectDir, config) {
|
|
|
116
196
|
// Only count the secret scan as a passed check if we actually scanned files.
|
|
117
197
|
// An empty scan that reports "no secrets" is a dangerous false ✅ — surface it.
|
|
118
198
|
if (scanned > 0) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
} else {
|
|
123
|
-
for (const f of findings) {
|
|
124
|
-
results.errors.push(`${f.file}: possible ${f.label} found`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
199
|
+
total++;
|
|
200
|
+
// Low-confidence (prose) findings do not fail the check — only real secrets do.
|
|
201
|
+
if (realSecretCount === 0) passed++;
|
|
127
202
|
} else {
|
|
128
|
-
|
|
129
|
-
'
|
|
130
|
-
|
|
203
|
+
findings.push(mkFinding({
|
|
204
|
+
code: 'SEC011', validator: 'security', severity: 'warn', confidence: 'high',
|
|
205
|
+
message: 'No source files were scanned for secrets — check config.sourceRoot / ignore patterns',
|
|
206
|
+
suggestion: { kind: 'review', text: 'Verify config.sourceRoot and ignore patterns actually include your source tree.' },
|
|
207
|
+
}));
|
|
131
208
|
}
|
|
132
209
|
|
|
133
210
|
// Check .gitignore includes .env
|
|
134
|
-
|
|
211
|
+
total++;
|
|
135
212
|
const gitignorePath = resolve(projectDir, '.gitignore');
|
|
136
213
|
if (existsSync(gitignorePath)) {
|
|
137
214
|
const gitignore = readFileSync(gitignorePath, 'utf-8');
|
|
138
215
|
if (gitignore.includes('.env') || gitignore.includes('.env.local')) {
|
|
139
|
-
|
|
216
|
+
passed++;
|
|
140
217
|
} else {
|
|
141
|
-
|
|
218
|
+
findings.push(mkFinding({
|
|
219
|
+
code: 'SEC010', validator: 'security', severity: 'warn', confidence: 'high',
|
|
220
|
+
message: '.gitignore does not include .env — secrets may be committed',
|
|
221
|
+
location: '.gitignore',
|
|
222
|
+
suggestion: { kind: 'fix', text: 'Add `.env` and `.env.local` to .gitignore.' },
|
|
223
|
+
}));
|
|
142
224
|
}
|
|
143
225
|
} else {
|
|
144
|
-
|
|
226
|
+
findings.push(mkFinding({
|
|
227
|
+
code: 'SEC010', validator: 'security', severity: 'warn', confidence: 'high',
|
|
228
|
+
message: 'No .gitignore found — secrets may be committed',
|
|
229
|
+
suggestion: { kind: 'fix', text: 'Create a .gitignore that excludes `.env` and `.env.local`.' },
|
|
230
|
+
}));
|
|
145
231
|
}
|
|
146
232
|
|
|
147
|
-
return
|
|
233
|
+
return resultFromFindings(findings, { passed, total });
|
|
148
234
|
}
|
|
149
235
|
|
|
150
236
|
function walkDir(dir, callback) {
|
|
@@ -262,6 +262,10 @@ function loadTrackingDocs(projectDir, config) {
|
|
|
262
262
|
const trackingFiles = [
|
|
263
263
|
'ROADMAP.md', 'CURRENT-STATE.md', 'TODO.md', 'BACKLOG.md',
|
|
264
264
|
'docs-canonical/ARCHITECTURE.md', 'CHANGELOG.md',
|
|
265
|
+
// v0.27 (field report #6): many projects keep the roadmap/backlog under
|
|
266
|
+
// docs-canonical/ — a TODO tracked there was wrongly read as "untracked".
|
|
267
|
+
'docs-canonical/ROADMAP.md', 'docs-canonical/CURRENT-STATE.md',
|
|
268
|
+
'docs-canonical/BACKLOG.md', 'docs-canonical/TODO.md',
|
|
265
269
|
...(config.todoTracking?.trackingFiles || []),
|
|
266
270
|
];
|
|
267
271
|
|
|
@@ -47,15 +47,33 @@ const esc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
47
47
|
function applyReplaceCount(projectDir, fix) {
|
|
48
48
|
const full = resolve(projectDir, fix.file);
|
|
49
49
|
if (!existsSync(full)) return { applied: false };
|
|
50
|
+
// Bug #2 (fail-closed): NEVER overwrite a number without provenance proving
|
|
51
|
+
// the "actual" describes the SAME subject. The Metrics-Consistency validator
|
|
52
|
+
// stamps `actualSource` (e.g. "docguard.guard.checks") only for claims it
|
|
53
|
+
// verified are bound to DocGuard. A fix lacking it is refused rather than risk
|
|
54
|
+
// corrupting a correct, unrelated number.
|
|
55
|
+
if (!fix.actualSource) {
|
|
56
|
+
return { applied: false, detail: `${fix.file}: skipped "${fix.found} ${fix.label}" → "${fix.actual}" — no provenance (actualSource) to prove same subject` };
|
|
57
|
+
}
|
|
50
58
|
const content = readFileSync(full, 'utf-8');
|
|
51
59
|
// v0.15.2 hotfix: case-insensitive label match. Mirrors the validator's
|
|
52
60
|
// detection regex (which is `gi`). Without `i` here, the applier would
|
|
53
61
|
// skip "21 Validators" (capitalized) even though Metrics-Consistency
|
|
54
62
|
// detected it — leaving the user with a warning they couldn't auto-fix.
|
|
55
|
-
// The /docguard.diagnose run on canonical-spec-kit surfaced this.
|
|
56
63
|
const re = new RegExp(`\\b${esc(fix.found)}(\\s+(?:automated\\s+)?${esc(fix.label)}\\b)`, 'gi');
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
// Only rewrite occurrences on a DocGuard-bound line (same predicate as the
|
|
65
|
+
// validator's subject-binding) so a stray "<found> <label>" elsewhere in the
|
|
66
|
+
// file is never collateral-damaged by the global replace.
|
|
67
|
+
let changed = false;
|
|
68
|
+
const next = content.replace(re, (m, tail, offset, str) => {
|
|
69
|
+
const lineStart = str.lastIndexOf('\n', offset) + 1;
|
|
70
|
+
let lineEnd = str.indexOf('\n', offset);
|
|
71
|
+
if (lineEnd === -1) lineEnd = str.length;
|
|
72
|
+
if (!/docguard/i.test(str.slice(lineStart, lineEnd))) return m; // not bound → leave untouched
|
|
73
|
+
changed = true;
|
|
74
|
+
return `${fix.actual}${tail}`;
|
|
75
|
+
});
|
|
76
|
+
if (!changed || next === content) return { applied: false };
|
|
59
77
|
writeFileSync(full, next, 'utf-8');
|
|
60
78
|
return { applied: true, detail: `${fix.file}: "${fix.found} ${fix.label}" → "${fix.actual} ${fix.label}"` };
|
|
61
79
|
}
|
|
@@ -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.27.0"
|
|
7
7
|
description: "Canonical-Driven Development enforcement as a true spec-kit extension. LLM-first design with automated validators, 4 AI behavior skills, spec-kit skill chaining, and workflow hooks. One pinned runtime dependency (@babel/parser); pure Node.js otherwise."
|
|
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.27.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-fix
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.27.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.
|
|
10
|
+
version: 0.27.0
|
|
11
11
|
source: extensions/spec-kit-docguard/skills/docguard-guard
|
|
12
12
|
---
|
|
13
|
-
<!-- docguard:version: 0.
|
|
13
|
+
<!-- docguard:version: 0.27.0 -->
|
|
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.27.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-review
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.27.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.
|
|
9
|
+
version: 0.27.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-score
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.27.0 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Score Skill
|
|
15
15
|
|
|
@@ -4,10 +4,10 @@ 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.27.0
|
|
8
8
|
source: extensions/spec-kit-docguard/skills/docguard-sync
|
|
9
9
|
---
|
|
10
|
-
<!-- docguard:version: 0.
|
|
10
|
+
<!-- docguard:version: 0.27.0 -->
|
|
11
11
|
|
|
12
12
|
# DocGuard Sync Skill
|
|
13
13
|
|
package/package.json
CHANGED