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
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"]
|
|
@@ -128,6 +128,8 @@ See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
|
128
128
|
|
|
129
129
|
## ⚡ Quick Start
|
|
130
130
|
|
|
131
|
+
> **Package naming:** this repo is `raccioly/docguard`; the published package is **`docguard-cli`** on both [npm](https://www.npmjs.com/package/docguard-cli) and [PyPI](https://pypi.org/project/docguard-cli/); the installed command is `docguard`. Same project — the `-cli` suffix is just the registry name. The package runs **no install scripts**, so `npm i -g docguard-cli --ignore-scripts` is equivalent.
|
|
132
|
+
|
|
131
133
|
### Node.js (npm)
|
|
132
134
|
|
|
133
135
|
```bash
|
|
@@ -248,7 +250,7 @@ This installs DocGuard's slash commands (`/docguard.init`, `/docguard.guard`, `/
|
|
|
248
250
|
|
|
249
251
|
## Usage
|
|
250
252
|
|
|
251
|
-
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).
|
|
252
254
|
|
|
253
255
|
**The Daily 5** — what you'll reach for 95% of the time:
|
|
254
256
|
|
|
@@ -270,7 +272,9 @@ DocGuard ships **14 commands** (the "Daily 5" + 9 situational tools, including t
|
|
|
270
272
|
| `fix --write` | Apply deterministic fixes (no AI — version bumps, counts, anchors, sections) |
|
|
271
273
|
| `fix --history` | Audit log of every mechanical fix applied (from `.docguard/fixed.json`) |
|
|
272
274
|
| `generate` | Reverse-engineer docs from existing codebase (`--plan` for AI scan) |
|
|
273
|
-
| `
|
|
275
|
+
| `agent` | One-shot agent task graph — ordered, pre-filled code-truth, per-task verify (`--format json`) |
|
|
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) |
|
|
274
278
|
| `memory` | Per-domain accuracy headline (endpoints / entities / env / tech) |
|
|
275
279
|
| `memory --diff` | Drill into which specific claims don't match code |
|
|
276
280
|
| `score --diff` | Drill into which checks pulled each category down |
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `docguard agent` — the one-shot agent task graph.
|
|
3
|
+
*
|
|
4
|
+
* Field report §2: an LLM told "run docguard and fix the docs" had to drive ~10
|
|
5
|
+
* manual round-trips (guard → init → config → generate → hand-write 7 docs →
|
|
6
|
+
* guard → fix FPs → …). This command collapses that into a SINGLE ordered,
|
|
7
|
+
* dependency-aware, self-contained task stream an agent executes without extra
|
|
8
|
+
* discovery:
|
|
9
|
+
* - code-truth tasks ship PRE-FILLED content (insert as-is; only `<!-- … -->`
|
|
10
|
+
* placeholders need values),
|
|
11
|
+
* - human-judgment tasks carry the instruction + grounding facts,
|
|
12
|
+
* - every task has an acceptance/verify command so the agent self-checks in
|
|
13
|
+
* isolation instead of re-running the whole suite and diffing,
|
|
14
|
+
* - phases impose order: config → canonical-docs → verify,
|
|
15
|
+
* - confidence is propagated (code-truth = high; prose = requires-human), and
|
|
16
|
+
* anything the profile suppressed surfaces as a note, never a committed guess.
|
|
17
|
+
*
|
|
18
|
+
* Read-only: it emits the plan, it never writes. A compact human summary by
|
|
19
|
+
* default; `--format json` emits the full machine task graph (the agent-
|
|
20
|
+
* executable artifact) — mirroring `generate --plan` / `generate --plan
|
|
21
|
+
* --format json`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { buildMemoryPlan } from '../scanners/memory-plan.mjs';
|
|
25
|
+
import { c } from '../shared.mjs';
|
|
26
|
+
|
|
27
|
+
const PHASES = ['config', 'canonical-docs', 'verify'];
|
|
28
|
+
|
|
29
|
+
function docSlug(path) {
|
|
30
|
+
return path.replace(/^docs-(?:canonical|implementation)\//, '').replace(/\.md$/, '').toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Transform a memory plan into the ordered agent task graph. Pure — no I/O — so
|
|
35
|
+
* it is unit-testable and reused by both the command and `generate`.
|
|
36
|
+
*/
|
|
37
|
+
export function buildAgentTaskGraph(projectDir, config, plan) {
|
|
38
|
+
const profileName = config.profile || 'standard';
|
|
39
|
+
const tasks = [];
|
|
40
|
+
|
|
41
|
+
// Phase 1 — config: make the project model explicit BEFORE any doc is written.
|
|
42
|
+
// (The agent learned this ordering by trial in the field report; encode it.)
|
|
43
|
+
tasks.push({
|
|
44
|
+
id: 'config.setup',
|
|
45
|
+
phase: 'config',
|
|
46
|
+
file: '.docguard.json',
|
|
47
|
+
kind: 'human-judgment',
|
|
48
|
+
instruction: `Ensure .docguard.json exists and is correct: run \`docguard init --profile ${profileName}\` if it is absent. Confirm projectName is "${config.projectName}" and profile is "${profileName}".`,
|
|
49
|
+
grounding: { projectName: config.projectName, profile: profileName, kind: plan.profile.kind, languages: plan.profile.languages },
|
|
50
|
+
prefilled: null,
|
|
51
|
+
acceptance: { verify: 'docguard guard --format json', expect: '.docguard.json present and the config validator passes' },
|
|
52
|
+
confidence: 'high',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Phase 2 — canonical-docs: one task per section. Section order from the plan
|
|
56
|
+
// already puts code-truth before prose within each doc, so insert-then-write.
|
|
57
|
+
for (const doc of plan.docs) {
|
|
58
|
+
const slug = docSlug(doc.path);
|
|
59
|
+
for (const sec of doc.sections) {
|
|
60
|
+
const isCode = sec.source === 'code';
|
|
61
|
+
tasks.push({
|
|
62
|
+
id: `${slug}.${sec.id}`,
|
|
63
|
+
phase: 'canonical-docs',
|
|
64
|
+
file: doc.path,
|
|
65
|
+
section: sec.id,
|
|
66
|
+
kind: isCode ? 'code-truth' : 'human-judgment',
|
|
67
|
+
prefilled: isCode ? sec.body : null,
|
|
68
|
+
instruction: isCode
|
|
69
|
+
? `Insert the pre-filled "${sec.id}" content into ${doc.path} verbatim — it is extracted from your code. Only fill any \`<!-- … -->\` placeholders.`
|
|
70
|
+
: sec.task,
|
|
71
|
+
grounding: sec.grounding || null,
|
|
72
|
+
acceptance: { verify: 'docguard guard --format json', expect: `no missing/stale finding for ${doc.path}` },
|
|
73
|
+
confidence: isCode ? 'high' : 'requires-human',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Phase 3 — verify: the agent's own gate. Self-check, don't assume.
|
|
79
|
+
tasks.push({
|
|
80
|
+
id: 'verify.guard',
|
|
81
|
+
phase: 'verify',
|
|
82
|
+
file: null,
|
|
83
|
+
kind: 'verify',
|
|
84
|
+
instruction: 'Run `docguard guard --format json`. Resolve every error and warning, then re-run until clean. Run `docguard score` to confirm the maturity grade.',
|
|
85
|
+
prefilled: null,
|
|
86
|
+
grounding: null,
|
|
87
|
+
acceptance: { verify: 'docguard guard --format json', expect: '0 errors' },
|
|
88
|
+
confidence: 'high',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
project: config.projectName,
|
|
93
|
+
profile: { name: profileName, kind: plan.profile.kind, languages: plan.profile.languages, frameworks: plan.profile.frameworks },
|
|
94
|
+
order: PHASES,
|
|
95
|
+
counts: {
|
|
96
|
+
tasks: tasks.length,
|
|
97
|
+
codeTruth: tasks.filter(t => t.kind === 'code-truth').length,
|
|
98
|
+
humanJudgment: tasks.filter(t => t.kind === 'human-judgment').length,
|
|
99
|
+
},
|
|
100
|
+
tasks,
|
|
101
|
+
notes: plan.notes || [],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function runAgent(projectDir, config, flags) {
|
|
106
|
+
// Allow `--profile <name>` to preview a profile's plan without having to run
|
|
107
|
+
// `init` first (the field-report agent had no config yet on its first call).
|
|
108
|
+
const cfg = flags.profile ? { ...config, profile: flags.profile } : config;
|
|
109
|
+
const plan = buildMemoryPlan(projectDir, cfg);
|
|
110
|
+
const graph = buildAgentTaskGraph(projectDir, cfg, plan);
|
|
111
|
+
|
|
112
|
+
if (flags.format === 'json') {
|
|
113
|
+
// The agent-executable artifact.
|
|
114
|
+
console.log(JSON.stringify({ ...graph, timestamp: new Date().toISOString() }, null, 2));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Default: a compact human summary of the same graph.
|
|
119
|
+
{
|
|
120
|
+
console.log(`${c.bold}🤖 DocGuard Agent Task Graph — ${graph.project}${c.reset}`);
|
|
121
|
+
console.log(`${c.dim} profile: ${graph.profile.name} · kind: ${graph.profile.kind} · ${graph.counts.tasks} tasks (${graph.counts.codeTruth} code-truth, ${graph.counts.humanJudgment} human)${c.reset}\n`);
|
|
122
|
+
for (const phase of graph.order) {
|
|
123
|
+
const inPhase = graph.tasks.filter(t => t.phase === phase);
|
|
124
|
+
if (!inPhase.length) continue;
|
|
125
|
+
console.log(` ${c.bold}▸ ${phase}${c.reset}`);
|
|
126
|
+
for (const t of inPhase) {
|
|
127
|
+
const tag = t.kind === 'code-truth' ? `${c.green}[code]${c.reset} `
|
|
128
|
+
: t.kind === 'verify' ? `${c.cyan}[verify]${c.reset}` : `${c.yellow}[human]${c.reset}`;
|
|
129
|
+
console.log(` ${tag} ${c.bold}${t.id}${c.reset}${t.file ? ` ${c.dim}→ ${t.file}${c.reset}` : ''}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const note of graph.notes) console.log(` ${c.yellow}ℹ️ ${note}${c.reset}`);
|
|
133
|
+
console.log(`\n ${c.dim}Run with --format json for the full machine-readable task stream.${c.reset}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
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
|
+
}
|
|
@@ -164,6 +164,8 @@ function surfaceConfidence(kind) {
|
|
|
164
164
|
* inserted as agent-task placeholders), respecting human prose via markers.
|
|
165
165
|
*/
|
|
166
166
|
export function runGeneratePlan(projectDir, config, flags) {
|
|
167
|
+
// `--profile <name>` previews a profile's doc set without needing `init` first.
|
|
168
|
+
if (flags.profile) config = { ...config, profile: flags.profile };
|
|
167
169
|
const plan = buildMemoryPlan(projectDir, config);
|
|
168
170
|
|
|
169
171
|
if (flags.format === 'json') {
|
|
@@ -181,6 +183,8 @@ export function runGeneratePlan(projectDir, config, flags) {
|
|
|
181
183
|
entities: plan.surface.entities.length,
|
|
182
184
|
screens: plan.surface.screens.length,
|
|
183
185
|
components: plan.surface.components.length,
|
|
186
|
+
modules: plan.surface.modules.length,
|
|
187
|
+
tests: { files: plan.surface.tests.totalFiles, cases: plan.surface.tests.totalCases },
|
|
184
188
|
envVars: plan.surface.envVars.length,
|
|
185
189
|
confidence: surfaceConfidence(plan.profile.kind),
|
|
186
190
|
},
|
|
@@ -191,6 +195,7 @@ export function runGeneratePlan(projectDir, config, flags) {
|
|
|
191
195
|
: { id: s.id, source: 'human', task: s.task, grounding: s.grounding }),
|
|
192
196
|
})),
|
|
193
197
|
agentTasks: plan.agentTasks,
|
|
198
|
+
notes: plan.notes || [],
|
|
194
199
|
timestamp: new Date().toISOString(),
|
|
195
200
|
}, null, 2));
|
|
196
201
|
return;
|
|
@@ -225,6 +230,9 @@ export function runGeneratePlan(projectDir, config, flags) {
|
|
|
225
230
|
if (registered > 0) {
|
|
226
231
|
console.log(` ${c.dim}Registered ${registered} canonical doc(s) in .docguard.json requiredFiles.${c.reset}`);
|
|
227
232
|
}
|
|
233
|
+
for (const note of plan.notes || []) {
|
|
234
|
+
console.log(` ${c.yellow}ℹ️ ${note}${c.reset}`);
|
|
235
|
+
}
|
|
228
236
|
console.log(` ${c.dim}Now run your AI agent (/docguard.fix) to write the prose sections, then ${c.cyan}docguard guard${c.dim}.${c.reset}\n`);
|
|
229
237
|
return;
|
|
230
238
|
}
|
|
@@ -232,7 +240,7 @@ export function runGeneratePlan(projectDir, config, flags) {
|
|
|
232
240
|
// Text summary.
|
|
233
241
|
console.log(`${c.bold}🔮 DocGuard Generate Plan — ${config.projectName}${c.reset}`);
|
|
234
242
|
console.log(`${c.dim} ${plan.profile.polyglot ? 'Polyglot' : 'Single-language'}: ${plan.profile.languages.join(', ')} | frameworks: ${plan.profile.frameworks.join(', ') || '—'} | kind: ${plan.profile.kind}${c.reset}\n`);
|
|
235
|
-
console.log(` ${c.bold}Code-truth surface:${c.reset} ${plan.surface.
|
|
243
|
+
console.log(` ${c.bold}Code-truth surface:${c.reset} ${plan.surface.modules.length} modules · ${plan.surface.tests.totalFiles} test files (${plan.surface.tests.totalCases} cases) · ${plan.surface.endpoints.length} endpoints · ${plan.surface.entities.length} entities · ${plan.surface.screens.length} screens · ${plan.surface.envVars.length} env vars\n`);
|
|
236
244
|
const webSurface = plan.surface.endpoints.length + plan.surface.entities.length + plan.surface.screens.length + plan.surface.components.length;
|
|
237
245
|
if (surfaceConfidence(plan.profile.kind) === 'low' && webSurface > 0) {
|
|
238
246
|
console.log(` ${c.yellow}⚠️ Low-confidence surface:${c.reset} ${c.dim}this looks like a ${plan.profile.kind} (not a web app), so the HTTP/SDK/route surface above may be pattern-matches in your OWN source — not real usage. Verify before documenting; pin any corrected code section with ${c.cyan}pinned="reason"${c.dim}.${c.reset}\n`);
|
|
@@ -243,6 +251,9 @@ export function runGeneratePlan(projectDir, config, flags) {
|
|
|
243
251
|
const prose = d.sections.filter(s => s.source === 'human').length;
|
|
244
252
|
console.log(` ${c.cyan}${d.path}${c.reset} ${c.dim}(${code} code section(s), ${prose} agent task(s))${c.reset}`);
|
|
245
253
|
}
|
|
254
|
+
for (const note of plan.notes || []) {
|
|
255
|
+
console.log(` ${c.yellow}ℹ️ ${note}${c.reset}`);
|
|
256
|
+
}
|
|
246
257
|
console.log(`\n ${c.bold}🤖 Agent tasks (${plan.agentTasks.length}):${c.reset} ${c.dim}prose the AI must write, grounded in scanned facts.${c.reset}`);
|
|
247
258
|
for (const t of plan.agentTasks) {
|
|
248
259
|
console.log(` ${c.dim}• [${t.doc} → ${t.sectionId}] ${t.instruction}${c.reset}`);
|
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
|