docguard-cli 0.35.0 → 0.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +8 -15
  2. package/cli/commands/agent.mjs +27 -6
  3. package/cli/commands/ci.mjs +3 -0
  4. package/cli/commands/diagnose.mjs +8 -2
  5. package/cli/commands/feedback.mjs +83 -89
  6. package/cli/commands/fix.mjs +4 -0
  7. package/cli/commands/generate.mjs +3 -0
  8. package/cli/commands/guard.mjs +37 -20
  9. package/cli/commands/hooks.mjs +61 -40
  10. package/cli/commands/init.mjs +51 -5
  11. package/cli/commands/memory.mjs +29 -15
  12. package/cli/commands/report.mjs +12 -7
  13. package/cli/commands/score.mjs +39 -19
  14. package/cli/commands/sync.mjs +2 -0
  15. package/cli/commands/watch.mjs +113 -70
  16. package/cli/config.mjs +6 -3
  17. package/cli/docguard.mjs +12 -4
  18. package/cli/findings.mjs +13 -13
  19. package/cli/scanners/memory-plan.mjs +279 -134
  20. package/cli/scanners/project-type.mjs +6 -1
  21. package/cli/scanners/semantic-claims.mjs +176 -26
  22. package/cli/shared-diff.mjs +22 -1
  23. package/cli/shared-doc-roles.mjs +59 -0
  24. package/cli/shared-ignore.mjs +15 -2
  25. package/cli/shared-source.mjs +223 -1
  26. package/cli/validator-coverage.mjs +20 -0
  27. package/cli/validators/api-surface.mjs +94 -70
  28. package/cli/validators/architecture.mjs +19 -5
  29. package/cli/validators/diff-suspicion.mjs +45 -9
  30. package/cli/validators/docs-coverage.mjs +6 -5
  31. package/cli/validators/docs-diff.mjs +51 -7
  32. package/cli/validators/environment.mjs +3 -2
  33. package/cli/validators/freshness.mjs +140 -83
  34. package/cli/validators/schema-sync.mjs +3 -2
  35. package/cli/validators/security.mjs +58 -23
  36. package/cli/validators/structure.mjs +3 -1
  37. package/cli/validators/test-spec.mjs +3 -2
  38. package/cli/validators/todo-tracking.mjs +61 -28
  39. package/cli/validators/traceability.mjs +152 -38
  40. package/docs/configuration.md +41 -0
  41. package/extensions/spec-kit-docguard/README.md +6 -6
  42. package/extensions/spec-kit-docguard/commands/sync.md +1 -1
  43. package/extensions/spec-kit-docguard/extension.yml +3 -4
  44. package/extensions/spec-kit-docguard/scripts/bash/common.sh +9 -17
  45. package/extensions/spec-kit-docguard/scripts/bash/docguard-check-docs.sh +18 -11
  46. package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +3 -3
  47. package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
  48. package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
  49. package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
  50. package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +2 -2
  51. package/extensions/spec-kit-docguard/templates/extensions.yml +1 -2
  52. package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +74 -29
  53. package/package.json +1 -1
  54. package/schemas/docguard-config.schema.json +43 -1
  55. package/templates/ci/github-actions.yml +51 -11
package/README.md CHANGED
@@ -102,20 +102,13 @@ graph TD
102
102
 
103
103
  ## Why DocGuard?
104
104
 
