docguard-cli 0.38.0 → 0.40.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.
Files changed (63) hide show
  1. package/README.md +45 -22
  2. package/cli/commands/agent.mjs +47 -1
  3. package/cli/commands/explain.mjs +16 -0
  4. package/cli/commands/feedback.mjs +147 -6
  5. package/cli/commands/fix.mjs +13 -11
  6. package/cli/commands/generate.mjs +52 -18
  7. package/cli/commands/guard.mjs +13 -2
  8. package/cli/commands/mcp.mjs +22 -2
  9. package/cli/commands/score.mjs +13 -1
  10. package/cli/commands/specs.mjs +21 -2
  11. package/cli/commands/sync.mjs +20 -7
  12. package/cli/commands/verify.mjs +65 -2
  13. package/cli/config.mjs +3 -0
  14. package/cli/docguard.mjs +48 -16
  15. package/cli/evidence/adapters.mjs +200 -0
  16. package/cli/evidence/evaluate.mjs +185 -0
  17. package/cli/evidence/manifest.mjs +194 -0
  18. package/cli/evidence/markdown.mjs +107 -0
  19. package/cli/feedback-fixture.mjs +188 -0
  20. package/cli/findings.mjs +31 -0
  21. package/cli/repository-root.mjs +159 -0
  22. package/cli/scanners/py-ast.mjs +39 -2
  23. package/cli/scanners/task-context.mjs +312 -0
  24. package/cli/shared-doc-roles.mjs +44 -1
  25. package/cli/shared-source.mjs +101 -28
  26. package/cli/validators/architecture.mjs +186 -13
  27. package/cli/validators/environment.mjs +14 -1
  28. package/cli/validators/evidence.mjs +52 -0
  29. package/cli/validators/security.mjs +5 -4
  30. package/cli/validators/todo-tracking.mjs +45 -2
  31. package/cli/writers/doc-generators.mjs +31 -17
  32. package/cli/writers/mechanical.mjs +44 -14
  33. package/cli/writers/sections.mjs +31 -3
  34. package/docs/ai-integration.md +31 -6
  35. package/docs/commands.md +43 -5
  36. package/docs/configuration.md +11 -3
  37. package/docs/quickstart.md +1 -1
  38. package/extensions/spec-kit-docguard/commands/fix.md +4 -2
  39. package/extensions/spec-kit-docguard/commands/generate.md +6 -1
  40. package/extensions/spec-kit-docguard/commands/guard.md +3 -2
  41. package/extensions/spec-kit-docguard/commands/sync.md +1 -1
  42. package/extensions/spec-kit-docguard/extension.yml +1 -1
  43. package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +14 -3
  44. package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +16 -5
  45. package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +8 -3
  46. package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +3 -2
  47. package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +6 -3
  48. package/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +2 -2
  49. package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +4 -4
  50. package/package.json +3 -1
  51. package/schemas/docguard-agent-context-benchmark.schema.json +92 -0
  52. package/schemas/docguard-agent-context-result.schema.json +95 -0
  53. package/schemas/docguard-benchmark.schema.json +84 -0
  54. package/schemas/docguard-config.schema.json +1 -0
  55. package/schemas/docguard-evidence.schema.json +169 -0
  56. package/schemas/docguard-feedback-fixture.schema.json +54 -0
  57. package/schemas/docguard-task-context.schema.json +144 -0
  58. package/templates/AGENTS.md.template +9 -4
  59. package/templates/ci/github-actions.yml +4 -4
  60. package/templates/commands/docguard.guard.md +5 -1
  61. package/templates/commands/docguard.review.md +6 -1
  62. package/templates/evidence-manifest.json +21 -0
  63. package/templates/feedback-fixture.json +18 -0
package/README.md CHANGED
@@ -77,7 +77,7 @@ graph TD
77
77
  Commands --> setup["setup wizard"]
78
78
  Commands --> other["diff · init · fix · trace · impact · sync · reconcile · retire · specs<br/>explain · memory · upgrade · agents · hooks · badge · ci · watch"]
79
79
 
80
- guard --> Validators["Validators (29)"]
80
+ guard --> Validators["Validators (30)"]
81
81
  generate --> Scanners["Scanners (4)<br/>routes · schemas · doc-tools · speckit"]
82
82
  score --> Scoring["Weighted Scoring<br/>8 categories"]
83
83
  diagnose --> Validators
@@ -104,7 +104,7 @@ graph TD
104
104
 
105
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
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.
107
+ A guard result describes the checks performed. The CDD grade measures structural maturity. Exact declarations in `.docguard-evidence.json` can verify selected statements against current local evidence; every other statement remains unverified. Coverage and unresolved claims remain visible, so teams can choose an appropriate enforcement policy.
108
108
 
109
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
110
 
