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 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 (14)"]
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 **14 commands** (the "Daily 5" + 9 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).
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
- | `explain <warning>` | Paste any warningget the validator's docstring + fix path |
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
+ }
@@ -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.endpoints.length} endpoints · ${plan.surface.entities.length} entities · ${plan.surface.screens.length} screens · ${plan.surface.components.length} components · ${plan.surface.envVars.length} env vars\n`);
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}`);
@@ -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
- if (show || v.status === 'fail') {
477
- for (const err of v.errors) {
478
- console.log(` ${c.red}✗ ${err}${c.reset}`);
479
- }
480
- }
481
- if (show || v.status === 'warn') {
482
- for (const warn of v.warnings) {
483
- console.log(` ${c.yellow}⚠ ${warn}${c.reset}`);
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 step hint always point to diagnose when issues exist
562
+ // ── Next stepsevery 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
- const agentMode = detectAgentMode(projectDir);
520
- if (agentMode === 'llm') {
521
- console.log(` ${c.dim}Use ${c.cyan}/docguard.diagnose${c.dim} to get AI fix prompts.${c.reset}`);
522
- } else {
523
- console.log(` ${c.dim}Run ${c.cyan}docguard diagnose${c.dim} to get AI fix prompts.${c.reset}`);
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