105
- Documentation that drifts from code is worse than no documentation it
106
- confidently misleads humans and AI agents alike. DocGuard treats your canonical
107
- docs as an enforced contract: deterministic validators diff what the docs claim
108
- against what the code does, on every commit, with no LLM required. The full
109
- thesis (and the research behind it) lives in [PHILOSOPHY.md](PHILOSOPHY.md);
110
- recent feature highlights moved [below](#-whats-new).
111
-
112
- The field data backs the enforcement-over-instructions bet: an ETH Zurich
113
- study across 138 repos / 5,694 agent PRs found the most popular style of
114
- agent-instruction file *hurts* agent performance, and practitioners keep
115
- converging on the same lesson — written rules are routinely ignored;
116
- programmatic checks are what agents (and humans) actually respect. That is
117
- exactly the layer DocGuard provides: not another instructions file, but the
118
- validator suite that makes the instructions and docs verifiably true.
105
+ DocGuard checks declared documentation facts against repository evidence and gives agents structured repair tasks. Deterministic checks cover supported facts, references, and generated sections. Human-authored requirements and architectural decisions retain their authority when implementation diverges.
106
+
107
+ A guard result describes the checks performed. The CDD grade measures structural maturity. Factual accuracy stays explicitly unverified until the relevant claims have supporting evidence. Coverage and unresolved claims remain visible, so teams can choose an appropriate enforcement policy.
108
+
109
+ Research motivates evaluation of this approach. A 2026 study found that repository context files did not generally improve task success and increased inference cost in its evaluated settings. It also found agents generally followed the instructions. These results support testing concise, relevant context and measuring actual task outcomes; they do not establish DocGuard's effectiveness. [Evaluating AGENTS.md, revised June 2026](https://arxiv.org/abs/2602.11988v2).
110
+
111
+ The development plan prioritizes accurate detection, reproducible evidence, and contributor-supplied regression cases. See [the trust roadmap](docs-implementation/TRUST-ROADMAP.md) for implementation status, proposed experiments, and acceptance criteria.
119
112
 
120
113
  ---
121
114
 
@@ -215,7 +208,7 @@ DocGuard splits drift into two kinds and is explicit about which is which:
215
208
 
216
209
  `docguard fix --write` only touches docs marked `<!-- docguard:generated true -->` (override with `--force`), is idempotent, and prints exactly what changed. It never rewrites prose — that stays with the agent.
217
210
 
218
- ### Hands-off loop (set and forget)
211
+ ### Continuous documentation workflow
219
212
 
220
213
  ```
221
214
  guard ──▶ fix --write (mechanical, auto) ──▶ guard ──▶ diagnose (agent prompts for the rest)
@@ -23,6 +23,8 @@
23
23
 
24
24
  import { buildMemoryPlan } from '../scanners/memory-plan.mjs';
25
25
  import { c } from '../shared.mjs';
26
+ import { createEvidenceReader, citedSources, taskEvidence, gitEvidence, SEMANTIC_COVERAGE_LIMITATION } from '../scanners/semantic-claims.mjs';
27
+ import { buildScoreAssurance } from './score.mjs';
26
28
 
27
29
  const PHASES = ['config', 'canonical-docs', 'verify'];
28
30
 
@@ -31,8 +33,8 @@ function docSlug(path) {
31
33
  }
32
34
 
33
35
  /**
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
+ * Transform a memory plan into the ordered agent task graph, with bounded
37
+ * read-only input snapshots. Evidence records inputs, never completed review.
36
38
  */
37
39
  export function buildAgentTaskGraph(projectDir, config, plan) {
38
40
  const profileName = config.profile || 'standard';
@@ -69,7 +71,13 @@ export function buildAgentTaskGraph(projectDir, config, plan) {
69
71
  ? `Insert the pre-filled "${sec.id}" content into ${doc.path} verbatim — it is extracted from your code. Only fill any \`<!-- … -->\` placeholders.`
70
72
  : sec.task,
71
73
  grounding: sec.grounding || null,
72
- acceptance: { verify: 'docguard guard --format json', expect: `no missing/stale finding for ${doc.path}` },
74
+ acceptance: {
75
+ verify: 'docguard guard --format json',
76
+ expect: `no missing/stale finding for ${doc.path}; ${isCode ? 'compare generated content with current sources' : 'review prose against sources and project intent separately'}`,
77
+ scope: 'structural-only',
78
+ reviewRequired: true,
79
+ factualAccuracy: 'unknown',
80
+ },
73
81
  confidence: isCode ? 'high' : 'requires-human',
74
82
  });
75
83
  }
@@ -81,15 +89,27 @@ export function buildAgentTaskGraph(projectDir, config, plan) {
81
89
  phase: 'verify',
82
90
  file: null,
83
91
  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.',
92
+ instruction: 'Run `docguard guard --format json`. Resolve every error, then re-run until there are 0 errors. Triage warnings and record unresolved warnings; warnings do not fail this acceptance gate. Run `docguard score` for structural maturity, and `docguard verify --semantic` to obtain unverified claim tasks for separate source review. Neither guard nor score verifies prose or factual accuracy.',
85
93
  prefilled: null,
86
94
  grounding: null,
87
- acceptance: { verify: 'docguard guard --format json', expect: '0 errors' },
95
+ acceptance: { verify: 'docguard guard --format json', expect: '0 errors', warnings: 'triage-and-report', scope: 'structural-only', factualAccuracy: 'unknown' },
88
96
  confidence: 'high',
89
97
  });
90
98
 
