docguard-cli 0.26.0 → 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 +4 -3
- package/cli/commands/explain.mjs +23 -1
- package/cli/commands/feedback.mjs +163 -0
- package/cli/commands/guard.mjs +77 -15
- package/cli/commands/score.mjs +65 -32
- package/cli/docguard.mjs +18 -1
- package/cli/findings.mjs +194 -0
- package/cli/shared-source.mjs +24 -2
- package/cli/validators/doc-quality.mjs +14 -3
- package/cli/validators/security.mjs +117 -31
- package/cli/validators/todo-tracking.mjs +4 -0
- 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
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ DocGuard is an official [GitHub Spec Kit](https://github.com/github/spec-kit) co
|
|
|
63
63
|
|
|
64
64
|
```mermaid
|
|
65
65
|
graph TD
|
|
66
|
-
CLI["CLI Entry<br/>docguard.mjs"] --> Commands["Commands (
|
|
66
|
+
CLI["CLI Entry<br/>docguard.mjs"] --> Commands["Commands (16)"]
|
|
67
67
|
Commands --> guard["guard"]
|
|
68
68
|
Commands --> generate["generate"]
|
|
69
69
|
Commands --> score["score"]
|
|
@@ -250,7 +250,7 @@ This installs DocGuard's slash commands (`/docguard.init`, `/docguard.guard`, `/
|
|
|
250
250
|
|
|
251
251
|
## Usage
|
|
252
252
|
|
|
253
|
-
DocGuard ships **
|
|
253
|
+
DocGuard ships **16 commands** (the "Daily 5" + 11 situational tools, including the zero-install `demo`). Six additional one-shot scaffolders are accessed via `docguard init --with <name>`. Eight v0.19 commands continue to work as deprecation aliases through v0.20.x — see [MIGRATION-v0.20.md](docs-implementation/MIGRATION-v0.20.md).
|
|
254
254
|
|
|
255
255
|
**The Daily 5** — what you'll reach for 95% of the time:
|
|
256
256
|
|
|
@@ -273,7 +273,8 @@ DocGuard ships **14 commands** (the "Daily 5" + 9 situational tools, including t
|
|
|
273
273
|
| `fix --history` | Audit log of every mechanical fix applied (from `.docguard/fixed.json`) |
|
|
274
274
|
| `generate` | Reverse-engineer docs from existing codebase (`--plan` for AI scan) |
|
|
275
275
|
| `agent` | One-shot agent task graph — ordered, pre-filled code-truth, per-task verify (`--format json`) |
|
|
276
|
-
| `explain <warning>` | Paste any warning — get the validator's docstring
|
|
276
|
+
| `explain <warning\|CODE>` | Paste any warning — or a finding code like `SEC001` — to get the validator's docstring, fix path, and how to suppress |
|
|
277
|
+
| `feedback` | Report likely false positives back to DocGuard — local-first record + a 1-click prefilled, redacted GitHub issue (zero typing) |
|
|
277
278
|
| `memory` | Per-domain accuracy headline (endpoints / entities / env / tech) |
|
|
278
279
|
| `memory --diff` | Drill into which specific claims don't match code |
|
|
279
280
|
| `score --diff` | Drill into which checks pulled each category down |
|
package/cli/commands/explain.mjs
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { c } from '../shared.mjs';
|
|
20
|
+
import { CODES } from '../findings.mjs';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Validator-key → human-readable explainer. Keyed by the same key DocGuard
|
|
@@ -292,7 +293,7 @@ const EXPLAINERS = {
|
|
|
292
293
|
why: 'Vague, passive, negation-heavy docs are hard for both humans and AI agents to act on. Metrics inspired by IEEE 830 / ISO 29148.',
|
|
293
294
|
triggers: [
|
|
294
295
|
['High negation load', 'Rephrase in positive terms ("must not fail" → "must succeed"). If the negation is intentional (security/operational docs legitimately use "never"/"must not"), add the per-doc override: `<!-- docguard:quality negation-load off — your reason -->`, or set a custom bar with `<!-- docguard:quality negation-load 0.35 — reason -->`. Project-wide default: `docQuality.negationLoadThreshold` in .docguard.json.'],
|
|
295
|
-
['High passive voice ratio', 'Use active voice: "the config is read by the loader" → "the loader reads the config".'],
|
|
296
|
+
['High passive voice ratio', 'Use active voice: "the config is read by the loader" → "the loader reads the config". If the doc is legitimately passive (a sequence/flow doc), add the per-doc override: `<!-- docguard:quality passive-voice off — your reason -->`, or set a custom bar with `<!-- docguard:quality passive-voice 0.4 — reason -->`. Project-wide default: `docQuality.passiveVoiceThreshold` in .docguard.json.'],
|
|
296
297
|
['High ambiguous pronoun ratio', 'Replace "it/this/that/they" with the specific noun.'],
|
|
297
298
|
['Low atomicity', 'Split compound sentences so each states one verifiable fact (IEEE 830 §4.1).'],
|
|
298
299
|
['Reading level too high', 'Aim for grade 12–16 for technical docs — shorter sentences, simpler words.'],
|
|
@@ -419,6 +420,27 @@ export function runExplain(projectDir, _config, flags) {
|
|
|
419
420
|
return;
|
|
420
421
|
}
|
|
421
422
|
|
|
423
|
+
// v0.27: finding-code lookup — `docguard explain SEC001`. Codes are the stable,
|
|
424
|
+
// LLM-addressable handles that guard prints next to each finding and that
|
|
425
|
+
// inline `// docguard:ignore <CODE>` keys off.
|
|
426
|
+
const codeKey = query.toUpperCase();
|
|
427
|
+
if (CODES[codeKey]) {
|
|
428
|
+
const cd = CODES[codeKey];
|
|
429
|
+
if (isJson) {
|
|
430
|
+
console.log(JSON.stringify({ query, code: codeKey, ...cd }, null, 2));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
console.log(`${c.bold}🧭 ${codeKey} — ${cd.title}${c.reset}`);
|
|
434
|
+
console.log(`${c.dim} validator: ${cd.validator}${c.reset}\n`);
|
|
435
|
+
console.log(`${c.bold}What it means:${c.reset}\n ${cd.help}\n`);
|
|
436
|
+
if (cd.suppress) {
|
|
437
|
+
console.log(`${c.bold}Suppress inline${c.reset} ${c.dim}(only if it's a confirmed false positive):${c.reset}`);
|
|
438
|
+
console.log(` ${c.cyan}${cd.suppress}${c.reset}\n`);
|
|
439
|
+
}
|
|
440
|
+
console.log(`${c.bold}Got it wrong?${c.reset} ${c.dim}Send a redacted report so a future release stops flagging it: ${c.cyan}docguard feedback${c.reset}`);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
422
444
|
const match = matchWarning(query);
|
|
423
445
|
if (!match) {
|
|
424
446
|
if (isJson) {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback Command — close the loop with the tool's maintainers (v0.27).
|
|
3
|
+
*
|
|
4
|
+
* DocGuard is a tool for LLMs: when it gets something wrong, the cheapest way to
|
|
5
|
+
* make the NEXT release better is to capture that signal. This command turns the
|
|
6
|
+
* low-confidence findings of a guard run (candidate false positives — and any
|
|
7
|
+
* other finding DocGuard itself flagged as uncertain) into:
|
|
8
|
+
*
|
|
9
|
+
* 1. a LOCAL-FIRST record under .docguard/feedback/<code>-<id>.json (full,
|
|
10
|
+
* reviewable, never sent anywhere automatically), and
|
|
11
|
+
* 2. a one-click, PREFILLED GitHub issue URL that needs zero typing.
|
|
12
|
+
*
|
|
13
|
+
* Hard constraints (learned the hard way — see commit 3b600fd, where an
|
|
14
|
+
* oversized prefilled URL overflowed GitHub's ~8 KB limit and silently failed):
|
|
15
|
+
* - The URL is CAPPED well under the limit; bulk lives in the local file.
|
|
16
|
+
* - It is REDACTED: no source code, no secret values — only a basename, a line
|
|
17
|
+
* number, and the safe `redactedContext` the validator built.
|
|
18
|
+
* - It is OPT-IN: nothing is filed automatically; the human clicks (or not).
|
|
19
|
+
*
|
|
20
|
+
* Not read-only in the strict sense — it writes its own .docguard/feedback/ —
|
|
21
|
+
* but it never scaffolds skills and never touches the user's source tree.
|
|
22
|
+
*
|
|
23
|
+
* Zero npm dependencies — pure Node.js built-ins.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
27
|
+
import { resolve, dirname, basename } from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { c } from '../shared.mjs';
|
|
30
|
+
import { runGuardInternal } from './guard.mjs';
|
|
31
|
+
|
|
32
|
+
const _PKG = JSON.parse(
|
|
33
|
+
readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json'), 'utf-8')
|
|
34
|
+
);
|
|
35
|
+
const CLI_VERSION = _PKG.version;
|
|
36
|
+
const ISSUES_BASE = (_PKG.bugs && _PKG.bugs.url) || 'https://github.com/raccioly/docguard/issues';
|
|
37
|
+
|
|
38
|
+
// Keep the prefilled URL comfortably under GitHub's ~8 KB request-URL limit.
|
|
39
|
+
const URL_CAP = 1800;
|
|
40
|
+
|
|
41
|
+
/** Deterministic short id from a string — avoids Date.now()/Math.random(). */
|
|
42
|
+
function shortId(str) {
|
|
43
|
+
let h = 5381;
|
|
44
|
+
for (let i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) >>> 0;
|
|
45
|
+
return h.toString(36).slice(0, 6);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Redact a finding location for the SHARED url: basename + line only. */
|
|
49
|
+
function safeLocation(location) {
|
|
50
|
+
if (!location) return '(unknown)';
|
|
51
|
+
const [path, line] = String(location).split(/:(?=\d+$)/);
|
|
52
|
+
return line ? `${basename(path)}:${line}` : basename(path);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build a prefilled, capped issue URL. Drops optional body lines (longest-value
|
|
57
|
+
* first) until under the cap; title + code + location always survive.
|
|
58
|
+
*/
|
|
59
|
+
function buildIssueUrl(finding) {
|
|
60
|
+
const code = finding.code || 'FINDING';
|
|
61
|
+
const validator = finding.validator || 'unknown';
|
|
62
|
+
const shortMsg = (finding.message || '').replace(/\s+/g, ' ').slice(0, 70);
|
|
63
|
+
const title = `[feedback] ${code} (${validator}): ${shortMsg}`;
|
|
64
|
+
|
|
65
|
+
// Optional lines are ordered most→least droppable.
|
|
66
|
+
const required = [
|
|
67
|
+
`DocGuard v${CLI_VERSION} flagged this and it may be a false positive (or other feedback).`,
|
|
68
|
+
'',
|
|
69
|
+
`- Code: ${code}`,
|
|
70
|
+
`- Validator: ${validator}`,
|
|
71
|
+
`- Location: ${safeLocation(finding.location)}`,
|
|
72
|
+
`- Confidence: ${finding.confidence}`,
|
|
73
|
+
];
|
|
74
|
+
const optional = [];
|
|
75
|
+
if (finding.redactedContext) optional.push(`- Context: ${finding.redactedContext}`);
|
|
76
|
+
if (finding.suggestion && finding.suggestion.text) optional.push(`- Suggestion shown: ${finding.suggestion.text}`);
|
|
77
|
+
const footer = ['', 'Generated by `docguard feedback` — no source code or secret values are included.'];
|
|
78
|
+
|
|
79
|
+
const compose = (opt) => `${ISSUES_BASE}/new?labels=${encodeURIComponent('docguard-feedback')}` +
|
|
80
|
+
`&title=${encodeURIComponent(title)}` +
|
|
81
|
+
`&body=${encodeURIComponent([...required, ...opt, ...footer].join('\n'))}`;
|
|
82
|
+
|
|
83
|
+
let opt = [...optional];
|
|
84
|
+
let url = compose(opt);
|
|
85
|
+
while (url.length > URL_CAP && opt.length > 0) {
|
|
86
|
+
opt = opt.slice(0, -1);
|
|
87
|
+
url = compose(opt);
|
|
88
|
+
}
|
|
89
|
+
if (url.length > URL_CAP) {
|
|
90
|
+
// Even the required body is too long (pathological) — collapse to a stub.
|
|
91
|
+
url = `${ISSUES_BASE}/new?labels=${encodeURIComponent('docguard-feedback')}` +
|
|
92
|
+
`&title=${encodeURIComponent(title)}` +
|
|
93
|
+
`&body=${encodeURIComponent(`DocGuard v${CLI_VERSION} — ${code} (${validator}). Full details saved locally; please attach.`)}`;
|
|
94
|
+
}
|
|
95
|
+
return { url, title };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function runFeedback(projectDir, config, flags) {
|
|
99
|
+
const data = runGuardInternal(projectDir, config);
|
|
100
|
+
const reportable = (data.findings || []).filter((f) => f.reportable);
|
|
101
|
+
const isJson = flags.format === 'json';
|
|
102
|
+
|
|
103
|
+
if (reportable.length === 0) {
|
|
104
|
+
if (isJson) {
|
|
105
|
+
console.log(JSON.stringify({ reportable: [], message: 'no uncertain findings' }, null, 2));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
console.log(`${c.bold}📮 DocGuard Feedback${c.reset}`);
|
|
109
|
+
console.log(`${c.green}✅ Nothing to report — DocGuard is confident about everything it flagged.${c.reset}`);
|
|
110
|
+
console.log(`${c.dim} (Feedback collects low-confidence findings, i.e. likely false positives.)${c.reset}\n`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Capture each reportable finding locally (full, reviewable) + build its URL.
|
|
115
|
+
const feedbackDir = resolve(projectDir, '.docguard', 'feedback');
|
|
116
|
+
const items = reportable.map((f) => {
|
|
117
|
+
const id = shortId(`${f.code}|${f.location || f.message}`);
|
|
118
|
+
const { url, title } = buildIssueUrl(f);
|
|
119
|
+
const fileName = `${(f.code || 'finding').toLowerCase()}-${id}.json`;
|
|
120
|
+
const filePath = resolve(feedbackDir, fileName);
|
|
121
|
+
return { finding: f, id, url, title, fileName, filePath };
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (isJson) {
|
|
125
|
+
console.log(JSON.stringify({
|
|
126
|
+
version: CLI_VERSION,
|
|
127
|
+
reportable: items.map((it) => ({ code: it.finding.code, location: it.finding.location, url: it.url, file: `.docguard/feedback/${it.fileName}` })),
|
|
128
|
+
}, null, 2));
|
|
129
|
+
// Still write the local records so the JSON path is not a dead end.
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let wrote = 0;
|
|
133
|
+
for (const it of items) {
|
|
134
|
+
try {
|
|
135
|
+
if (!existsSync(feedbackDir)) mkdirSync(feedbackDir, { recursive: true });
|
|
136
|
+
writeFileSync(it.filePath, JSON.stringify({
|
|
137
|
+
capturedBy: `docguard feedback (v${CLI_VERSION})`,
|
|
138
|
+
finding: it.finding,
|
|
139
|
+
issueUrl: it.url,
|
|
140
|
+
}, null, 2) + '\n', 'utf-8');
|
|
141
|
+
wrote++;
|
|
142
|
+
} catch { /* best-effort local capture */ }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (isJson) return;
|
|
146
|
+
|
|
147
|
+
console.log(`${c.bold}📮 DocGuard Feedback${c.reset}`);
|
|
148
|
+
console.log(`${c.dim} ${items.length} uncertain finding(s) — likely false positives. Saved locally to ${c.cyan}.docguard/feedback/${c.reset}\n`);
|
|
149
|
+
|
|
150
|
+
for (const it of items) {
|
|
151
|
+
const f = it.finding;
|
|
152
|
+
console.log(` ${c.yellow}[${f.code}]${c.reset} ${f.message}`);
|
|
153
|
+
if (f.suggestion && f.suggestion.pragma) {
|
|
154
|
+
console.log(` ${c.dim}Suppress locally instead: ${f.suggestion.pragma}${c.reset}`);
|
|
155
|
+
}
|
|
156
|
+
console.log(` ${c.dim}Report (1 click, prefilled, redacted):${c.reset}`);
|
|
157
|
+
console.log(` ${c.cyan}${it.url}${c.reset}`);
|
|
158
|
+
console.log(` ${c.dim}Local copy: .docguard/feedback/${it.fileName}${c.reset}\n`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(`${c.dim}These reports help DocGuard stop flagging the same false positive in a future release.${c.reset}`);
|
|
162
|
+
console.log(`${c.dim}Nothing is sent automatically — open a link only if you want to.${c.reset}\n`);
|
|
163
|
+
}
|
package/cli/commands/guard.mjs
CHANGED
|
@@ -180,6 +180,27 @@ export function classifyResult(result) {
|
|
|
180
180
|
return { status, quality };
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* v0.27: the list of issues to render for a validator. Prefers structured
|
|
185
|
+
* findings (code + confidence + suggestion) when present; otherwise maps the
|
|
186
|
+
* legacy error/warning strings into the same shape so the renderer is uniform.
|
|
187
|
+
*/
|
|
188
|
+
function renderableItems(v) {
|
|
189
|
+
if (Array.isArray(v.findings) && v.findings.length > 0) {
|
|
190
|
+
return v.findings.map((f) => ({
|
|
191
|
+
severity: f.severity,
|
|
192
|
+
message: f.message,
|
|
193
|
+
code: f.code,
|
|
194
|
+
confidence: f.confidence,
|
|
195
|
+
suggestion: f.suggestion,
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
return [
|
|
199
|
+
...(v.errors || []).map((m) => ({ severity: 'error', message: m })),
|
|
200
|
+
...(v.warnings || []).map((m) => ({ severity: 'warn', message: m })),
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
|
|
183
204
|
export function runGuardInternal(projectDir, config) {
|
|
184
205
|
const validators = config.validators || {};
|
|
185
206
|
const results = [];
|
|
@@ -323,6 +344,15 @@ export function runGuardInternal(projectDir, config) {
|
|
|
323
344
|
// what the user reads is what CI does.
|
|
324
345
|
const overallStatus = effectiveErrors > 0 ? 'FAIL' : effectiveWarnings > 0 ? 'WARN' : 'PASS';
|
|
325
346
|
|
|
347
|
+
// v0.27: stable, LLM-addressable contract. `findings` is the flattened,
|
|
348
|
+
// structured view (those validators that emit it); `reportable` are the
|
|
349
|
+
// low-confidence ones the feedback loop offers to report; `nextStep` is the
|
|
350
|
+
// single machine hint so an agent in a hook never has to parse prose.
|
|
351
|
+
const allFindings = activeResults.flatMap((r) => (Array.isArray(r.findings) ? r.findings : []));
|
|
352
|
+
const reportable = allFindings.filter((f) => f.reportable);
|
|
353
|
+
const nextStep =
|
|
354
|
+
overallStatus === 'PASS' ? null : 'docguard diagnose';
|
|
355
|
+
|
|
326
356
|
return {
|
|
327
357
|
project: config.projectName,
|
|
328
358
|
profile: config.profile || 'standard',
|
|
@@ -331,6 +361,9 @@ export function runGuardInternal(projectDir, config) {
|
|
|
331
361
|
total: totalChecks,
|
|
332
362
|
errors: totalErrors,
|
|
333
363
|
warnings: totalWarnings,
|
|
364
|
+
findings: allFindings,
|
|
365
|
+
reportable,
|
|
366
|
+
nextStep,
|
|
334
367
|
// v0.5: severity-aware counts for exit-code logic. The display still uses
|
|
335
368
|
// the raw counts above so users see every warning, but CI only fails on
|
|
336
369
|
// things they've marked as high-severity.
|
|
@@ -473,14 +506,26 @@ export function runGuard(projectDir, config, flags) {
|
|
|
473
506
|
// overall validator status — useful when a validator passes overall
|
|
474
507
|
// (passed < total) without surfacing the specific failing checks.
|
|
475
508
|
const show = flags.verbose || flags.showFailing;
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
509
|
+
const showErr = show || v.status === 'fail';
|
|
510
|
+
const showWarn = show || v.status === 'warn';
|
|
511
|
+
// v0.27: render from structured findings when the validator emits them
|
|
512
|
+
// (each issue carries a code, confidence, and a `→ suggestion`); otherwise
|
|
513
|
+
// fall back to the legacy error/warning strings. Identical gating.
|
|
514
|
+
for (const item of renderableItems(v)) {
|
|
515
|
+
if (item.severity === 'error' && !showErr) continue;
|
|
516
|
+
if (item.severity === 'warn' && !showWarn) continue;
|
|
517
|
+
const mark = item.severity === 'error' ? `${c.red}✗` : `${c.yellow}⚠`;
|
|
518
|
+
const codeTag = item.code ? `${c.dim}[${item.code}]${c.reset} ` : '';
|
|
519
|
+
const conf = item.confidence === 'low'
|
|
520
|
+
? ` ${c.dim}(low confidence — possible false positive)${c.reset}` : '';
|
|
521
|
+
console.log(` ${mark} ${codeTag}${item.message}${c.reset}${conf}`);
|
|
522
|
+
if (item.suggestion) {
|
|
523
|
+
console.log(` ${c.cyan}→${c.reset} ${c.dim}${item.suggestion.text}${c.reset}`);
|
|
524
|
+
if (item.suggestion.command) {
|
|
525
|
+
console.log(` ${c.cyan}${item.suggestion.command}${c.reset}`);
|
|
526
|
+
} else if (item.suggestion.pragma) {
|
|
527
|
+
console.log(` ${c.dim}${item.suggestion.pragma}${c.reset}`);
|
|
528
|
+
}
|
|
484
529
|
}
|
|
485
530
|
}
|
|
486
531
|
// If a validator reports passed < total but has no errors/warnings, surface
|
|
@@ -514,14 +559,31 @@ export function runGuard(projectDir, config, flags) {
|
|
|
514
559
|
console.log(` ${c.red}${c.bold}❌ FAIL${c.reset} ${c.red}— ${data.passed}/${data.total} passed, ${data.effectiveErrors} blocking issue(s)${warnSuffix}${c.reset}`);
|
|
515
560
|
}
|
|
516
561
|
|
|
517
|
-
// Next
|
|
562
|
+
// ── Next steps — every run ends with a suggested action (v0.27) ──
|
|
563
|
+
// The field-report principle: whenever DocGuard calls out an issue it must
|
|
564
|
+
// suggest what to do next; on a clean run it points at the next workflow step
|
|
565
|
+
// rather than nagging. JSON consumers read this off the `nextStep`/`reportable`
|
|
566
|
+
// contract fields instead of this prose.
|
|
567
|
+
const agentMode = detectAgentMode(projectDir);
|
|
568
|
+
const skill = (name) => (agentMode === 'llm' ? `/docguard.${name}` : `docguard ${name}`);
|
|
569
|
+
|
|
518
570
|
if (data.status !== 'PASS') {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
571
|
+
console.log(` ${c.dim}Next: run ${c.cyan}${skill('diagnose')}${c.dim} to get AI fix prompts that resolve the issues above.${c.reset}`);
|
|
572
|
+
} else {
|
|
573
|
+
console.log(` ${c.dim}Next: ${c.cyan}${skill('score')}${c.dim} for your CDD maturity score, or commit with confidence.${c.reset}`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Low-confidence findings (possible false positives) → offer the local-first
|
|
577
|
+
// feedback path. Broader than secrets: anything DocGuard flagged uncertainly.
|
|
578
|
+
if (Array.isArray(data.reportable) && data.reportable.length > 0) {
|
|
579
|
+
const n = data.reportable.length;
|
|
580
|
+
console.log(` ${c.dim}↪ ${n} finding(s) look uncertain (possible false positives). Review or report: ${c.cyan}${skill('feedback')}${c.reset}`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Read-only skills nudge (never writes — that's `init`'s job). If the agent
|
|
584
|
+
// has no /docguard.* commands installed yet, say how to get them.
|
|
585
|
+
if (agentMode === 'llm' && !existsSync(resolvePath(projectDir, '.agent', 'skills', 'docguard-guard'))) {
|
|
586
|
+
console.log(` ${c.dim}💡 Install ${c.cyan}/docguard.*${c.dim} commands for your agent: ${c.cyan}docguard init${c.reset}`);
|
|
525
587
|
}
|
|
526
588
|
|
|
527
589
|
// Badge snippet
|
package/cli/commands/score.mjs
CHANGED
|
@@ -10,6 +10,70 @@ import { c, docHasSection } from '../shared.mjs';
|
|
|
10
10
|
import { validateSecurity } from '../validators/security.mjs';
|
|
11
11
|
import { runGuardInternal } from './guard.mjs';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Detect whether the project configures a test runner (the "Check 3" of the
|
|
15
|
+
* testing score). Extracted as an exported seam so it's unit-testable without
|
|
16
|
+
* the full score pipeline.
|
|
17
|
+
*
|
|
18
|
+
* Recognises, in order: standalone config files; pytest config inside
|
|
19
|
+
* pyproject.toml / tox.ini; node:test via projectTypeConfig or scripts.test;
|
|
20
|
+
* a `scripts.test` that invokes a known runner; Vitest configured INSIDE
|
|
21
|
+
* vite.config.* (field report #3 — `vitest/config` import or a `test:` block);
|
|
22
|
+
* and runner configs in common workspace subdirs.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} dir
|
|
25
|
+
* @param {object} config
|
|
26
|
+
* @returns {boolean}
|
|
27
|
+
*/
|
|
28
|
+
export function detectTestRunner(dir, config = {}) {
|
|
29
|
+
const testConfigFiles = ['jest.config.js', 'jest.config.ts', 'vitest.config.ts', 'vitest.config.js', 'pytest.ini', 'setup.cfg', '.mocharc.yml'];
|
|
30
|
+
if (testConfigFiles.some((f) => existsSync(resolve(dir, f)))) return true;
|
|
31
|
+
|
|
32
|
+
// Python: pytest config usually lives inside pyproject.toml ([tool.pytest.ini_options])
|
|
33
|
+
// or tox.ini ([pytest]) — not a standalone file.
|
|
34
|
+
for (const [file, marker] of [['pyproject.toml', /\[tool\.pytest/], ['tox.ini', /\[pytest\]/]]) {
|
|
35
|
+
const p = resolve(dir, file);
|
|
36
|
+
if (!existsSync(p)) continue;
|
|
37
|
+
try { if (marker.test(readFileSync(p, 'utf-8'))) return true; } catch { /* skip */ }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// node:test has no config file — recognize it via projectTypeConfig or package.json.
|
|
41
|
+
const ptc = config.projectTypeConfig || {};
|
|
42
|
+
if (ptc.testFramework === 'node:test') return true;
|
|
43
|
+
const pkgPath = resolve(dir, 'package.json');
|
|
44
|
+
if (existsSync(pkgPath)) {
|
|
45
|
+
try {
|
|
46
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
47
|
+
const testScript = pkg.scripts?.test || '';
|
|
48
|
+
if (testScript.includes('node --test') || testScript.includes('node:test')) return true;
|
|
49
|
+
// v0.27 (field report #3): a `scripts.test` that runs a known runner IS a
|
|
50
|
+
// configured test runner, even without a standalone config file.
|
|
51
|
+
if (/\b(vitest|jest|mocha|ava|playwright|cypress|pytest)\b/.test(testScript)) return true;
|
|
52
|
+
} catch { /* skip */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// v0.27 (field report #3): Vitest configured INSIDE vite.config.* rather than a
|
|
56
|
+
// standalone vitest.config (`vitest/config` import + a `test:` block).
|
|
57
|
+
for (const f of ['vite.config.ts', 'vite.config.js', 'vite.config.mts', 'vite.config.mjs']) {
|
|
58
|
+
const p = resolve(dir, f);
|
|
59
|
+
if (!existsSync(p)) continue;
|
|
60
|
+
try {
|
|
61
|
+
const src = readFileSync(p, 'utf-8');
|
|
62
|
+
if (/vitest\/config/.test(src) || /^\s*test\s*:/m.test(src)) return true;
|
|
63
|
+
} catch { /* skip */ }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Workspace subdirs: a runner config one level down still configures the project.
|
|
67
|
+
const subConfigs = ['vitest.config.ts', 'vitest.config.js', 'jest.config.ts', 'jest.config.js', 'vite.config.ts'];
|
|
68
|
+
for (const sub of ['backend', 'frontend', 'server', 'client', 'app', 'web', 'api']) {
|
|
69
|
+
for (const f of subConfigs) {
|
|
70
|
+
if (existsSync(resolve(dir, sub, f))) return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
13
77
|
/**
|
|
14
78
|
* v0.18-P3: map score categories to the validator keys that contribute.
|
|
15
79
|
* One category can roll up multiple validators (e.g. "environment" pulls
|
|
@@ -585,38 +649,7 @@ function calcTestingScore(dir, config) {
|
|
|
585
649
|
else failures.push({ issue: 'TEST-SPEC.md missing', fixCmd: 'docguard fix --doc test-spec' });
|
|
586
650
|
|
|
587
651
|
// ── Check 3: Test config or built-in runner (15 pts) ──
|
|
588
|
-
|
|
589
|
-
let hasTestRunner = testConfigFiles.some(f => existsSync(resolve(dir, f)));
|
|
590
|
-
|
|
591
|
-
// Python: pytest config usually lives inside pyproject.toml ([tool.pytest.ini_options])
|
|
592
|
-
// or tox.ini ([pytest]) — not a standalone file. Detect those too, so a uv/pytest
|
|
593
|
-
// project isn't told to "add a test runner" it already configured (field report, Issue B).
|
|
594
|
-
if (!hasTestRunner) {
|
|
595
|
-
for (const [file, marker] of [['pyproject.toml', /\[tool\.pytest/], ['tox.ini', /\[pytest\]/]]) {
|
|
596
|
-
const p = resolve(dir, file);
|
|
597
|
-
if (!existsSync(p)) continue;
|
|
598
|
-
try { if (marker.test(readFileSync(p, 'utf-8'))) { hasTestRunner = true; break; } } catch { /* skip */ }
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// node:test has no config file — recognize it via projectTypeConfig or package.json.
|
|
603
|
-
if (!hasTestRunner) {
|
|
604
|
-
const ptc = config.projectTypeConfig || {};
|
|
605
|
-
if (ptc.testFramework === 'node:test') {
|
|
606
|
-
hasTestRunner = true;
|
|
607
|
-
} else {
|
|
608
|
-
const pkgPath = resolve(dir, 'package.json');
|
|
609
|
-
if (existsSync(pkgPath)) {
|
|
610
|
-
try {
|
|
611
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
612
|
-
const testScript = pkg.scripts?.test || '';
|
|
613
|
-
if (testScript.includes('node --test') || testScript.includes('node:test')) hasTestRunner = true;
|
|
614
|
-
} catch { /* skip */ }
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
if (hasTestRunner) score += 15;
|
|
652
|
+
if (detectTestRunner(dir, config)) score += 15;
|
|
620
653
|
else failures.push({ issue: 'no test runner config detected (jest/vitest/pytest/node:test)' });
|
|
621
654
|
|
|
622
655
|
// ── Check 4: CI test step (15 pts) ──
|
package/cli/docguard.mjs
CHANGED
|
@@ -43,6 +43,7 @@ import { runSetup } from './commands/setup.mjs';
|
|
|
43
43
|
import { runUpgrade } from './commands/upgrade.mjs';
|
|
44
44
|
import { runImpact } from './commands/impact.mjs';
|
|
45
45
|
import { runExplain } from './commands/explain.mjs';
|
|
46
|
+
import { runFeedback } from './commands/feedback.mjs';
|
|
46
47
|
import { runMemory } from './commands/memory.mjs';
|
|
47
48
|
import { runDemo } from './commands/demo.mjs';
|
|
48
49
|
import { runAgent } from './commands/agent.mjs';
|
|
@@ -85,7 +86,8 @@ ${c.bold}Tools (situational, but day-to-day useful)${c.reset}
|
|
|
85
86
|
${c.green}fix${c.reset} Generate AI fix instructions for specific docs
|
|
86
87
|
${c.green}generate${c.reset} Reverse-engineer canonical docs from existing code (${c.cyan}--plan${c.reset} for AI scan)
|
|
87
88
|
${c.green}agent${c.reset} One-shot agent task graph — ordered tasks, pre-filled code-truth, per-task verify (${c.cyan}--format json${c.reset})
|
|
88
|
-
${c.green}explain${c.reset} Explain a validator key or
|
|
89
|
+
${c.green}explain${c.reset} Explain a validator key, warning text, or finding code (${c.cyan}docguard explain SEC001${c.reset})
|
|
90
|
+
${c.green}feedback${c.reset} Report likely false positives back to DocGuard (local-first + 1-click prefilled issue)
|
|
89
91
|
${c.green}memory${c.reset} Show what DocGuard remembers (${c.cyan}--diff${c.reset} drills into drift)
|
|
90
92
|
${c.green}trace${c.reset} Requirements traceability matrix (${c.cyan}--reverse${c.reset} for code→doc map)
|
|
91
93
|
${c.green}upgrade${c.reset} Migrate ${c.cyan}.docguard.json${c.reset} schema + CLI (${c.cyan}--apply --pr${c.reset} for team-wide PR)
|
|
@@ -279,6 +281,12 @@ const COMMAND_HELP = {
|
|
|
279
281
|
flags: [['--diff', 'Drill into drift between memory and code']],
|
|
280
282
|
examples: ['docguard memory', 'docguard memory --diff'],
|
|
281
283
|
},
|
|
284
|
+
feedback: {
|
|
285
|
+
summary: 'Report likely false positives back to DocGuard. Collects the low-confidence findings of a guard run, saves a full local record under .docguard/feedback/, and prints a one-click, prefilled, redacted GitHub issue URL (zero typing, no source code or secret values).',
|
|
286
|
+
usage: 'docguard feedback [--format json]',
|
|
287
|
+
flags: [['--format json', 'Machine-readable list of reportable findings + URLs']],
|
|
288
|
+
examples: ['docguard feedback'],
|
|
289
|
+
},
|
|
282
290
|
};
|
|
283
291
|
|
|
284
292
|
function printCommandHelp(command) {
|
|
@@ -507,6 +515,9 @@ async function main() {
|
|
|
507
515
|
const READ_ONLY_COMMANDS = new Set([
|
|
508
516
|
'guard', 'audit', 'score', 'diff', 'impact',
|
|
509
517
|
'diagnose', 'trace', 'explain', 'memory', 'demo', 'agent',
|
|
518
|
+
// feedback only writes its own .docguard/feedback/ — it must NOT scaffold
|
|
519
|
+
// skills or touch source, so it's gated out of ensureSkills like the rest.
|
|
520
|
+
'feedback',
|
|
510
521
|
]);
|
|
511
522
|
|
|
512
523
|
// Silent auto-check: install skills/commands if missing. Skip entirely in
|
|
@@ -646,6 +657,12 @@ async function main() {
|
|
|
646
657
|
case 'explain':
|
|
647
658
|
runExplain(projectDir, config, flags);
|
|
648
659
|
break;
|
|
660
|
+
case 'feedback':
|
|
661
|
+
// v0.27 (field report #3 / LLM feedback loop): collect low-confidence
|
|
662
|
+
// findings (likely false positives) → local record + 1-click prefilled,
|
|
663
|
+
// redacted, capped GitHub issue URL. Opt-in; nothing filed automatically.
|
|
664
|
+
runFeedback(projectDir, config, flags);
|
|
665
|
+
break;
|
|
649
666
|
case 'memory':
|
|
650
667
|
runMemory(projectDir, config, flags);
|
|
651
668
|
break;
|
package/cli/findings.mjs
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Findings — the structured, LLM-addressable result unit (v0.27).
|
|
3
|
+
*
|
|
4
|
+
* Background (LLM field report #3): DocGuard's whole job is to tell an agent
|
|
5
|
+
* what to do NEXT. A free-text `errors`/`warnings` string can't carry a stable
|
|
6
|
+
* code (for `explain <CODE>` + inline suppression), a confidence (the signal
|
|
7
|
+
* the false-positive feedback loop runs on), or a machine-readable suggested
|
|
8
|
+
* action. A Finding carries all three.
|
|
9
|
+
*
|
|
10
|
+
* The migration is INCREMENTAL and BACKWARD-COMPATIBLE. A validator that opts in
|
|
11
|
+
* builds `Finding[]` and returns `resultFromFindings(...)`, which still emits the
|
|
12
|
+
* exact `{ errors, warnings, passed, total }` shape every existing consumer
|
|
13
|
+
* (guard counts + exit code, diagnose, score, ci, `--format json`) already reads
|
|
14
|
+
* — PLUS a `findings` array that guard renders richly (each issue gets its
|
|
15
|
+
* `→ suggestion`). Validators that haven't migrated keep returning their
|
|
16
|
+
* hand-built results and render exactly as before. Nothing regresses.
|
|
17
|
+
*
|
|
18
|
+
* Zero npm dependencies — pure Node.js built-ins.
|
|
19
|
+
*
|
|
20
|
+
* @typedef {Object} Suggestion
|
|
21
|
+
* @property {'fix'|'suppress'|'review'|'report'} kind
|
|
22
|
+
* @property {string} text One concise line: what to do next.
|
|
23
|
+
* @property {string} [command] Optional CLI/skill command to run.
|
|
24
|
+
* @property {string} [pragma] Optional inline suppression snippet.
|
|
25
|
+
*
|
|
26
|
+
* @typedef {Object} Finding
|
|
27
|
+
* @property {string} code Stable code, e.g. 'SEC001' (see CODES).
|
|
28
|
+
* @property {string} validator Owning validator key.
|
|
29
|
+
* @property {'error'|'warn'} severity
|
|
30
|
+
* @property {'high'|'low'} confidence 'low' = candidate false positive.
|
|
31
|
+
* @property {string} message Concise, NO ansi colour.
|
|
32
|
+
* @property {string|null} location 'path:line' or 'path'.
|
|
33
|
+
* @property {Suggestion|null} suggestion
|
|
34
|
+
* @property {boolean} reportable Surface in `docguard feedback`.
|
|
35
|
+
* @property {string|null} redactedContext Safe-to-share context for a report.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Stable finding-code registry. `docguard explain <CODE>` reads this, and
|
|
40
|
+
* inline `// docguard:ignore <CODE>` keys off it. Keep codes append-only — a
|
|
41
|
+
* published code is a public surface we don't renumber.
|
|
42
|
+
*/
|
|
43
|
+
export const CODES = {
|
|
44
|
+
SEC001: {
|
|
45
|
+
validator: 'security',
|
|
46
|
+
title: 'Hardcoded password',
|
|
47
|
+
help: 'A `password`/`passwd`/`pwd` assignment with a quoted literal value (8+ chars). If the value is natural-language UI copy or a validation message — not a credential — this is a false positive: DocGuard now flags those low-confidence, but you can suppress inline.',
|
|
48
|
+
suppress: '// docguard:ignore SEC001 — UI copy, not a credential',
|
|
49
|
+
},
|
|
50
|
+
SEC002: {
|
|
51
|
+
validator: 'security',
|
|
52
|
+
title: 'Hardcoded API key',
|
|
53
|
+
help: 'An `api_key`/`apikey` assignment with a quoted literal value (16+ chars). Move it to an environment variable and read it via `process.env`.',
|
|
54
|
+
suppress: '// docguard:ignore SEC002 — sample value in fixture',
|
|
55
|
+
},
|
|
56
|
+
SEC003: {
|
|
57
|
+
validator: 'security',
|
|
58
|
+
title: 'Hardcoded secret key',
|
|
59
|
+
help: 'A `secret_key`/`secretkey` assignment with a quoted literal value (16+ chars). Move it to an environment variable.',
|
|
60
|
+
suppress: '// docguard:ignore SEC003 — reason',
|
|
61
|
+
},
|
|
62
|
+
SEC004: {
|
|
63
|
+
validator: 'security',
|
|
64
|
+
title: 'Hardcoded access token',
|
|
65
|
+
help: 'An `access_token`/`accesstoken` assignment with a quoted literal value (16+ chars). Move it to an environment variable.',
|
|
66
|
+
suppress: '// docguard:ignore SEC004 — reason',
|
|
67
|
+
},
|
|
68
|
+
SEC005: {
|
|
69
|
+
validator: 'security',
|
|
70
|
+
title: 'AWS Access Key ID',
|
|
71
|
+
help: 'A string matching the AWS Access Key ID format (AKIA…). Rotate it immediately if real, and move credentials to the AWS credential chain / environment.',
|
|
72
|
+
suppress: '// docguard:ignore SEC005 — documented example key',
|
|
73
|
+
},
|
|
74
|
+
SEC006: {
|
|
75
|
+
validator: 'security',
|
|
76
|
+
title: 'API secret key (Stripe/OpenAI pattern)',
|
|
77
|
+
help: 'A string matching a live/test secret-key format (sk-…, sk_live_…). Rotate it if real and move it to an environment variable.',
|
|
78
|
+
suppress: '// docguard:ignore SEC006 — reason',
|
|
79
|
+
},
|
|
80
|
+
SEC010: {
|
|
81
|
+
validator: 'security',
|
|
82
|
+
title: '.env not in .gitignore',
|
|
83
|
+
help: 'No `.env` entry was found in .gitignore, so a local `.env` could be committed. Add `.env` (and `.env.local`) to .gitignore.',
|
|
84
|
+
suppress: null,
|
|
85
|
+
},
|
|
86
|
+
SEC011: {
|
|
87
|
+
validator: 'security',
|
|
88
|
+
title: 'No source files scanned for secrets',
|
|
89
|
+
help: 'The secret scan matched zero source files — usually a too-broad ignore config or a wrong sourceRoot. A scan that checks nothing is a dangerous false ✅.',
|
|
90
|
+
suppress: null,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build a Finding with sane defaults. `reportable` defaults to true for
|
|
96
|
+
* low-confidence findings — low confidence IS the feedback signal.
|
|
97
|
+
*
|
|
98
|
+
* @param {Partial<Finding>} f
|
|
99
|
+
* @returns {Finding}
|
|
100
|
+
*/
|
|
101
|
+
export function mkFinding(f) {
|
|
102
|
+
const severity = f.severity === 'error' ? 'error' : 'warn';
|
|
103
|
+
const confidence = f.confidence === 'low' ? 'low' : 'high';
|
|
104
|
+
return {
|
|
105
|
+
code: f.code || null,
|
|
106
|
+
validator: f.validator || null,
|
|
107
|
+
severity,
|
|
108
|
+
confidence,
|
|
109
|
+
message: f.message || '',
|
|
110
|
+
location: f.location || null,
|
|
111
|
+
suggestion: f.suggestion || null,
|
|
112
|
+
reportable: f.reportable === true || confidence === 'low',
|
|
113
|
+
redactedContext: f.redactedContext || null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Derive the legacy `{ errors, warnings, passed, total }` result from a list of
|
|
119
|
+
* findings, keeping `findings` attached for the rich renderer. ONE source of
|
|
120
|
+
* truth — the strings guard counts and the findings guard renders can never
|
|
121
|
+
* disagree because they're computed from the same array.
|
|
122
|
+
*
|
|
123
|
+
* @param {Finding[]} findings
|
|
124
|
+
* @param {{passed?:number, total?:number, applicable?:boolean}} [opts]
|
|
125
|
+
*/
|
|
126
|
+
export function resultFromFindings(findings, opts = {}) {
|
|
127
|
+
const errors = [];
|
|
128
|
+
const warnings = [];
|
|
129
|
+
for (const f of findings) {
|
|
130
|
+
if (f.severity === 'error') errors.push(f.message);
|
|
131
|
+
else warnings.push(f.message);
|
|
132
|
+
}
|
|
133
|
+
const res = {
|
|
134
|
+
errors,
|
|
135
|
+
warnings,
|
|
136
|
+
passed: opts.passed || 0,
|
|
137
|
+
total: opts.total != null ? opts.total : 0,
|
|
138
|
+
findings,
|
|
139
|
+
};
|
|
140
|
+
if (opts.applicable !== undefined) res.applicable = opts.applicable;
|
|
141
|
+
return res;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Does an inline `docguard:ignore` pragma in `text` suppress finding `code`?
|
|
146
|
+
*
|
|
147
|
+
* Accepted forms (mirrors the ergonomics of eslint-disable / ruff `# noqa`):
|
|
148
|
+
* docguard:ignore → suppresses ANY code on the line
|
|
149
|
+
* docguard:ignore SEC001 → suppresses exactly SEC001
|
|
150
|
+
* docguard:ignore SEC001,DQ002 → comma list
|
|
151
|
+
* docguard:ignore SEC* → prefix wildcard
|
|
152
|
+
* docguard:ignore all → suppresses any code
|
|
153
|
+
* docguard:ignore-secret → convenience alias for any SEC* code
|
|
154
|
+
*
|
|
155
|
+
* @param {string} text
|
|
156
|
+
* @param {string} code
|
|
157
|
+
* @returns {boolean}
|
|
158
|
+
*/
|
|
159
|
+
export function suppressesCode(text, code) {
|
|
160
|
+
if (!text || !code) return false;
|
|
161
|
+
const m = text.match(/docguard:ignore(-secret)?\b[ \t]*([A-Za-z0-9_,*-]+)?/i);
|
|
162
|
+
if (!m) return false;
|
|
163
|
+
if (m[1]) return /^SEC/i.test(code); // ignore-secret alias
|
|
164
|
+
const arg = (m[2] || '').trim();
|
|
165
|
+
if (!arg) return true; // bare ignore → any code
|
|
166
|
+
return arg.split(',').map((s) => s.trim()).some((tok) => {
|
|
167
|
+
if (!tok) return false;
|
|
168
|
+
if (tok.toLowerCase() === 'all') return true;
|
|
169
|
+
if (tok.endsWith('*')) return code.toUpperCase().startsWith(tok.slice(0, -1).toUpperCase());
|
|
170
|
+
return tok.toUpperCase() === code.toUpperCase();
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Source-line suppression: an ignore pragma counts if it's on the flagged line
|
|
176
|
+
* OR the line directly above it (so a comment can sit above the offending
|
|
177
|
+
* statement, the common style for non-trailing-comment languages).
|
|
178
|
+
*/
|
|
179
|
+
export function lineSuppresses(code, line, prevLine = '') {
|
|
180
|
+
return suppressesCode(line, code) || suppressesCode(prevLine, code);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Flatten a one-line, colour-free rendering of a suggestion — used by JSON
|
|
185
|
+
* consumers, diagnose, and the feedback body. Guard does its own coloured
|
|
186
|
+
* rendering and does not use this.
|
|
187
|
+
*/
|
|
188
|
+
export function suggestionLine(s) {
|
|
189
|
+
if (!s) return '';
|
|
190
|
+
let out = s.text || '';
|
|
191
|
+
if (s.command) out += ` → ${s.command}`;
|
|
192
|
+
else if (s.pragma) out += ` → ${s.pragma}`;
|
|
193
|
+
return out;
|
|
194
|
+
}
|
package/cli/shared-source.mjs
CHANGED
|
@@ -295,6 +295,27 @@ function classifyChars(content, ext) {
|
|
|
295
295
|
* including bracket access.
|
|
296
296
|
* @returns {Set<string>} variable names referenced in code
|
|
297
297
|
*/
|
|
298
|
+
/**
|
|
299
|
+
* v0.27 (field report #7): env vars injected by the test runner / CI / cloud
|
|
300
|
+
* SDK are READ in code (e.g. `if (process.env.VITEST)` as a test guard) but no
|
|
301
|
+
* application documents them as config — flagging them "undocumented" is a
|
|
302
|
+
* false positive. This is the env equivalent of the SYSTEM allowlist already
|
|
303
|
+
* applied on the docs side in environment.mjs.
|
|
304
|
+
*
|
|
305
|
+
* Deliberately conservative — NODE_ENV is intentionally NOT here: this project
|
|
306
|
+
* already decided NODE_ENV is legitimate app config (see environment.mjs).
|
|
307
|
+
*/
|
|
308
|
+
const RUNNER_ENV_VARS = new Set([
|
|
309
|
+
'VITEST', 'CI', 'JEST_WORKER_ID', 'AWS_SESSION_TOKEN', 'AWS_EXECUTION_ENV',
|
|
310
|
+
]);
|
|
311
|
+
const RUNNER_ENV_PREFIXES = ['GITHUB_', 'RUNNER_', 'VITEST_', 'JEST_', 'CIRCLE_', 'GITLAB_CI'];
|
|
312
|
+
|
|
313
|
+
/** True when `name` is a runner/CI/SDK-injected var, not product config. */
|
|
314
|
+
export function isRunnerEnvVar(name) {
|
|
315
|
+
if (RUNNER_ENV_VARS.has(name)) return true;
|
|
316
|
+
return RUNNER_ENV_PREFIXES.some((p) => name.startsWith(p));
|
|
317
|
+
}
|
|
318
|
+
|
|
298
319
|
export function grepEnvUsage(projectDir, config = {}) {
|
|
299
320
|
const names = new Set();
|
|
300
321
|
const roots = resolveSourceRoots(projectDir, config);
|
|
@@ -347,6 +368,7 @@ export function grepEnvUsage(projectDir, config = {}) {
|
|
|
347
368
|
while ((m = rx.exec(content)) !== null) {
|
|
348
369
|
if (kind[m.index] !== 0) continue; // keyword inside a string/comment → a mention, not a read
|
|
349
370
|
if (isViteSource && VITE_INTRINSICS.has(m[1])) continue;
|
|
371
|
+
if (isRunnerEnvVar(m[1])) continue; // v0.27 (#7): runner/CI/SDK var, not product config
|
|
350
372
|
names.add(m[1]);
|
|
351
373
|
}
|
|
352
374
|
}
|
|
@@ -366,12 +388,12 @@ export function grepEnvUsage(projectDir, config = {}) {
|
|
|
366
388
|
// camelCase keys, so requiring UPPER_SNAKE keeps this env-specific.
|
|
367
389
|
const keyRe = /^\s*['"]?([A-Z][A-Z0-9_]*[A-Z0-9])['"]?\s*:/gm;
|
|
368
390
|
while ((km = keyRe.exec(content)) !== null) {
|
|
369
|
-
if (km[1].length >= 3 && !VITE_INTRINSICS.has(km[1])) names.add(km[1]);
|
|
391
|
+
if (km[1].length >= 3 && !VITE_INTRINSICS.has(km[1]) && !isRunnerEnvVar(km[1])) names.add(km[1]);
|
|
370
392
|
}
|
|
371
393
|
// convict: the env var name is the `env:` property value, not the key.
|
|
372
394
|
const convictRe = /\benv\s*:\s*['"]([A-Z][A-Z0-9_]*[A-Z0-9])['"]/g;
|
|
373
395
|
while ((km = convictRe.exec(content)) !== null) {
|
|
374
|
-
if (km[1].length >= 3) names.add(km[1]);
|
|
396
|
+
if (km[1].length >= 3 && !isRunnerEnvVar(km[1])) names.add(km[1]);
|
|
375
397
|
}
|
|
376
398
|
}
|
|
377
399
|
};
|
|
@@ -533,7 +533,13 @@ function analyzeDocument(doc) {
|
|
|
533
533
|
conditionalLoad: conditional.ratio,
|
|
534
534
|
},
|
|
535
535
|
details: { passive, ambiguous, atomicity, negation, conditional },
|
|
536
|
-
overrides: {
|
|
536
|
+
overrides: {
|
|
537
|
+
negationLoad: parseQualityOverride(content, 'negation-load'),
|
|
538
|
+
// v0.27 (#9): parity with negation-load. Sequence/flow docs (MESSAGE-FLOWS,
|
|
539
|
+
// INTEGRATIONS) are legitimately passive; let them opt out per-doc instead
|
|
540
|
+
// of warning unconditionally.
|
|
541
|
+
passiveVoice: parseQualityOverride(content, 'passive-voice'),
|
|
542
|
+
},
|
|
537
543
|
};
|
|
538
544
|
}
|
|
539
545
|
|
|
@@ -561,12 +567,17 @@ export function validateDocQuality(projectDir, config) {
|
|
|
561
567
|
|
|
562
568
|
// ── Check 1: Passive Voice ──
|
|
563
569
|
results.total++;
|
|
564
|
-
|
|
570
|
+
const passiveOv = analysis.overrides?.passiveVoice;
|
|
571
|
+
const passiveThreshold = passiveOv?.threshold
|
|
572
|
+
?? config.docQuality?.passiveVoiceThreshold
|
|
573
|
+
?? THRESHOLDS.passiveVoiceRatio.warn;
|
|
574
|
+
if (passiveOv?.off || m.passiveVoiceRatio <= passiveThreshold) {
|
|
565
575
|
results.passed++;
|
|
566
576
|
} else {
|
|
567
577
|
results.warnings.push(
|
|
568
578
|
`${doc.name}: High passive voice ratio (${(m.passiveVoiceRatio * 100).toFixed(0)}% of sentences). ` +
|
|
569
|
-
`Use active voice for clarity. Found ${analysis.details.passive.count}/${analysis.details.passive.total} passive sentences`
|
|
579
|
+
`Use active voice for clarity. Found ${analysis.details.passive.count}/${analysis.details.passive.total} passive sentences. ` +
|
|
580
|
+
`If the passive voice is intentional (sequence/flow doc), add: <!-- docguard:quality passive-voice off — your reason -->`
|
|
570
581
|
);
|
|
571
582
|
}
|
|
572
583
|
|
|
@@ -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
|
|
|
@@ -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