@@ -275,7 +275,7 @@ DocGuard ships **23 commands** (the "Daily 5" + 18 situational tools, including
275
275
  | Command | What It Does |
276
276
  |:--------|:-------------|
277
277
  | `init` | Bootstrap a project (`--wizard` for interactive · `--with <name>` for scaffolders) |
278
- | `guard` | Validate against canonical docs — 29 validators |
278
+ | `guard` | Validate against canonical docs — 30 validators |
279
279
  | `diff` | Show gaps between docs and code (`--since <ref>` for impact mode) |
280
280
  | `sync` | Refresh code-truth doc sections — keeps memory always up to date |
281
281
  | `score` | CDD maturity score (0-100; `--diff` for delta between refs) |
@@ -290,14 +290,15 @@ DocGuard ships **23 commands** (the "Daily 5" + 18 situational tools, including
290
290
  | `fix --write` | Apply deterministic fixes (no AI — version bumps, counts, anchors, sections) |
291
291
  | `fix --history` | Audit log of every mechanical fix applied (from `.docguard/fixed.json`) |
292
292
  | `generate` | Reverse-engineer docs from existing codebase (`--plan` for AI scan) — includes auto-generated Mermaid ER diagrams from your detected schemas (Prisma/Drizzle/TypeORM/Sequelize/Django/Rails) in DATA-MODEL.md |
293
- | `agent` | One-shot agent task graph ordered, pre-filled code-truth, per-task verify (`--format json`) |
293
+ | `agent` | One-shot agent task graph, or a bounded current-evidence packet for one task (`--task <text>`, `--format json`) |
294
294
  | `explain <warning\|CODE>` | Paste any warning — or a finding code like `SEC001` — to get the validator's docstring, fix path, and how to suppress |
295
+ | `verify --evidence` | Evaluate strict statement-to-source declarations for typed JSON values, bounded collection counts, saved oasdiff JSON, and saved Buf JSON Lines. Results distinguish scoped verification, contradiction, stale inputs, inconclusive evidence, and unsupported formats. |
295
296
  | `verify --semantic` | Extract documented numbers/limits/enums (retention days, rate limits, GSI/role counts, status enums) as a task list for an agent to check against code — the semantic-drift class regex/AST can't see |
296
297
  | `verify --instructions` | Audit AGENTS.md/CLAUDE.md themselves for drift: duplicate rules, never-vs-always contradictions, stale file pointers, unknown commands — plus clustered rule pairs as agent judgment tasks |
297
- | `feedback` | Report likely false positives back to DocGuard local-first record + a 1-click prefilled, redacted GitHub issue (zero typing) |
298
+ | `feedback` | Review any finding or a synthetic false-positive/false-negative/unsupported fixture; verify its opposite control, reduce it deterministically, search open and closed duplicates, and optionally emit a test-only contribution. Nothing is submitted automatically. |
298
299
  | `retire` | Find completed or superseded planning material (`--plan`/`--check`; `--fail-on-warning` gates advisory candidates) and explicitly remove clean tracked documentation from active AI context. `.docguard-archive.json` records recovery metadata and retired requirement identities, and `--retention-ref` proves the source revision remains reachable. This is separate from the Spec Kit Archive extension, which consolidates feature documents. |
299
300
  | `reconcile` | Build a read-only code↔spec review graph since a Git ref. Classifies mechanical facts, approved intent, decisions, unrelated changes, and unsupported evidence; `--write` applies only mechanical generated-section refreshes. |
300
- | `specs` | Maintain the versioned spec registry, preflight new specs, and apply evidence-gated completion transactions with bounded outcomes and active-context regeneration. |
301
+ | `specs` | Maintain the versioned spec registry, preflight new specs, and apply evidence-gated completion transactions with bounded outcomes and active-context regeneration. Verified living specs can record later reviewed maintenance without reopening or duplicating the specification. |
301
302
  | `specs --check` / `specs --write` | Validate or refresh `.docguard-specs.json`, the byte-stable index of immutable spec IDs, reviewed lifecycle/lineage/scope, artifact digests, task state, explicitly scoped test evidence, and archive tombstones. Refreshes preserve the reviewed block. |
302
303
  | `specs preflight [--path <spec>]` | Before specification, print current spec lifecycle and evidence. Before planning, check the generated draft for structural blockers and report semantic overlap as review-only evidence. |
303
304
  | `mcp` | MCP server — exposes guard/score/explain/verify/report/diagnose as native tools for Claude, Cursor, and any MCP client. Stdio: `claude mcp add docguard -- npx docguard-cli mcp`. Team-shared HTTP: `docguard mcp --transport http --port 8585` (loopback by default; non-loopback binds require `--api-key`) |
@@ -326,16 +327,23 @@ DocGuard ships **23 commands** (the "Daily 5" + 18 situational tools, including
326
327
 
327
328
  Run them solo (`docguard init --with hooks`) or stacked (`docguard init --with agents,hooks,badge,ci`).
328
329
 
330
+ To declare an exact fact, copy `templates/evidence-manifest.json` to
331
+ `.docguard-evidence.json`, point its literal Markdown template at one unique
332
+ statement, and bind that value to a supported local source. Run
333
+ `docguard verify --evidence --format json` before enabling the guard in CI.
334
+ External compatibility declarations consume saved oasdiff or Buf output and
335
+ require current SHA-256 identities for every declared repository input.
336
+
329
337
  **Deprecation aliases** — `setup` · `agents` · `hooks` · `badge` · `llms` · `publish` · `impact` remain compatible until v1.0 with a yellow stderr warning. `audit → guard` is permanent and silent; `ci` is a current first-class pipeline command.
330
338
 
331
339
  ### CLI Flags
332
340
 
333
341
  | Flag | Description | Commands |
334
342
  |:-----|:------------|:---------|
335
- | `--dir <path>` | Project directory (default: `.`) | All |
343
+ | `--dir <path>` | Project directory (default: `.`); explicit selection suppresses ancestor-root guidance | All |
336
344
  | `--verbose` | Show detailed output | All |
337
345
  | `--quiet` / `-q` | Suppress banner — for hooks, CI loops, scripts | All |
338
- | `--format json` | Machine-readable output (clean JSON, no ANSI bleed) | guard, score, diff, trace, diagnose, memory, impact, explain, reconcile, retire, specs |
346
+ | `--format json` | Machine-readable output (clean JSON, no ANSI bleed) | guard, score, diff, trace, diagnose, memory, impact, explain, verify, reconcile, retire, specs |
339
347
  | `--format sarif` | SARIF 2.1.0 output — findings as rules/results for GitHub Code Scanning and SARIF dashboards | guard |
340
348
  | `--format junit` | JUnit XML output — one testcase per validator, for GitLab CI (`artifacts:reports:junit`), Jenkins, Azure DevOps, CircleCI | guard |
341
349
  | `--update-baseline` | Adopt DocGuard on a legacy repo without a red day one: freeze today's findings into a committed `.docguard.baseline.json`; guard/ci then gate only NEW drift. Suppression is always visible ("N pre-existing finding(s) suppressed"), and `--no-baseline` shows the full picture | guard |
@@ -347,7 +355,7 @@ Run them solo (`docguard init --with hooks`) or stacked (`docguard init --with a
347
355
  | `--force-redo` | Bypass ping-pong suppression in `.docguard/fixed.json` | fix --write |
348
356
  | `--profile <name>` | Starter / standard / enterprise | init |
349
357
  | `--no-spec-kit` | Skip auto-init of `.specify/` / `.agent/` scaffolding | init |
350
- | `--changed-only [--since <ref>]` | Pre-commit lite mode (5 fast validators on changed files only) | guard |
358
+ | `--changed-only [--since <ref>]` | Pre-commit lite mode (6 fast validators on changed files only) | guard |
351
359
  | `--timings` | Per-validator wall-time profile (slowest first) | guard |
352
360
  | `--show-failing` | Show warnings/errors even when status is PASS | guard |
353
361
  | `--pin` | Record running CLI version into `.docguard.json` (reproducibility) | guard |
@@ -361,6 +369,15 @@ Run them solo (`docguard init --with hooks`) or stacked (`docguard init --with a
361
369
  | `--transport http` `--port` `--host` `--api-key` `--path` | Serve MCP over Streamable HTTP instead of stdio (team-shared server; loopback-only unless an api-key is set) | mcp |
362
370
  | `--history` | Show fix audit log | fix |
363
371
 
372
+ When run from a nested package without `--dir`, DocGuard checks only that
373
+ selected directory. If a bounded ancestor scan finds a `.docguard.json` or an
374
+ npm/pnpm workspace declaration that owns the package, stderr shows an exact
375
+ repository-scope rerun command. DocGuard never changes scope automatically. JSON,
376
+ SARIF, and JUnit stdout remain valid; machine runs receive one typed
377
+ `docguard.repository-root-guidance` JSON diagnostic on stderr. A local config,
378
+ an explicit `--dir`, an unmatched workspace, or a nested Git boundary suppresses
379
+ the suggestion.
380
+
364
381
  ### Example Output
365
382
 
366
383
  ```
@@ -394,7 +411,7 @@ $ npx docguard-cli generate
394
411
 
395
412
  ## 🔍 Validators
396
413
 
397
- DocGuard runs **29 automated validators** on every `guard` check. Source-facing validators are language-aware where their evidence model applies; repository and document validators operate independently of source language.
414
+ DocGuard runs **30 automated validators** on every `guard` check. Source-facing validators are language-aware where their evidence model applies; repository and document validators operate independently of source language.
398
415
 
399
416
  | # | Validator | What It Checks | Default |
400
417
  |:--|:----------|:--------------|:--------|
@@ -419,14 +436,15 @@ DocGuard runs **29 automated validators** on every `guard` check. Source-facing
419
436
  | 19 | **Spec-Kit** | Spec quality validation (FR-IDs, mandatory sections, phased tasks) | ✅ On |
420
437
  | 20 | **Document-Lifecycle** | Exact terminal states, advisory completion signals, incomplete coverage, and manifest/working-tree inconsistencies | ✅ On |
421
438
  | 21 | **Spec-Registry** | Immutable spec identities, byte-stable evidence projection, reviewed lifecycle preservation, and archive/storage consistency | ✅ On |
422
- | 22 | **Cross-Reference** | Internal markdown links + anchors resolve (with "did you mean?" hints); Obsidian wikilinks validated when the repo uses them as file links (`.obsidian` present or a target resolves) | ✅ On |
423
- | 23 | **Generated-Staleness** | `source=code` sections match scanner output; `status: draft` doc age | ✅ On |
424
- | 24 | **Canonical-Sync** | DocGuard's own README count claims match code-truth (DocGuard repo only N/A elsewhere) | ✅ On |
425
- | 25 | **Metrics-Consistency** | Hardcoded numbers match actual counts | ✅ On |
426
- | 26 | **Surface-Sync** | Item-level enumerable drift — names in doc tables/lists (commands, checks, etc.) match code-truth (opt-in via `surfaceSync.surfaces`; N/A unless configured) | ✅ On |
427
- | 27 | **Diff-Suspicion** | Change-driven: a doc/agent-instruction file that references code changed since the ref AND shares removed domain symbols is flagged for review (arXiv 2010.01625, F1 74.7) | ✅ On |
428
- | 28 | **Reference-Existence** | Two-revision check: a backticked code symbol present when the doc was last updated but gone at HEAD is flagged as outdated (arXiv 2212.01479) | ✅ On |
429
- | 29 | **API-Doc-Smells** | Bloated (≥300 words) / Lazy (≤6 prose words) API documentation units, keyed on signature-headed sections (F1 0.90/0.95) | ✅ On |
439
+ | 22 | **Evidence** | Exact declared Markdown statements match current typed JSON, bounded collections, or saved compatibility reports; unsupported and missing evidence stays visible | ✅ On |
440
+ | 23 | **Cross-Reference** | Internal markdown links + anchors resolve (with "did you mean?" hints); Obsidian wikilinks validated when the repo uses them as file links (`.obsidian` present or a target resolves) | ✅ On |
441
+ | 24 | **Generated-Staleness** | `source=code` sections match scanner output; `status: draft` doc age | ✅ On |
442
+ | 25 | **Canonical-Sync** | DocGuard's own README count claims match code-truth (DocGuard repo only — N/A elsewhere) | ✅ On |
443
+ | 26 | **Metrics-Consistency** | Hardcoded numbers match actual counts | ✅ On |
444
+ | 27 | **Surface-Sync** | Item-level enumerable drift names in doc tables/lists (commands, checks, etc.) match code-truth (opt-in via `surfaceSync.surfaces`; N/A unless configured) | ✅ On |
445
+ | 28 | **Diff-Suspicion** | Change-driven: a doc/agent-instruction file that references code changed since the ref AND shares removed domain symbols is flagged for review (arXiv 2010.01625, F1 74.7) | ✅ On |
446
+ | 29 | **Reference-Existence** | Two-revision check: a backticked code symbol present when the doc was last updated but gone at HEAD is flagged as outdated (arXiv 2212.01479) | ✅ On |
447
+ | 30 | **API-Doc-Smells** | Bloated (≥300 words) / Lazy (≤6 prose words) API documentation units, keyed on signature-headed sections (F1 0.90/0.95) | ✅ On |
430
448
 
431
449
  **Per-validator controls** (in `.docguard.json`):
432
450
  ```json
@@ -517,7 +535,7 @@ DocGuard provides AI agent slash commands for integrated workflows. Installed au
517
535
  | Command | What It Does |
518
536
  |:--------|:-------------|
519
537
  | `/docguard.init` | Initialize Canonical-Driven Development in a new or existing project |
520
- | `/docguard.guard` | Run quality validation — check all 29 validators |
538
+ | `/docguard.guard` | Run quality validation — check all 30 validators |
521
539
  | `/docguard.review` | Analyze doc quality and suggest improvements |
522
540
  | `/docguard.fix` | Generate targeted fix prompts for specific issues |
523
541
  | `/docguard.update` | Update canonical docs after code changes — detect drift and sync documentation |
@@ -703,11 +721,16 @@ Highlights of the current line (v0.29 → v0.33):
703
721
  agent tools: `claude mcp add docguard -- npx docguard-cli mcp`.
704
722
  - **Agent-file family sync** — `agents --sync` treats AGENTS.md as canonical and regenerates
705
723
  CLAUDE.md / `.cursor/rules` / Copilot / Gemini variants with drift-proof source-hash markers.
706
- - **`verify --semantic` and `verify --instructions`** — extract documented numbers/limits/enums
707
- as agent verification tasks; audit the agent-instruction files themselves for contradictions
708
- and stale pointers.
724
+ - **`verify --evidence`, `verify --semantic`, and `verify --instructions`** — check exact local
725
+ evidence declarations first, extract remaining numbers/limits/enums as agent tasks, and audit
726
+ agent-instruction files for contradictions and stale pointers.
709
727
  - **`docguard agent`** — one-shot ordered task graph with pre-filled code-truth, collapsing ~10
710
728
  agent round-trips into one call.
729
+ - **`docguard agent --task <text>`** — opt-in task context from approved current
730
+ specs and canonical docs, with hashed excerpts, source/test pointers, strict
731
+ budgets, and honest abstention. The frozen 27-run evaluation preserved every
732
+ tested behavior and cut median steps by 50% and latency by 17% versus the
733
+ context pack, while using 80% more uncached input tokens.
711
734
 
712
735
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
713
736
 
@@ -1,4 +1,6 @@
1
1
  /**
2
+ * @implements docguard.evidence-scoped-verification#FR-012
3
+ * @implements docguard.task-specific-agent-context#FR-010
2
4
  * `docguard agent` — the one-shot agent task graph.
3
5
  *
4
6
  * Field report §2: an LLM told "run docguard and fix the docs" had to drive ~10
@@ -25,6 +27,8 @@ import { buildMemoryPlan } from '../scanners/memory-plan.mjs';
25
27
  import { c } from '../shared.mjs';
26
28
  import { createEvidenceReader, citedSources, taskEvidence, gitEvidence, SEMANTIC_COVERAGE_LIMITATION } from '../scanners/semantic-claims.mjs';
27
29
  import { buildScoreAssurance } from './score.mjs';
30
+ import { evaluateEvidence } from '../evidence/evaluate.mjs';
31
+ import { buildTaskContextPacket } from '../scanners/task-context.mjs';
28
32
 
29
33
  const PHASES = ['config', 'canonical-docs', 'verify'];
30
34
 
@@ -89,7 +93,7 @@ export function buildAgentTaskGraph(projectDir, config, plan) {
89
93
  phase: 'verify',
90
94
  file: null,
91
95
  kind: 'verify',
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.',
96
+ 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 verify --evidence` for exact declared checks and `docguard verify --semantic` for remaining heuristic claim tasks. Run `docguard score` for structural maturity. Neither guard nor score verifies prose; scoped evidence verifies only its selected statements and does not establish whole-document factual accuracy.',
93
97
  prefilled: null,
94
98
  grounding: null,
95
99
  acceptance: { verify: 'docguard guard --format json', expect: '0 errors', warnings: 'triage-and-report', scope: 'structural-only', factualAccuracy: 'unknown' },
@@ -105,11 +109,13 @@ export function buildAgentTaskGraph(projectDir, config, plan) {
105
109
 
106
110
  const assurance = buildScoreAssurance(projectDir, config);
107
111
  assurance.limitation += ` ${SEMANTIC_COVERAGE_LIMITATION}`;
112
+ const evidence = evaluateEvidence(projectDir, config);
108
113
 
109
114
  return {
110
115
  project: config.projectName,
111
116
  provenance: { kind: 'snapshot', git: gitEvidence(projectDir) },
112
117
  assurance,
118
+ evidence,
113
119
  profile: { name: profileName, kind: plan.profile.kind, languages: plan.profile.languages, frameworks: plan.profile.frameworks },
114
120
  order: PHASES,
115
121
  counts: {
@@ -123,6 +129,46 @@ export function buildAgentTaskGraph(projectDir, config, plan) {
123
129
  }
124
130
 
125
131
  export function runAgent(projectDir, config, flags) {
132
+ if (flags.task !== undefined) {
133
+ let packet;
134
+ try {
135
+ packet = buildTaskContextPacket(projectDir, config, flags.task);
136
+ } catch (error) {
137
+ const message = error instanceof Error ? error.message : 'Task context input is invalid.';
138
+ if (flags.format === 'json') console.log(JSON.stringify({ status: 'error', code: 'TASK_CONTEXT_INPUT', message }, null, 2));
139
+ else console.error(`${c.red}Error:${c.reset} ${message}`);
140
+ process.exitCode = 1;
141
+ return null;
142
+ }
143
+ if (flags.format === 'json') {
144
+ console.log(JSON.stringify(packet, null, 2));
145
+ return packet;
146
+ }
147
+
148
+ console.log(`${c.bold}🎯 DocGuard Task Context — ${config.projectName}${c.reset}`);
149
+ console.log(`${c.dim} ${packet.selection.status} · ${packet.selection.selectedExcerpts} excerpt(s) · task ${packet.task.digest}${c.reset}\n`);
150
+ console.log(` ${c.dim}Retrieval only · factual accuracy remains unknown · verify before editing.${c.reset}\n`);
151
+ if (packet.selection.status === 'abstained') {
152
+ console.log(` ${c.yellow}No evidence met the relevance threshold. Use the navigation map for normal repository discovery.${c.reset}\n`);
153
+ }
154
+ for (const excerpt of packet.excerpts) {
155
+ console.log(` ${c.bold}${excerpt.path}:${excerpt.startLine}-${excerpt.endLine}${c.reset} ${c.dim}[${excerpt.kind} · score ${excerpt.score}]${c.reset}`);
156
+ for (const line of excerpt.content.split('\n')) console.log(` ${line}`);
157
+ console.log('');
158
+ }
159
+ if (packet.pointers.length) {
160
+ console.log(` ${c.bold}Evidence pointers${c.reset}`);
161
+ for (const pointer of packet.pointers) console.log(` - ${pointer.path} (${pointer.kind}: ${pointer.reasons.join(', ')})`);
162
+ console.log('');
163
+ }
164
+ console.log(` ${c.bold}Verify${c.reset}`);
165
+ for (const item of packet.verification) console.log(` - ${item.command} — ${item.purpose}`);
166
+ console.log(`\n ${c.bold}Limits${c.reset}`);
167
+ for (const limitation of packet.limitations) console.log(` - ${limitation}`);
168
+ console.log(`\n ${c.dim}Run with --format json for hashes, machine-readable reasons, assurance, and navigation.${c.reset}`);
169
+ return packet;
170
+ }
171
+
126
172
  // Allow `--profile <name>` to preview a profile's plan without having to run
127
173
  // `init` first (the field-report agent had no config yet on its first call).
128
174
  const cfg = flags.profile ? { ...config, profile: flags.profile } : config;
@@ -1,4 +1,5 @@
1
1
  /**
2
+ * @implements docguard.evidence-scoped-verification#FR-013
2
3
  * Explain Command — v0.16-P6.
3
4
  *
4
5
  * Asked for by a user who'd spent 5-10 minutes per warning spelunking
@@ -278,6 +279,20 @@ const EXPLAINERS = {
278
279
  example: '`docguard specs preflight --path specs/007-feature/spec.md` blocks reused IDs and reports prior lifecycle/evidence before planning.',
279
280
  standard: 'Canonical-Driven Development lifecycle registry contract',
280
281
  },
282
+ evidence: {
283
+ title: 'Evidence — declared statements match bounded local sources',
284
+ what: 'Evaluates opt-in declarations in `.docguard-evidence.json`. Each declaration selects one exact statement under one Markdown heading, reads a bounded local JSON value, file collection, saved oasdiff report, or saved Buf report, and reports verified-within-scope, contradicted, stale, inconclusive, or unsupported.',
285
+ why: 'Structural checks can prove that documentation exists and is connected, but they cannot prove factual claims. Evidence declarations add reproducible checks for selected high-value statements without claiming that the whole document is accurate.',
286
+ triggers: [
287
+ ['Evidence manifest is invalid', 'Validate `.docguard-evidence.json` against `schemas/docguard-evidence.schema.json`; unknown fields and ambiguous declarations fail closed.'],
288
+ ['Declared evidence contradicts', 'Inspect the selected statement and source. Update the implementation when the canonical statement is still intended, or revise the statement through the normal reviewed documentation workflow.'],
289
+ ['Declared evidence is stale', 'Regenerate the saved external-tool report from the declared inputs and update their SHA-256 digests.'],
290
+ ['Declared evidence is inconclusive', 'Repair the missing or ambiguous heading/statement, unreadable source, unsafe collection, or malformed saved report before relying on the declaration.'],
291
+ ['Declared evidence uses an unsupported', 'Use a documented adapter version and command, or upgrade DocGuard when support for that producer shape becomes available.'],
292
+ ],
293
+ example: 'A retention statement under `## Policy` declares JSON Pointer `/retentionDays`; `docguard verify --evidence` reports verified-within-scope only when the exact typed values match.',
294
+ standard: 'RFC 6901 JSON Pointer; JSON Schema 2020-12; DocGuard evidence-scoping contract',
295
+ },
281
296
 
282
297
  // ── Backfilled in v0.24 (field report, Issue A) ─────────────────────────
283
298
  // These validators were registered in guard but had no explain entry, so
@@ -410,6 +425,7 @@ const DISPLAY_NAMES = {
410
425
  surfaceSync: 'Surface-Sync',
411
426
  canonicalSync: 'Canonical-Sync',
412
427
  metricsConsistency: 'Metrics-Consistency',
428
+ evidence: 'Evidence',
413
429
  diffSuspicion: 'Diff-Suspicion',
414
430
  referenceExistence: 'Reference-Existence',
415
431
  apiDocSmells: 'API-Doc-Smells',
@@ -12,15 +12,23 @@
12
12
  *
13
13
  * Nothing is transmitted automatically. This command never scaffolds skills
14
14
  * or edits source files. Zero npm dependencies — pure Node.js built-ins.
15
+ * @implements docguard.precision-evidence-loop#FR-012
16
+ * @implements docguard.precision-evidence-loop#FR-015
17
+ * @implements docguard.precision-evidence-loop#FR-016
15
18
  */
16
19
 
17
- import { existsSync, mkdirSync, readFileSync } from 'node:fs';
18
- import { resolve, dirname } from 'node:path';
20
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
21
+ import { resolve, dirname, join, relative, sep } from 'node:path';
22
+ import { tmpdir } from 'node:os';
19
23
  import { fileURLToPath } from 'node:url';
20
24
  import { c } from '../shared.mjs';
21
25
  import { CODES } from '../findings.mjs';
22
26
  import { safeWrite } from '../writers/generate-io.mjs';
23
27
  import { runGuardInternal } from './guard.mjs';
28
+ import {
29
+ buildTestOnlyContribution, feedbackFindingIdentity, feedbackSearchUrls, parseFeedbackFixture,
30
+ reduceFixtureDeterministically,
31
+ } from '../feedback-fixture.mjs';
24
32
 
25
33
  const _PKG = JSON.parse(
26
34
  readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json'), 'utf-8')
@@ -39,16 +47,20 @@ function shortId(str) {
39
47
  }
40
48
 
41
49
  /** Shared output deliberately excludes every source-derived string. */
42
- export function buildIssueUrl(finding) {
50
+ export function buildIssueUrl(finding, context = null) {
43
51
  const code = Object.hasOwn(CODES, finding.code || '') ? finding.code : 'FINDING';
44
52
  const validator = CODES[code]?.validator || 'unknown';
45
- const title = `[feedback] ${code} (${validator}): detection feedback`;
53
+ const classification = context?.classification || 'false_positive';
54
+ const title = `[feedback] ${code} (${validator}): ${classification.replaceAll('_', ' ')}`;
46
55
  const confidence = ['high', 'medium', 'low'].includes(finding.confidence) ? finding.confidence : 'unknown';
47
56
  const body = [
48
57
  `DocGuard v${CLI_VERSION}`,
49
58
  `- Code: ${code}`,
50
59
  `- Validator: ${validator}`,
51
60
  `- Confidence: ${confidence}`,
61
+ `- Classification: ${classification}`,
62
+ ...(context?.parserTier ? [`- Parser tier: ${context.parserTier}`] : []),
63
+ ...(context?.duplicateIdentity ? [`- Duplicate identity: ${context.duplicateIdentity}`] : []),
52
64
  '',
53
65
  'Expected behavior:',
54
66
  'Actual behavior:',
@@ -60,12 +72,139 @@ export function buildIssueUrl(finding) {
60
72
  'Generated by docguard feedback. No project paths, messages, source code, or secret values are included.',
61
73
  ].join('\n');
62
74
  const url = `${ISSUES_BASE}/new?labels=docguard-feedback&title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`;
63
- const query = `repo:raccioly/docguard ${code}`;
75
+ const query = context?.duplicateIdentity ? `repo:raccioly/docguard "${context.duplicateIdentity}"` : `repo:raccioly/docguard ${code}`;
64
76
  const searchUrl = `https://github.com/search?q=${encodeURIComponent(query)}&type=issues`;
65
77
  return { url: url.length <= URL_CAP ? url : `${ISSUES_BASE}/new`, title, searchUrl };
66
78
  }
67
79
 
80
+ const FEEDBACK_CLASSES = new Set(['false_positive', 'false_negative', 'unsupported_syntax', 'ambiguous', 'policy_disagreement']);
81
+
82
+ function projectFile(projectDir, value, label) {
83
+ if (typeof value !== 'string' || !value || value.includes('\\')) throw new Error(`${label} must be a project-relative path.`);
84
+ const root = resolve(projectDir);
85
+ const path = resolve(root, value);
86
+ const rel = relative(root, path);
87
+ if (!rel || rel.startsWith(`..${sep}`) || rel === '..' || rel.split(sep).some(part => ['.git', '.local'].includes(part.toLowerCase()))) {
88
+ throw new Error(`${label} must remain in the project and outside protected directories.`);
89
+ }
90
+ return path;
91
+ }
92
+
93
+ function evaluateFileSet(manifest, fileSet) {
94
+ const root = mkdtempSync(join(tmpdir(), 'docguard-feedback-fixture-'));
95
+ try {
96
+ for (const file of fileSet.files) {
97
+ const target = resolve(root, file.path);
98
+ mkdirSync(dirname(target), { recursive: true });
99
+ writeFileSync(target, file.content, 'utf8');
100
+ }
101
+ const report = runGuardInternal(root, manifest.config);
102
+ const validator = (report.validators || []).find(item => item.key === manifest.detector.validator);
103
+ return {
104
+ identities: (report.findings || []).filter(item => item.code === manifest.detector.code).map(feedbackFindingIdentity).sort(),
105
+ applicability: validator?.applicability?.status || 'unknown',
106
+ };
107
+ } finally {
108
+ rmSync(root, { recursive: true, force: true });
109
+ }
110
+ }
111
+
112
+ function predicateMatches(manifest, result) {
113
+ if (manifest.interestingness.predicate === 'finding_present') return result.identities.includes(manifest.expectedIdentity);
114
+ if (manifest.interestingness.predicate === 'finding_absent') return !result.identities.includes(manifest.expectedIdentity);
115
+ return result.applicability === 'unsupported';
116
+ }
117
+
118
+ function oppositeMatches(manifest, result) {
119
+ if (manifest.interestingness.predicate === 'finding_present') return result.identities.includes(manifest.expectedIdentity);
120
+ if (manifest.interestingness.predicate === 'finding_absent') return !result.identities.includes(manifest.expectedIdentity);
121
+ return result.applicability === 'checked';
122
+ }
123
+
124
+ function runFixtureFeedback(projectDir, flags) {
125
+ let manifest;
126
+ try {
127
+ const manifestPath = projectFile(projectDir, flags.fixtureManifest, '--fixture-manifest');
128
+ if (!existsSync(manifestPath) || lstatSync(manifestPath).isSymbolicLink()) throw new Error('Fixture manifest must be a regular non-symlink file.');
129
+ const text = readFileSync(manifestPath, 'utf8');
130
+ if (Buffer.byteLength(text) > 262_144) throw new Error('Fixture manifest exceeds 256 KiB.');
131
+ manifest = parseFeedbackFixture(JSON.parse(text));
132
+ if (flags.classification && flags.classification !== manifest.classification) throw new Error('--classification disagrees with the fixture manifest.');
133
+ } catch (error) {
134
+ console.log(JSON.stringify({ status: 'ERROR', error: error.message }, null, 2));
135
+ process.exitCode = 1;
136
+ return;
137
+ }
138
+
139
+ const initial = evaluateFileSet(manifest, manifest.fixture);
140
+ const control = evaluateFileSet(manifest, manifest.oppositeControl);
141
+ const reproduced = predicateMatches(manifest, initial);
142
+ const controlConfirmed = oppositeMatches(manifest, control);
143
+ if (!reproduced || !controlConfirmed) {
144
+ console.log(JSON.stringify({
145
+ status: 'NOT_REPRODUCED', classification: manifest.classification,
146
+ reproductionConfirmed: reproduced, controlConfirmed,
147
+ message: 'The explicit predicate and opposite control must both reproduce before reduction or contribution.',
148
+ }, null, 2));
149
+ process.exitCode = 1;
150
+ return;
151
+ }
152
+
153
+ let reduction = null;
154
+ if (flags.reduce) {
155
+ reduction = reduceFixtureDeterministically(manifest, candidate => predicateMatches(candidate, evaluateFileSet(candidate, candidate.fixture)));
156
+ manifest = reduction.manifest;
157
+ }
158
+ const searches = feedbackSearchUrls(manifest, ISSUES_BASE);
159
+ const issue = buildIssueUrl({ code: manifest.detector.code, confidence: 'unknown' }, {
160
+ classification: manifest.classification, parserTier: manifest.parserTier, duplicateIdentity: searches.identity,
161
+ });
162
+ const feedbackDir = resolve(projectDir, '.docguard', 'feedback');
163
+ let record = null;
164
+ let contribution = null;
165
+ let contributionPreview = null;
166
+ try {
167
+ if (!flags.preview) {
168
+ record = resolve(feedbackDir, `${searches.identity}.fixture.json`);
169
+ safeWrite(record, `${JSON.stringify(manifest, null, 2)}\n`);
170
+ }
171
+ if (flags.contribution) {
172
+ if (!/^tests\/[^/]+\.test\.mjs$/.test(flags.contribution)) {
173
+ throw new Error('--contribution must be a direct tests/<name>.test.mjs path.');
174
+ }
175
+ contributionPreview = buildTestOnlyContribution(manifest);
176
+ if (!flags.preview) {
177
+ contribution = projectFile(projectDir, flags.contribution, '--contribution');
178
+ safeWrite(contribution, contributionPreview);
179
+ }
180
+ }
181
+ } catch (error) {
182
+ console.log(JSON.stringify({ status: 'ERROR', error: error.message }, null, 2));
183
+ process.exitCode = 1;
184
+ return;
185
+ }
186
+ const output = {
187
+ status: 'READY', preview: Boolean(flags.preview), classification: manifest.classification,
188
+ reproductionConfirmed: true, controlConfirmed: true, reduction: reduction ? { status: reduction.status, attempts: reduction.attempts } : null,
189
+ duplicateIdentity: searches.identity, searchUrls: searches, issueUrl: issue.url,
190
+ record: record ? relative(projectDir, record).split(sep).join('/') : null,
191
+ contribution: contribution ? relative(projectDir, contribution).split(sep).join('/') : null,
192
+ contributionPreview: flags.preview && flags.contribution ? contributionPreview : undefined,
193
+ };
194
+ console.log(JSON.stringify(output, null, 2));
195
+ }
196
+
68
197
  export function runFeedback(projectDir, config, flags) {
198
+ if (flags.fixtureManifest) return runFixtureFeedback(projectDir, flags);
199
+ const classification = flags.classification || 'false_positive';
200
+ if (!FEEDBACK_CLASSES.has(classification) || classification === 'false_negative' || classification === 'unsupported_syntax') {
201
+ const error = classification === 'false_negative' || classification === 'unsupported_syntax'
202
+ ? 'This classification requires --fixture-manifest with an explicit expected identity and opposite control.'
203
+ : 'Unknown feedback classification.';
204
+ if (flags.format === 'json') console.log(JSON.stringify({ error, reportable: [] })); else console.error(error);
205
+ process.exitCode = 1;
206
+ return;
207
+ }
69
208
  const data = runGuardInternal(projectDir, config);
70
209
  const selectedCode = typeof flags.code === 'string' ? flags.code.toUpperCase() : null;
71
210
  if (flags.code !== undefined && (!selectedCode || !Object.hasOwn(CODES, selectedCode))) {
@@ -93,7 +232,7 @@ export function runFeedback(projectDir, config, flags) {
93
232
  const feedbackDir = resolve(projectDir, '.docguard', 'feedback');
94
233
  const items = reportable.map((f) => {
95
234
  const id = shortId(`${f.code}|${f.location || f.message}`);
96
- const { url, title, searchUrl } = buildIssueUrl(f);
235
+ const { url, title, searchUrl } = buildIssueUrl(f, { classification });
97
236
  const fileName = `${(f.code || 'finding').toLowerCase()}-${id}.json`;
98
237
  const filePath = resolve(feedbackDir, fileName);
99
238
  return { finding: f, id, url, title, searchUrl, fileName, filePath, saved: false, error: null };
@@ -105,6 +244,7 @@ export function runFeedback(projectDir, config, flags) {
105
244
  if (!existsSync(feedbackDir)) mkdirSync(feedbackDir, { recursive: true });
106
245
  safeWrite(it.filePath, JSON.stringify({
107
246
  capturedBy: `docguard feedback (v${CLI_VERSION})`,
247
+ classification,
108
248
  finding: it.finding,
109
249
  issueUrl: it.url,
110
250
  searchUrl: it.searchUrl,
@@ -123,6 +263,7 @@ export function runFeedback(projectDir, config, flags) {
123
263
  console.log(JSON.stringify({
124
264
  version: CLI_VERSION,
125
265
  preview: Boolean(flags.preview),
266
+ classification,
126
267
  reportable: items.map(it => ({
127
268
  code: it.finding.code,
128
269
  location: it.finding.location,
@@ -1,4 +1,8 @@
1
- import { assertDefaultDocWrites } from '../shared-doc-roles.mjs';
1
+ /**
2
+ * @implements docguard.language-repository-coverage#FR-010
3
+ * @implements docguard.language-repository-coverage#FR-011
4
+ */
5
+ import { assertMappedFullDocumentWrites, docRolePath, isMappedDocPath, resolveDocRole } from '../shared-doc-roles.mjs';
2
6
  /**
3
7
  * Fix Command — The AI Orchestrator
4
8
  *
@@ -14,7 +18,7 @@ import { assertDefaultDocWrites } from '../shared-doc-roles.mjs';
14
18
  * --auto Create skeleton files (NOT content) via init
15
19
  */
16
20
 
17
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
21
+ import { existsSync, readFileSync, mkdirSync } from 'node:fs';
18
22
  import { resolve, basename, dirname } from 'node:path';
19
23
  import { execSync, execFileSync } from 'node:child_process';
20
24
  import { fileURLToPath } from 'node:url';
@@ -23,10 +27,9 @@ import { computeApiSurfaceDrift } from '../validators/api-surface.mjs';
23
27
  import { removeEndpoints, hasGeneratedMarker } from '../writers/api-reference.mjs';
24
28
  import { applyMechanicalFixes } from '../writers/mechanical.mjs';
25
29
  import { loadFixMemory } from '../writers/fix-memory.mjs';
30
+ import { safeWrite } from '../writers/generate-io.mjs';
26
31
  import { runGuardInternal } from './guard.mjs';
27
32
 
28
- const API_DOC = 'docs-canonical/API-REFERENCE.md';
29
-
30
33
  /**
31
34
  * Apply DETERMINISTIC, no-LLM API-surface fixes: remove endpoints documented in
32
35
  * API-REFERENCE.md that the OpenAPI spec confirms no longer exist. Removes the
@@ -38,13 +41,13 @@ const API_DOC = 'docs-canonical/API-REFERENCE.md';
38
41
  * @returns {{ applied: boolean, removed: Array<{method,path}>, skipped?: string }}
39
42
  */
40
43
  export function applyApiSurfaceWrites(projectDir, config, { force = false } = {}) {
41
- assertDefaultDocWrites(config);
42
44
  const drift = computeApiSurfaceDrift(projectDir, config);
43
45
  // Only spec-confirmed absences are safe to delete deterministically.
44
46
  const removable = drift.confidence === 'spec' ? drift.documentedButAbsent : [];
45
47
  if (removable.length === 0) return { applied: false, removed: [] };
46
48
 
47
- const apiDocPath = resolve(projectDir, API_DOC);
49
+ const apiDoc = docRolePath(config, 'apiReference');
50
+ const apiDocPath = resolveDocRole(projectDir, config, 'apiReference');
48
51
  if (!existsSync(apiDocPath)) return { applied: false, removed: [] };
49
52
 
50
53
  const content = readFileSync(apiDocPath, 'utf-8');
@@ -52,17 +55,18 @@ export function applyApiSurfaceWrites(projectDir, config, { force = false } = {}
52
55
  return {
53
56
  applied: false,
54
57
  removed: [],
55
- skipped: `${API_DOC} is not marked '<!-- docguard:generated true -->'. ` +
58
+ skipped: `${apiDoc} is not marked '<!-- docguard:generated true -->'. ` +
56
59
  `Re-run with --force to edit it, or fix it via an AI agent (/docguard.fix --doc api-reference).`,
57
60
  };
58
61
  }
62
+ if (isMappedDocPath(config, apiDoc)) assertMappedFullDocumentWrites(projectDir, config, ['apiReference']);
59
63
 
60
64
  const { content: newContent, removed } = removeEndpoints(content, removable);
61
65
  if (removed.length === 0 || newContent === content) {
62
66
  return { applied: false, removed: [] }; // idempotent no-op
63
67
  }
64
68
 
65
- writeFileSync(apiDocPath, newContent, 'utf-8');
69
+ safeWrite(apiDocPath, newContent);
66
70
  // Map removed keys back to {method,path} for reporting.
67
71
  const removedEndpoints = removable.filter(e => removed.includes(`${e.method.toUpperCase()} ${normalizeForKey(e.path)}`));
68
72
  return { applied: true, removed: removedEndpoints.length ? removedEndpoints : removable };
@@ -275,7 +279,6 @@ IMPORTANT: A new contributor should be able to follow this doc and have the proj
275
279
  * @returns {{ applied: object[], skipped: object[], total: number }}
276
280
  */
277
281
  export function applyAllMechanicalFixes(projectDir, config, opts = {}) {
278
- assertDefaultDocWrites(config);
279
282
  const { force = false, forceRedo = false } = opts;
280
283
  const guardData = runGuardInternal(projectDir, config);
281
284
  const fixes = [];
@@ -284,7 +287,7 @@ export function applyAllMechanicalFixes(projectDir, config, opts = {}) {
284
287
  }
285
288
  // v0.14-P1: forwarding forceRedo so users with `--force-redo` can override
286
289
  // ping-pong suppression for a specific fix they actually want re-applied.
287
- const { applied, skipped } = applyMechanicalFixes(projectDir, fixes, { force, forceRedo });
290
+ const { applied, skipped } = applyMechanicalFixes(projectDir, fixes, { force, forceRedo, config });
288
291
  return { applied, skipped, total: fixes.length };
289
292
  }
290
293
 
@@ -375,7 +378,6 @@ function runWriteMode(projectDir, config, flags) {
375
378
  // ── Main Entry ─────────────────────────────────────────────────────────────
376
379
 
377
380
  export function runFix(projectDir, config, flags) {
378
- if (flags.write) assertDefaultDocWrites(config);
379
381
  const isJson = flags.format === 'json';
380
382
  const isPrompt = flags.format === 'prompt';
381
383
  const autoFix = flags.auto || false;