99
+ const read = createEvidenceReader(projectDir);
100
+ for (const task of tasks) {
101
+ const inputs = JSON.stringify({ instruction: task.instruction, prefilled: task.prefilled, grounding: task.grounding });
102
+ const citations = citedSources([read(task.file).content, task.prefilled, task.instruction].filter(Boolean).join('\n'));
103
+ task.evidence = taskEvidence(read, task.file, citations, inputs);
104
+ }
105
+
106
+ const assurance = buildScoreAssurance(projectDir, config);
107
+ assurance.limitation += ` ${SEMANTIC_COVERAGE_LIMITATION}`;
108
+
91
109
  return {
92
110
  project: config.projectName,
111
+ provenance: { kind: 'snapshot', git: gitEvidence(projectDir) },
112
+ assurance,
93
113
  profile: { name: profileName, kind: plan.profile.kind, languages: plan.profile.languages, frameworks: plan.profile.frameworks },
94
114
  order: PHASES,
95
115
  counts: {
@@ -106,7 +126,7 @@ export function runAgent(projectDir, config, flags) {
106
126
  // Allow `--profile <name>` to preview a profile's plan without having to run
107
127
  // `init` first (the field-report agent had no config yet on its first call).
108
128
  const cfg = flags.profile ? { ...config, profile: flags.profile } : config;
109
- const plan = buildMemoryPlan(projectDir, cfg);
129
+ const plan = buildMemoryPlan(projectDir, { ...cfg, diskCache: false });
110
130
  const graph = buildAgentTaskGraph(projectDir, cfg, plan);
111
131
 
112
132
  if (flags.format === 'json') {
@@ -119,6 +139,7 @@ export function runAgent(projectDir, config, flags) {
119
139
  {
120
140
  console.log(`${c.bold}🤖 DocGuard Agent Task Graph — ${graph.project}${c.reset}`);
121
141
  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`);
142
+ console.log(` ${c.dim}Snapshot only · structural checks do not verify prose · ${graph.assurance.unverifiedClaims ?? 'unknown'} extracted claims await review.${c.reset}\n`);
122
143
  for (const phase of graph.order) {
123
144
  const inPhase = graph.tasks.filter(t => t.phase === phase);
124
145
  if (!inPhase.length) continue;
@@ -62,6 +62,7 @@ export function runCI(projectDir, config, flags) {
62
62
  errors: guardData.errors,
63
63
  warnings: guardData.warnings,
64
64
  baselineSuppressed: guardData.baselineSuppressed || 0,
65
+ checkCoverage: guardData.checkCoverage,
65
66
  passed: guardData.passed,
66
67
  total: guardData.total,
67
68
  status,
@@ -74,6 +75,8 @@ export function runCI(projectDir, config, flags) {
74
75
  project: config.projectName,
75
76
  profile: config.profile || 'standard',
76
77
  projectType: config.projectType || 'unknown',
78
+ scoreKind: scoreData.scoreKind,
79
+ assurance: scoreData.assurance,
77
80
  score: scoreData.score,
78
81
  grade: scoreData.grade,
79
82
  guard: {
@@ -1,3 +1,4 @@
1
+ import { assertDefaultDocWrites } from '../shared-doc-roles.mjs';
1
2
  /**
2
3
  * Diagnose Command — The AI Orchestrator
3
4
  *
@@ -181,6 +182,7 @@ const FIX_INSTRUCTIONS = {
181
182
  };
182
183
 
183
184
  export function runDiagnose(projectDir, config, flags) {
185
+ if (flags.auto) assertDefaultDocWrites(config);
184
186
  // ── Step 0: Detect agent mode (LLM-first) ──
185
187
  const agentMode = detectAgentMode(projectDir);
186
188
 
@@ -347,6 +349,9 @@ function outputJSON(guardData, scoreData, issues) {
347
349
  status: guardData.status,
348
350
  score: scoreData.score,
349
351
  grade: scoreData.grade,
352
+ scoreKind: scoreData.scoreKind,
353
+ assurance: scoreData.assurance,
354
+ checkCoverage: guardData.checkCoverage,
350
355
  issueCount: issues.length,
351
356
  issues: issues.map(i => ({
352
357
  severity: i.severity,
@@ -493,7 +498,8 @@ function outputPrompt(projectDir, guardData, scoreData, issues, flags, agentMode
493
498
  lines.push('After making all fixes, run: docguard guard');
494
499
  }
495
500
  lines.push('Expected result: All checks pass (0 errors, 0 warnings)');
496
- lines.push(`Target score: ≥${Math.min(scoreData.score + 5, 100)}/100`);
501
+ lines.push(`Structural baseline: ${scoreData.score}/100. Resolve evidenced defects; verify material claims separately.`);
502
+ lines.push('Preserve approved requirements when implementation disagrees. A higher score is not proof of factual correctness.');
497
503
 
498
504
  // Agent-aware: add explicit checklist for basic-tier agents
499
505
  if (agentTier === 'basic') {
@@ -581,7 +587,7 @@ function outputDebatePrompt(projectDir, guardData, scoreData, issues, agentMode
581
587
  lines.push(' c. What content to write (be specific, not vague)');
582
588
  const verifyCmd = agentMode === 'llm' ? '/docguard.guard' : 'docguard guard';
583
589
  lines.push(`4. After all fixes, verify with: ${verifyCmd}`);
584
- lines.push(`5. Target score: ≥${Math.min(scoreData.score + 10, 100)}/100`);
590
+ lines.push('5. Verify repaired claims against their evidence and retain unresolved uncertainty. Structural score is a proxy.');
585
591
  lines.push('');
586
592
  lines.push('═══════════════════════════════════════════════════════');
587
593
  lines.push('Execute all three perspectives in sequence, then implement the Synthesizer\'s plan.');
@@ -1,32 +1,25 @@
1
1
  /**
2
- * Feedback Command — close the loop with the tool's maintainers (v0.27).
2
+ * Feedback Command — prepare opt-in detection feedback.
3
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:
4
+ * Select uncertain findings by default, or challenge active findings with
5
+ * --code / --all. Full local records may contain private diagnostics;
6
+ * --preview skips feedback-record writes. Persistence outcomes are explicit.
8
7
  *
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.
8
+ * Shared issue and search URLs contain only allowlisted finding identity,
9
+ * tool metadata, and contribution instructions. Paths, source text, messages,
10
+ * suggestions, and redactedContext are excluded. URLs are capped; users must
11
+ * review and supply a synthetic reproduction before submitting an issue.
12
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.
13
+ * Nothing is transmitted automatically. This command never scaffolds skills
14
+ * or edits source files. Zero npm dependencies pure Node.js built-ins.
24
15
  */
25
16
 
26
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
27
- import { resolve, dirname, basename } from 'node:path';
17
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
18
+ import { resolve, dirname } from 'node:path';
28
19
  import { fileURLToPath } from 'node:url';
29
20
  import { c } from '../shared.mjs';
21
+ import { CODES } from '../findings.mjs';
22
+ import { safeWrite } from '../writers/generate-io.mjs';
30
23
  import { runGuardInternal } from './guard.mjs';
31
24
 
32
25
  const _PKG = JSON.parse(
@@ -45,69 +38,54 @@ function shortId(str) {
45
38
  return h.toString(36).slice(0, 6);
46
39
  }
47
40
 
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
- '',
41
+ /** Shared output deliberately excludes every source-derived string. */
42
+ export function buildIssueUrl(finding) {
43
+ const code = Object.hasOwn(CODES, finding.code || '') ? finding.code : 'FINDING';
44
+ const validator = CODES[code]?.validator || 'unknown';
45
+ const title = `[feedback] ${code} (${validator}): detection feedback`;
46
+ const confidence = ['high', 'medium', 'low'].includes(finding.confidence) ? finding.confidence : 'unknown';
47
+ const body = [
48
+ `DocGuard v${CLI_VERSION}`,
69
49
  `- Code: ${code}`,
70
50
  `- 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 };
51
+ `- Confidence: ${confidence}`,
52
+ '',
53
+ 'Expected behavior:',
54
+ 'Actual behavior:',
55
+ 'Minimal synthetic reproduction (review before attaching):',
56
+ '',
57
+ 'Check existing open AND closed issues and pull requests before submitting.',
58
+ 'A regression-test-only contribution is welcome; explain the expected behavior.',
59
+ '',
60
+ 'Generated by docguard feedback. No project paths, messages, source code, or secret values are included.',
61
+ ].join('\n');
62
+ const url = `${ISSUES_BASE}/new?labels=docguard-feedback&title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
63
+ const query = `repo:raccioly/docguard ${code}`;
64
+ const searchUrl = `https://github.com/search?q=${encodeURIComponent(query)}&type=issues`;
65
+ return { url: url.length <= URL_CAP ? url : `${ISSUES_BASE}/new`, title, searchUrl };
96
66
  }
97
67
 
98
68
  export function runFeedback(projectDir, config, flags) {
99
69
  const data = runGuardInternal(projectDir, config);
100
- const reportable = (data.findings || []).filter((f) => f.reportable);
70
+ const selectedCode = typeof flags.code === 'string' ? flags.code.toUpperCase() : null;
71
+ if (flags.code !== undefined && (!selectedCode || !Object.hasOwn(CODES, selectedCode))) {
72
+ const error = 'Unknown finding code. Run docguard explain <CODE> to inspect supported findings.';
73
+ if (flags.format === 'json') console.log(JSON.stringify({ error, reportable: [] }));
74
+ else console.error(error);
75
+ process.exitCode = 1;
76
+ return;
77
+ }
78
+ const reportable = (data.findings || []).filter(f => selectedCode ? f.code === selectedCode : flags.all || f.reportable);
101
79
  const isJson = flags.format === 'json';
102
80
 
103
81
  if (reportable.length === 0) {
104
82
  if (isJson) {
105
- console.log(JSON.stringify({ reportable: [], message: 'no uncertain findings' }, null, 2));
83
+ console.log(JSON.stringify({ reportable: [], message: 'no matching findings; use --code <CODE> or --all to challenge confident findings' }, null, 2));
106
84
  return;
107
85
  }
108
86
  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`);
87
+ console.log(`${c.green}✅ No findings matched this selection. Confidence is not proof of correctness.${c.reset}`);
88
+ console.log(`${c.dim} Use --code <CODE> or --all to challenge a confident finding; --preview avoids saving records.${c.reset}\n`);
111
89
  return;
112
90
  }
113
91
 
@@ -115,37 +93,51 @@ export function runFeedback(projectDir, config, flags) {
115
93
  const feedbackDir = resolve(projectDir, '.docguard', 'feedback');
116
94
  const items = reportable.map((f) => {
117
95
  const id = shortId(`${f.code}|${f.location || f.message}`);
118
- const { url, title } = buildIssueUrl(f);
96
+ const { url, title, searchUrl } = buildIssueUrl(f);
119
97
  const fileName = `${(f.code || 'finding').toLowerCase()}-${id}.json`;
120
98
  const filePath = resolve(feedbackDir, fileName);
121
- return { finding: f, id, url, title, fileName, filePath };
99
+ return { finding: f, id, url, title, searchUrl, fileName, filePath, saved: false, error: null };
122
100
  });
123
101
 
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
102
  let wrote = 0;
133
- for (const it of items) {
103
+ for (const it of flags.preview ? [] : items) {
134
104
  try {
135
105
  if (!existsSync(feedbackDir)) mkdirSync(feedbackDir, { recursive: true });
136
- writeFileSync(it.filePath, JSON.stringify({
106
+ safeWrite(it.filePath, JSON.stringify({
137
107
  capturedBy: `docguard feedback (v${CLI_VERSION})`,
138
108
  finding: it.finding,
139
109
  issueUrl: it.url,
140
- }, null, 2) + '\n', 'utf-8');
110
+ searchUrl: it.searchUrl,
111
+ sharing: 'The finding record can contain private project information. Share only a reviewed synthetic reproduction.',
112
+ }, null, 2) + '\n');
113
+ it.saved = true;
141
114
  wrote++;
142
- } catch { /* best-effort local capture */ }
115
+ } catch (err) {
116
+ it.error = `Unable to save feedback record (${err.code || 'write failed'}).`;
117
+ }
143
118
  }
144
119
 
145
- if (isJson) return;
120
+ if (items.some(it => it.error)) process.exitCode = 1;
121
+
122
+ if (isJson) {
123
+ console.log(JSON.stringify({
124
+ version: CLI_VERSION,
125
+ preview: Boolean(flags.preview),
126
+ reportable: items.map(it => ({
127
+ code: it.finding.code,
128
+ location: it.finding.location,
129
+ url: it.url,
130
+ searchUrl: it.searchUrl,
131
+ file: it.saved ? `.docguard/feedback/${it.fileName}` : null,
132
+ saved: it.saved,
133
+ error: it.error,
134
+ })),
135
+ }, null, 2));
136
+ return;
137
+ }
146
138
 
147
139
  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`);
140
+ console.log(`${c.dim} ${items.length} selected finding(s). ${flags.preview ? 'Preview only; no feedback records saved.' : `${wrote} local record(s) saved to .docguard/feedback/`}\n`);
149
141
 
150
142
  for (const it of items) {
151
143
  const f = it.finding;
@@ -153,9 +145,11 @@ export function runFeedback(projectDir, config, flags) {
153
145
  if (f.suggestion && f.suggestion.pragma) {
154
146
  console.log(` ${c.dim}Suppress locally instead: ${f.suggestion.pragma}${c.reset}`);
155
147
  }
156
- console.log(` ${c.dim}Report (1 click, prefilled, redacted):${c.reset}`);
148
+ console.log(` ${c.dim}Check for duplicates (including closed work): ${it.searchUrl}${c.reset}`);
149
+ console.log(` ${c.dim}Report (review before submitting):${c.reset}`);
157
150
  console.log(` ${c.cyan}${it.url}${c.reset}`);
158
- console.log(` ${c.dim}Local copy: .docguard/feedback/${it.fileName}${c.reset}\n`);
151
+ if (it.saved) console.log(` ${c.dim}Local copy (may contain private project details): .docguard/feedback/${it.fileName}${c.reset}\n`);
152
+ if (it.error) console.error(` ${c.red}${it.error}${c.reset}`);
159
153
  }
160
154
 
161
155
  console.log(`${c.dim}These reports help DocGuard stop flagging the same false positive in a future release.${c.reset}`);
@@ -1,3 +1,4 @@
1
+ import { assertDefaultDocWrites } from '../shared-doc-roles.mjs';
1
2
  /**
2
3
  * Fix Command — The AI Orchestrator
3
4
  *
@@ -37,6 +38,7 @@ const API_DOC = 'docs-canonical/API-REFERENCE.md';
37
38
  * @returns {{ applied: boolean, removed: Array<{method,path}>, skipped?: string }}
38
39
  */
39
40
  export function applyApiSurfaceWrites(projectDir, config, { force = false } = {}) {
41
+ assertDefaultDocWrites(config);
40
42
  const drift = computeApiSurfaceDrift(projectDir, config);
41
43
  // Only spec-confirmed absences are safe to delete deterministically.
42
44
  const removable = drift.confidence === 'spec' ? drift.documentedButAbsent : [];
@@ -273,6 +275,7 @@ IMPORTANT: A new contributor should be able to follow this doc and have the proj
273
275
  * @returns {{ applied: object[], skipped: object[], total: number }}
274
276
  */
275
277
  export function applyAllMechanicalFixes(projectDir, config, opts = {}) {
278
+ assertDefaultDocWrites(config);
276
279
  const { force = false, forceRedo = false } = opts;
277
280
  const guardData = runGuardInternal(projectDir, config);
278
281
  const fixes = [];
@@ -372,6 +375,7 @@ function runWriteMode(projectDir, config, flags) {
372
375
  // ── Main Entry ─────────────────────────────────────────────────────────────
373
376
 
374
377
  export function runFix(projectDir, config, flags) {
378
+ if (flags.write) assertDefaultDocWrites(config);
375
379
  const isJson = flags.format === 'json';
376
380
  const isPrompt = flags.format === 'prompt';
377
381
  const autoFix = flags.auto || false;
@@ -1,3 +1,4 @@
1
+ import { assertDefaultDocWrites } from '../shared-doc-roles.mjs';
1
2
  /**
2
3
  * Generate Command — Reverse-engineer canonical docs from an existing codebase
3
4
  * Scans source code and creates documentation templates pre-filled with project data.
@@ -39,6 +40,7 @@ const CODE_EXTENSIONS = new Set([
39
40
  * inserted as agent-task placeholders), respecting human prose via markers.
40
41
  */
41
42
  export function runGeneratePlan(projectDir, config, flags) {
43
+ if (flags.write) assertDefaultDocWrites(config);
42
44
  // `--profile <name>` previews a profile's doc set without needing `init` first.
43
45
  if (flags.profile) config = { ...config, profile: flags.profile };
44
46
  const plan = buildMemoryPlan(projectDir, config);
@@ -137,6 +139,7 @@ export function runGeneratePlan(projectDir, config, flags) {
137
139
  }
138
140
 
139
141
  export function runGenerate(projectDir, config, flags) {
142
+ if (!flags.plan || flags.write) assertDefaultDocWrites(config);
140
143
  // --plan: emit the AI-powered "memory plan" — the agent task manifest. The CLI
141
144
  // builds the code-truth skeleton (marked sections) + tells the agent exactly
142
145
  // what prose to write per section. This is the language-aware Generate path.