docguard-cli 0.39.0 → 0.40.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 (57) hide show
  1. package/README.md +43 -20
  2. package/cli/commands/agent.mjs +47 -1
  3. package/cli/commands/explain.mjs +16 -0
  4. package/cli/commands/fix.mjs +13 -11
  5. package/cli/commands/generate.mjs +52 -18
  6. package/cli/commands/guard.mjs +13 -2
  7. package/cli/commands/mcp.mjs +22 -2
  8. package/cli/commands/score.mjs +13 -1
  9. package/cli/commands/sync.mjs +20 -7
  10. package/cli/commands/verify.mjs +65 -2
  11. package/cli/config.mjs +3 -0
  12. package/cli/docguard.mjs +33 -12
  13. package/cli/evidence/adapters.mjs +200 -0
  14. package/cli/evidence/evaluate.mjs +185 -0
  15. package/cli/evidence/manifest.mjs +194 -0
  16. package/cli/evidence/markdown.mjs +107 -0
  17. package/cli/findings.mjs +31 -0
  18. package/cli/release-pr-policy.mjs +107 -0
  19. package/cli/repository-root.mjs +159 -0
  20. package/cli/scanners/py-ast.mjs +39 -2
  21. package/cli/scanners/task-context.mjs +312 -0
  22. package/cli/shared-doc-roles.mjs +44 -1
  23. package/cli/shared-source.mjs +101 -28
  24. package/cli/validators/architecture.mjs +186 -13
  25. package/cli/validators/environment.mjs +14 -1
  26. package/cli/validators/evidence.mjs +52 -0
  27. package/cli/validators/todo-tracking.mjs +45 -2
  28. package/cli/writers/doc-generators.mjs +31 -17
  29. package/cli/writers/mechanical.mjs +44 -14
  30. package/cli/writers/sections.mjs +31 -3
  31. package/docs/ai-integration.md +31 -6
  32. package/docs/commands.md +43 -5
  33. package/docs/configuration.md +11 -3
  34. package/docs/quickstart.md +1 -1
  35. package/extensions/spec-kit-docguard/commands/fix.md +4 -2
  36. package/extensions/spec-kit-docguard/commands/generate.md +6 -1
  37. package/extensions/spec-kit-docguard/commands/guard.md +3 -2
  38. package/extensions/spec-kit-docguard/commands/sync.md +1 -1
  39. package/extensions/spec-kit-docguard/extension.yml +1 -1
  40. package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +14 -3
  41. package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +16 -5
  42. package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +8 -3
  43. package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +3 -2
  44. package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +6 -3
  45. package/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +2 -2
  46. package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +4 -4
  47. package/package.json +2 -1
  48. package/schemas/docguard-agent-context-benchmark.schema.json +92 -0
  49. package/schemas/docguard-agent-context-result.schema.json +95 -0
  50. package/schemas/docguard-config.schema.json +1 -0
  51. package/schemas/docguard-evidence.schema.json +169 -0
  52. package/schemas/docguard-task-context.schema.json +144 -0
  53. package/templates/AGENTS.md.template +9 -4
  54. package/templates/ci/github-actions.yml +4 -4
  55. package/templates/commands/docguard.guard.md +5 -1
  56. package/templates/commands/docguard.review.md +6 -1
  57. package/templates/evidence-manifest.json +21 -0
@@ -1,4 +1,7 @@
1
1
  /**
2
+ * @implements docguard.language-repository-coverage#FR-010
3
+ * @implements docguard.language-repository-coverage#FR-011
4
+ *
2
5
  * Document Generators — the seven doc emitters behind `docguard generate`:
3
6
  * ARCHITECTURE, API-REFERENCE, DATA-MODEL, ENVIRONMENT, TEST-SPEC, SECURITY,
4
7
  * plus the root files (AGENTS.md, CHANGELOG.md, DRIFT-LOG.md).
@@ -12,15 +15,17 @@ import { resolve, basename, extname } from 'node:path';
12
15
  import { c } from '../shared.mjs';
13
16
  import { generateERDiagram } from '../scanners/schemas.mjs';
14
17
  import { safeWrite, appendStandardsCitation } from './generate-io.mjs';
18
+ import { assertMappedFullDocumentWrites, docRolePath, resolveDocRole } from '../shared-doc-roles.mjs';
15
19
 
16
20
  // ── Document Generators ────────────────────────────────────────────────────
17
21
 
18
22
  export function generateArchitecture(dir, config, stack, scan, flags, docTools) {
19
- const path = resolve(dir, 'docs-canonical/ARCHITECTURE.md');
23
+ const path = resolveDocRole(dir, config, 'architecture');
20
24
  if (existsSync(path) && !flags.force) {
21
25
  console.log(` ${c.dim}⏭️ ARCHITECTURE.md (exists)${c.reset}`);
22
26
  return false;
23
27
  }
28
+ assertMappedFullDocumentWrites(dir, config, ['architecture']);
24
29
 
25
30
  const techRows = Object.entries(stack)
26
31
  .filter(([, v]) => v)
@@ -182,7 +187,7 @@ See \\\`docs-canonical/ADR.md\\\` for the full decision log.
182
187
  ## 10. Quality Requirements
183
188
  <!-- arc42: §10 — Quality Requirements -->
184
189
 
185
- See \\\`docs-canonical/TEST-SPEC.md\\\` for test requirements and coverage targets.
190
+ See \\\`${docRolePath(config, 'testSpec')}\\\` for test requirements and coverage targets.
186
191
 
187
192
  ## 11. Risks & Technical Debt
188
193
  <!-- arc42: §11 — Risk Assessment and Technical Debt -->
@@ -216,11 +221,12 @@ See \\\`docs-canonical/KNOWN-GOTCHAS.md\\\` for known issues.
216
221
  // ── API Reference Generator (NEW — from deep route scanning) ───────────────
217
222
 
218
223
  export function generateApiReference(dir, config, stack, deepRoutes, flags) {
219
- const path = resolve(dir, 'docs-canonical/API-REFERENCE.md');
224
+ const path = resolveDocRole(dir, config, 'apiReference');
220
225
  if (existsSync(path) && !flags.force) {
221
226
  console.log(` ${c.dim}⏭️ API-REFERENCE.md (exists)${c.reset}`);
222
227
  return false;
223
228
  }
229
+ assertMappedFullDocumentWrites(dir, config, ['apiReference']);
224
230
 
225
231
  // Group routes by resource (first path segment after /api/)
226
232
  const groups = {};
@@ -313,11 +319,12 @@ ${resourceSections}
313
319
  // ── Enhanced Data Model Generator ──────────────────────────────────────────
314
320
 
315
321
  export function generateDataModel(dir, config, stack, scan, flags, deepSchemas) {
316
- const path = resolve(dir, 'docs-canonical/DATA-MODEL.md');
322
+ const path = resolveDocRole(dir, config, 'dataModel');
317
323
  if (existsSync(path) && !flags.force) {
318
324
  console.log(` ${c.dim}⏭️ DATA-MODEL.md (exists)${c.reset}`);
319
325
  return false;
320
326
  }
327
+ assertMappedFullDocumentWrites(dir, config, ['dataModel']);
321
328
 
322
329
  // Use deep schemas if available, fallback to basic scan
323
330
  let entities = [];
@@ -466,11 +473,12 @@ ${erDiagram}
466
473
  }
467
474
 
468
475
  export function generateEnvironment(dir, config, stack, scan, flags) {
469
- const path = resolve(dir, 'docs-canonical/ENVIRONMENT.md');
476
+ const path = resolveDocRole(dir, config, 'environment');
470
477
  if (existsSync(path) && !flags.force) {
471
478
  console.log(` ${c.dim}⏭️ ENVIRONMENT.md (exists)${c.reset}`);
472
479
  return false;
473
480
  }
481
+ assertMappedFullDocumentWrites(dir, config, ['environment']);
474
482
 
475
483
  const envVarRows = scan.envVars.map(v =>
476
484
  `| \`${v.name}\` | ${categorizeEnvVar(v.name)} | Yes | \`${v.example}\` | |`
@@ -529,11 +537,12 @@ ${envVarRows || '| <!-- No .env.example found --> | | | | |'}
529
537
  }
530
538
 
531
539
  export function generateTestSpec(dir, config, stack, scan, flags) {
532
- const path = resolve(dir, 'docs-canonical/TEST-SPEC.md');
540
+ const path = resolveDocRole(dir, config, 'testSpec');
533
541
  if (existsSync(path) && !flags.force) {
534
542
  console.log(` ${c.dim}⏭️ TEST-SPEC.md (exists)${c.reset}`);
535
543
  return false;
536
544
  }
545
+ assertMappedFullDocumentWrites(dir, config, ['testSpec']);
537
546
 
538
547
  // Build service-to-test map
539
548
  const serviceMap = [];
@@ -614,11 +623,12 @@ ${serviceRows || '| <!-- No services found --> | | | |'}
614
623
  }
615
624
 
616
625
  export function generateSecurity(dir, config, stack, scan, flags) {
617
- const path = resolve(dir, 'docs-canonical/SECURITY.md');
626
+ const path = resolveDocRole(dir, config, 'security');
618
627
  if (existsSync(path) && !flags.force) {
619
628
  console.log(` ${c.dim}⏭️ SECURITY.md (exists)${c.reset}`);
620
629
  return false;
621
630
  }
631
+ assertMappedFullDocumentWrites(dir, config, ['security']);
622
632
 
623
633
  const content = `# Security
624
634
 
@@ -682,6 +692,15 @@ ${scan.envVars.filter(v => isSecretVar(v.name)).map(v =>
682
692
  export function generateRootFiles(dir, config, stack, scan, flags, docTools) {
683
693
  let created = 0;
684
694
  let skipped = 0;
695
+ const canonicalFiles = [
696
+ [docRolePath(config, 'architecture'), 'System design (arc42 aligned)'],
697
+ [docRolePath(config, 'apiReference'), 'API endpoint documentation'],
698
+ [docRolePath(config, 'dataModel'), 'Database schemas & entities'],
699
+ [docRolePath(config, 'security'), 'Auth & secrets'],
700
+ [docRolePath(config, 'testSpec'), 'Test requirements'],
701
+ [docRolePath(config, 'environment'), 'Environment setup'],
702
+ ];
703
+ const canonicalRows = canonicalFiles.map(([path, purpose]) => `| \`${path}\` | ${purpose} |`).join('\n');
685
704
 
686
705
  // AGENTS.md (AGENTS.md Standard compliant)
687
706
  const agentsPath = resolve(dir, 'AGENTS.md');
@@ -696,7 +715,7 @@ export function generateRootFiles(dir, config, stack, scan, flags, docTools) {
696
715
 
697
716
  ## Workflow
698
717
 
699
- 1. **Read** \`docs-canonical/\` before suggesting changes
718
+ 1. **Read** the configured canonical files under **Key Files** before suggesting changes
700
719
  2. **Check** existing patterns in the codebase
701
720
  3. **Run** \`npx docguard-cli diagnose\` to see what needs fixing
702
721
  4. **Confirm** your approach before writing code
@@ -712,12 +731,7 @@ ${Object.entries(stack).filter(([, v]) => v).map(([k, v]) => `- **${k}**: ${v}`)
712
731
 
713
732
  | File | Purpose |
714
733
  |------|---------|
715
- | \`docs-canonical/ARCHITECTURE.md\` | System design (arc42 aligned) |
716
- | \`docs-canonical/API-REFERENCE.md\` | API endpoint documentation |
717
- | \`docs-canonical/DATA-MODEL.md\` | Database schemas & entities |
718
- | \`docs-canonical/SECURITY.md\` | Auth & secrets |
719
- | \`docs-canonical/TEST-SPEC.md\` | Test requirements |
720
- | \`docs-canonical/ENVIRONMENT.md\` | Environment setup |
734
+ ${canonicalRows}
721
735
  | \`AGENTS.md\` | AI agent instructions (this file) |
722
736
  | \`CHANGELOG.md\` | Change tracking |
723
737
  | \`DRIFT-LOG.md\` | Documented deviations |
@@ -729,7 +743,7 @@ ${Object.entries(stack).filter(([, v]) => v).map(([k, v]) => `- **${k}**: ${v}`)
729
743
  ### Allowed
730
744
 
731
745
  - Read any file in the repository
732
- - Modify files within \`src/\`, \`tests/\`, and \`docs-canonical/\`
746
+ - Modify files within \`src/\`, \`tests/\`, and the configured canonical documentation paths
733
747
  - Run test commands (\`npm test\`, \`npx docguard-cli guard\`)
734
748
  - Create new files in appropriate directories
735
749
 
@@ -780,8 +794,8 @@ npx docguard-cli generate # Generate docs from code
780
794
 
781
795
  - Never commit without updating CHANGELOG.md
782
796
  - If code deviates from docs, add \`// DRIFT: reason\`
783
- - Security rules in SECURITY.md are mandatory
784
- - Test requirements in TEST-SPEC.md must be met
797
+ - Security rules in \`${docRolePath(config, 'security')}\` are mandatory
798
+ - Test requirements in \`${docRolePath(config, 'testSpec')}\` must be met
785
799
  - Documentation changes must pass \`docguard guard\`
786
800
  `;
787
801
  safeWrite(agentsPath, content);
@@ -1,4 +1,8 @@
1
1
  /**
2
+ * @implements docguard.language-repository-coverage#FR-009
3
+ * @implements docguard.language-repository-coverage#FR-010
4
+ * @implements docguard.language-repository-coverage#FR-011
5
+ *
2
6
  * Mechanical Fix Registry — applies deterministic, no-LLM fixes in place.
3
7
  *
4
8
  * Validators surface structured `fixes[]` actions; this module knows how to
@@ -37,16 +41,30 @@ try {
37
41
  _sectionsModule = null;
38
42
  }
39
43
 
40
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
44
+ import { existsSync, readFileSync } from 'node:fs';
41
45
  import { resolve } from 'node:path';
42
46
  import { removeEndpoints, hasGeneratedMarker } from './api-reference.mjs';
47
+ import { safeWrite } from './generate-io.mjs';
48
+ import { assertMappedFullDocumentWrites, isMappedDocPath, mappedRolesForPath } from '../shared-doc-roles.mjs';
49
+
50
+ function authorizeMappedWholeDocument(projectDir, config, path) {
51
+ if (!isMappedDocPath(config, path)) return null;
52
+ try {
53
+ assertMappedFullDocumentWrites(projectDir, config, mappedRolesForPath(config, path));
54
+ return null;
55
+ } catch (error) {
56
+ return error.message;
57
+ }
58
+ }
43
59
 
44
60
  const esc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
45
61
 
46
62
  /** replace-count: "<found> <label>" → "<actual> <label>" in the file. */
47
- function applyReplaceCount(projectDir, fix) {
63
+ function applyReplaceCount(projectDir, fix, opts = {}) {
48
64
  const full = resolve(projectDir, fix.file);
49
65
  if (!existsSync(full)) return { applied: false };
66
+ const blocked = authorizeMappedWholeDocument(projectDir, opts.config, fix.file);
67
+ if (blocked) return { applied: false, skipped: blocked };
50
68
  // Bug #2 (fail-closed): NEVER overwrite a number without provenance proving
51
69
  // the "actual" describes the SAME subject. The Metrics-Consistency validator
52
70
  // stamps `actualSource` (e.g. "docguard.guard.checks") only for claims it
@@ -74,14 +92,16 @@ function applyReplaceCount(projectDir, fix) {
74
92
  return `${fix.actual}${tail}`;
75
93
  });
76
94
  if (!changed || next === content) return { applied: false };
77
- writeFileSync(full, next, 'utf-8');
95
+ safeWrite(full, next);
78
96
  return { applied: true, detail: `${fix.file}: "${fix.found} ${fix.label}" → "${fix.actual} ${fix.label}"` };
79
97
  }
80
98
 
81
99
  /** replace-version: stale version → current, ONLY in actionable contexts. */
82
- function applyReplaceVersion(projectDir, fix) {
100
+ function applyReplaceVersion(projectDir, fix, opts = {}) {
83
101
  const full = resolve(projectDir, fix.file);
84
102
  if (!existsSync(full)) return { applied: false };
103
+ const blocked = authorizeMappedWholeDocument(projectDir, opts.config, fix.file);
104
+ if (blocked) return { applied: false, skipped: blocked };
85
105
  const content = readFileSync(full, 'utf-8');
86
106
  const f = esc(fix.found);
87
107
  // Mirror metadata-sync's actionable detection so we never touch prose.
@@ -93,7 +113,7 @@ function applyReplaceVersion(projectDir, fix) {
93
113
  let next = content;
94
114
  for (const re of patterns) next = next.replace(re, `$1${fix.actual}`);
95
115
  if (next === content) return { applied: false };
96
- writeFileSync(full, next, 'utf-8');
116
+ safeWrite(full, next);
97
117
  return { applied: true, detail: `${fix.file}: v${fix.found} → v${fix.actual}` };
98
118
  }
99
119
 
@@ -112,21 +132,23 @@ function applyInsertChangelogUnreleased(projectDir, fix) {
112
132
  }
113
133
  const block = idx > 0 && lines[idx - 1].trim() !== '' ? ['', '## [Unreleased]', ''] : ['## [Unreleased]', ''];
114
134
  lines.splice(idx, 0, ...block);
115
- writeFileSync(full, lines.join('\n'), 'utf-8');
135
+ safeWrite(full, lines.join('\n'));
116
136
  return { applied: true, detail: `${fix.file}: added ## [Unreleased]` };
117
137
  }
118
138
 
119
139
  /** remove-endpoint: delegate to the API-REFERENCE writer (marker-gated). */
120
- function applyRemoveEndpoint(projectDir, fix, { force = false } = {}) {
140
+ function applyRemoveEndpoint(projectDir, fix, { force = false, config = {} } = {}) {
121
141
  const full = resolve(projectDir, fix.doc || 'docs-canonical/API-REFERENCE.md');
122
142
  if (!existsSync(full)) return { applied: false };
143
+ const blocked = authorizeMappedWholeDocument(projectDir, config, fix.doc || 'docs-canonical/API-REFERENCE.md');
144
+ if (blocked) return { applied: false, skipped: blocked };
123
145
  const content = readFileSync(full, 'utf-8');
124
146
  if (!hasGeneratedMarker(content) && !force) {
125
147
  return { applied: false, skipped: `${fix.doc} not docguard:generated (use --force)` };
126
148
  }
127
149
  const { content: next, removed } = removeEndpoints(content, [{ method: fix.method, path: fix.path }]);
128
150
  if (removed.length === 0 || next === content) return { applied: false };
129
- writeFileSync(full, next, 'utf-8');
151
+ safeWrite(full, next);
130
152
  return { applied: true, detail: `${fix.doc}: removed ${fix.method} ${fix.path}` };
131
153
  }
132
154
 
@@ -142,7 +164,7 @@ function applyRemoveEndpoint(projectDir, fix, { force = false } = {}) {
142
164
  *
143
165
  * fix shape: { type: 'regenerate-section', doc, sectionId, body }
144
166
  */
145
- function applyRegenerateSection(projectDir, fix) {
167
+ function applyRegenerateSection(projectDir, fix, opts = {}) {
146
168
  if (!fix.doc || !fix.sectionId || fix.body == null) {
147
169
  return { applied: false, skipped: 'regenerate-section needs doc, sectionId, body' };
148
170
  }
@@ -152,17 +174,23 @@ function applyRegenerateSection(projectDir, fix) {
152
174
  // Lazy-import the section writer to avoid a top-level circular risk.
153
175
  // section APIs are synchronous and well-isolated; this works because
154
176
  // mechanical.mjs already uses top-level await for fix-memory.
155
- const { getSection, replaceSection } = _sectionsModule || {};
156
- if (typeof getSection !== 'function' || typeof replaceSection !== 'function') {
177
+ const { assertOwnedCodeSection, getSection, replaceSection } = _sectionsModule || {};
178
+ if (typeof getSection !== 'function' || typeof replaceSection !== 'function' || typeof assertOwnedCodeSection !== 'function') {
157
179
  return { applied: false, skipped: 'sections module unavailable' };
158
180
  }
159
181
  const existing = getSection(content, fix.sectionId);
160
182
  if (!existing) return { applied: false, skipped: `section ${fix.sectionId} not present in ${fix.doc}` };
183
+ if (isMappedDocPath(opts.config, fix.doc)) {
184
+ try { assertOwnedCodeSection(content, fix.sectionId, fix.doc); }
185
+ catch (error) { return { applied: false, skipped: error.message }; }
186
+ } else if (existing.source !== 'code') {
187
+ return { applied: false, skipped: `${fix.doc}: section ${fix.sectionId} is not source=code` };
188
+ }
161
189
  if (existing.body.trim() === String(fix.body).trim()) {
162
190
  return { applied: false, skipped: `${fix.doc} § ${fix.sectionId} already current` };
163
191
  }
164
192
  const next = replaceSection(content, fix.sectionId, fix.body).content;
165
- writeFileSync(full, next, 'utf-8');
193
+ safeWrite(full, next);
166
194
  return { applied: true, detail: `${fix.doc}: regenerated § ${fix.sectionId}` };
167
195
  }
168
196
 
@@ -177,12 +205,14 @@ function applyRegenerateSection(projectDir, fix) {
177
205
  * forms — won't touch the broken slug if it happens to appear as plain text.
178
206
  * Idempotent: if no occurrence is found (already fixed), no-op.
179
207
  */
180
- function applyReplaceAnchor(projectDir, fix) {
208
+ function applyReplaceAnchor(projectDir, fix, opts = {}) {
181
209
  if (!fix.doc || !fix.from || !fix.to) {
182
210
  return { applied: false, skipped: 'replace-anchor needs doc, from, to' };
183
211
  }
184
212
  const full = resolve(projectDir, fix.doc);
185
213
  if (!existsSync(full)) return { applied: false, skipped: `doc not found: ${fix.doc}` };
214
+ const blocked = authorizeMappedWholeDocument(projectDir, opts.config, fix.doc);
215
+ if (blocked) return { applied: false, skipped: blocked };
186
216
  const content = readFileSync(full, 'utf-8');
187
217
 
188
218
  // Match an anchor inside a markdown link: `](#from)` OR `](path#from)`.
@@ -194,7 +224,7 @@ function applyReplaceAnchor(projectDir, fix) {
194
224
  if (next === content) {
195
225
  return { applied: false, skipped: `${fix.doc}: anchor #${fix.from} not found (already fixed?)` };
196
226
  }
197
- writeFileSync(full, next, 'utf-8');
227
+ safeWrite(full, next);
198
228
  return { applied: true, detail: `${fix.doc}: #${fix.from} → #${fix.to}` };
199
229
  }
200
230
 
@@ -53,8 +53,14 @@ function parseAttrs(attrStr) {
53
53
  * @returns {Array<{ id, source, attrs, openLine, closeLine, body }>}
54
54
  */
55
55
  export function parseSections(content) {
56
+ return inspectSections(content).sections;
57
+ }
58
+
59
+ /** Parse sections and retain malformed-marker evidence for write authorization. */
60
+ export function inspectSections(content) {
56
61
  const lines = String(content).split('\n');
57
62
  const sections = [];
63
+ const issues = [];
58
64
  let open = null;
59
65
 
60
66
  for (let i = 0; i < lines.length; i++) {
@@ -70,17 +76,39 @@ export function parseSections(content) {
70
76
  body: lines.slice(open.openLine + 1, i).join('\n'),
71
77
  });
72
78
  open = null;
73
- }
74
- // A close with no matching open is ignored.
79
+ } else issues.push({ code: 'unmatched-close', line: i + 1 });
75
80
  continue;
76
81
  }
77
82
  const om = line.match(OPEN_RE);
78
83
  if (om) {
79
84
  // Abandon any still-open (malformed) section; start fresh here.
85
+ if (open !== null) issues.push({ code: 'nested-or-unclosed', line: open.openLine + 1 });
80
86
  open = { attrs: parseAttrs(om[1] || ''), openLine: i };
81
87
  }
82
88
  }
83
- return sections;
89
+ if (open !== null) issues.push({ code: 'unclosed', line: open.openLine + 1 });
90
+ const counts = new Map();
91
+ for (const section of sections) {
92
+ if (!section.id) issues.push({ code: 'missing-id', line: section.openLine + 1 });
93
+ counts.set(section.id, (counts.get(section.id) || 0) + 1);
94
+ }
95
+ for (const [id, count] of counts) if (id && count > 1) issues.push({ code: 'duplicate-id', id, count });
96
+ return { sections, issues };
97
+ }
98
+
99
+ /**
100
+ * Return the unique code-owned section or throw before a mapped write.
101
+ * @implements docguard.language-repository-coverage#FR-009
102
+ */
103
+ export function assertOwnedCodeSection(content, id, path = 'document') {
104
+ const inspected = inspectSections(content);
105
+ if (inspected.issues.length) {
106
+ throw new Error(`${path}: malformed or duplicate docguard:section markers; no write was applied.`);
107
+ }
108
+ const matches = inspected.sections.filter(section => section.id === id);
109
+ if (matches.length !== 1) throw new Error(`${path}: section ${id} must exist exactly once before a bounded write.`);
110
+ if (matches[0].source !== 'code') throw new Error(`${path}: section ${id} is source=${matches[0].source}; only source=code is writable.`);
111
+ return matches[0];
84
112
  }
85
113
 
86
114
  /** Get a single section by id, or null. */
@@ -18,6 +18,7 @@ what it finds. The division of labour never changes:
18
18
  | GitHub Code Scanning / SARIF dashboards | **SARIF output** | `npx docguard-cli guard --format sarif` |
19
19
  | A PR reviewer (human or bot) | **GitHub Action** | inline annotations + sticky doc-impact comment, default on |
20
20
  | An LLM reading the repo cold | **llms.txt / llms-full.txt / context pack** | `docguard llms`, `llms --full`, `memory --pack` |
21
+ | An agent starting one concrete change | **Task context** | `docguard agent --task "Implement acme.feature#FR-001" --format json` |
21
22
 
22
23
  ## MCP server (native tools, no shelling out)
23
24
 
@@ -28,14 +29,16 @@ claude mcp add docguard -- npx docguard-cli mcp
28
29
  npx docguard-cli mcp
29
30
  ```
30
31
 
31
- Five tools, each accepting an optional `projectDir`:
32
+ Seven tools, each accepting an optional `projectDir`:
32
33
 
33
34
  | Tool | Returns |
34
35
  |------|---------|
35
36
  | `docguard_guard` | The full guard JSON contract — status, findings (stable codes), coverage, unverified-claim count |
36
37
  | `docguard_score` | `{score, grade, categories}` |
37
38
  | `docguard_explain` | A finding code's contract: title, help, suppression pragma, owning validator |
39
+ | `docguard_verify_evidence` | Exact declared statement-to-source checks with scoped verification states |
38
40
  | `docguard_verify_claims` | Documented numbers/limits/enums as verification tasks — **the caller checks each against the code** |
41
+ | `docguard_report` | Commit-stamped compliance evidence with a tamper-evident integrity hash |
39
42
  | `docguard_diagnose` | Failing/warning validators with per-finding suggestions, shaped for action |
40
43
 
41
44
  The server is read-only (never scaffolds), keeps stdout as a pure JSON-RPC
@@ -108,10 +111,25 @@ and degrade gracefully on fork tokens and shallow clones.
108
111
  | `llms-full.txt` | `docguard llms --full` | Full doc bodies inlined — one fetch, per-doc 400-line cap |
109
112
  | `.docguard/context-pack.md` | `docguard memory --pack` | Compact session-start context: guard status, scanner-derived surface counts, doc index with review dates, your AGENTS.md rules verbatim, known drift. Everything derived from code — regenerable, hallucination-free |
110
113
  | `.docguard-specs.json` | `docguard specs --check` / `--write` | Committed spec lifecycle index: reviewed status and lineage plus deterministic artifact, task, and requirement-scoped test evidence. Requirement prose stays in each authoritative spec. |
114
+ | Task-context JSON | `docguard agent --task <text> --format json` | Read-only, bounded excerpts from approved current evidence plus source/test pointers. It excludes retired and unsafe material and abstains when relevance is weak. |
111
115
 
112
116
  Load the context pack at agent session start; regenerate any time — it is
113
117
  never hand-edited.
114
118
 
119
+ For a concrete change, task context can reduce discovery steps:
120
+
121
+ ```bash
122
+ docguard agent --task "Implement acme.payments#FR-003 in src/payments.mjs" --format json
123
+ ```
124
+
125
+ Treat the packet as a retrieval aid. Hashes prove which bytes were selected;
126
+ they do not prove the prose is correct. Read additional code or documentation
127
+ when the task requires it, and use the navigation map when the selector
128
+ abstains. Protocol v1 preserved all 27 synthetic task outcomes and reduced
129
+ median steps by 50% and latency by 17% versus `memory --pack`, but increased
130
+ median uncached input tokens by 80%; teams optimizing token spend may prefer
131
+ the context pack.
132
+
115
133
  ## One source of truth for agent files
116
134
 
117
135
  Teams hand-duplicate AGENTS.md into `CLAUDE.md`, `.cursor/rules/`,
@@ -135,7 +153,7 @@ is the canonical source.
135
153
  ## The agent workflow
136
154
 
137
155
  ```
138
- specs preflight → diagnose → fix (research + write) → guard → verify --semantic → done
156
+ specs preflight → diagnose → fix (research + write) → guard → verify --evidence → verify --semantic → done
139
157
  ```
140
158
 
141
159
  1. **`docguard specs preflight`** — before specification, load the current intent
@@ -146,7 +164,10 @@ specs preflight → diagnose → fix (research + write) → guard → verify --s
146
164
  3. **`docguard fix --doc <name>`** — emits research steps + expected structure
147
165
  for one doc. Execute the research, write real content, no placeholders.
148
166
  4. **`docguard guard`** — verify. Loop until PASS.
149
- 5. **`docguard verify --semantic`** — extract every checkable documented claim
167
+ 5. **`docguard verify --evidence`** — evaluate exact, typed declarations against
168
+ local JSON, bounded collections, or saved compatibility reports. Preserve
169
+ each state and its statement-level scope.
170
+ 6. **`docguard verify --semantic`** — extract every remaining checkable documented claim
150
171
  (counts, limits, enums) with the nearest cited code path. **You** compare
151
172
  each value against the code: a green guard asserts structure, not the truth
152
173
  of documented numbers. This is the highest-value step an agent can run.
@@ -183,9 +204,13 @@ integrity. Each failing metric names its fix.
183
204
  acting, suppress at the site with it, report false positives via `feedback`.
184
205
  3. **Run `guard` after every fix batch** — loop until PASS.
185
206
  4. **Never treat `na` as a pass** — "nothing to validate" is a coverage gap.
186
- 5. **Check `semanticClaims.count` on green runs** — offer `verify --semantic`.
187
- 6. **Respect the drift protocol** deviating from canonical docs requires
207
+ 5. **Inspect `evidence` on every configured run** — resolve contradictions,
208
+ stale inputs, inconclusive targets, and unsupported formats before claiming
209
+ the selected statement is current.
210
+ 6. **Check `semanticClaims.count` on green runs** — run `verify --semantic` for
211
+ the claims that lack unique exact evidence.
212
+ 7. **Respect the drift protocol** — deviating from canonical docs requires
188
213
  `// DRIFT: reason` + a DRIFT-LOG.md entry, not a silent doc rewrite; the
189
214
  docs may be right and the code wrong.
190
- 7. **`score --tax`** periodically — documentation should stay an asset, not a
215
+ 8. **`score --tax`** periodically — documentation should stay an asset, not a
191
216
  burden.
package/docs/commands.md CHANGED
@@ -170,9 +170,11 @@ the same local ID commonly appears in several specs.
170
170
 
171
171
  The generated-spec preflight blocks missing or duplicate identity, stale
172
172
  registry state, unsafe paths, and broken lifecycle lineage. Text similarity is
173
- low-confidence review context and never blocks by itself. A future
174
- `specs complete` transaction will verify exact-revision implementation evidence,
175
- canonical outcomes, and context regeneration before marking a spec verified.
173
+ low-confidence review context and never blocks by itself. `specs complete`
174
+ verifies exact-revision implementation evidence, canonical outcomes, and context
175
+ regeneration before marking a spec verified; `--write` requires a reviewed
176
+ reason and performs the registry, spec outcome, and current-context writes as
177
+ one rollback-safe transaction.
176
178
 
177
179
  ### `docguard retire`
178
180
 
@@ -219,6 +221,26 @@ npx docguard-cli fix --doc environment
219
221
 
220
222
  **Output includes:** TASK, PURPOSE, RESEARCH STEPS (what to grep/read), WRITE THE DOCUMENT (expected sections).
221
223
 
224
+ ### `docguard agent`
225
+
226
+ **Build an agent task graph or select bounded evidence for one task.** Existing
227
+ task-graph behavior is unchanged when `--task` is absent.
228
+
229
+ ```bash
230
+ npx docguard-cli agent
231
+ npx docguard-cli agent --format json
232
+ npx docguard-cli agent --task "Implement acme.payments#FR-003 in src/payments.mjs"
233
+ npx docguard-cli agent --task "Fix SEC001 in src/config.mjs" --format json
234
+ ```
235
+
236
+ Task mode reads only configured canonical documents, approved current specs,
237
+ project rules, and exact linked source/test pointers. It excludes retired,
238
+ unapproved, digest-stale, private, unsafe, and symlinked material. JSON output
239
+ uses `schemas/docguard-task-context.schema.json`, contains a task digest instead
240
+ of raw task text, and reports retrieval-only assurance. When no excerpt reaches
241
+ the frozen relevance threshold, it returns `selection.status: "abstained"` with
242
+ a navigation map rather than weak context.
243
+
222
244
  ### `docguard agents`
223
245
 
224
246
  **Generate agent-specific config files** from AGENTS.md.
@@ -237,7 +259,8 @@ never touched without `--force`.
237
259
 
238
260
  **MCP server over stdio** — DocGuard's read-only core as native agent tools
239
261
  (`docguard_guard`, `docguard_score`, `docguard_explain`,
240
- `docguard_verify_claims`, `docguard_report`, `docguard_diagnose`).
262
+ `docguard_verify_evidence`, `docguard_verify_claims`, `docguard_report`,
263
+ `docguard_diagnose`).
241
264
 
242
265
  ```bash
243
266
  claude mcp add docguard -- npx docguard-cli mcp
@@ -248,6 +271,14 @@ claude mcp add docguard -- npx docguard-cli mcp
248
271
  **Extract documented claims** (counts, limits, enums) as a verification task
249
272
  list with cited code paths — the agent checks each value against the code.
250
273
 
274
+ ### `docguard verify --evidence`
275
+
276
+ **Evaluate exact declared evidence** from `.docguard-evidence.json`. Supported
277
+ sources are typed RFC 6901 JSON values, bounded repository collections, saved
278
+ oasdiff JSON, and saved Buf JSON Lines. Output preserves
279
+ `verified-within-scope`, `contradicted`, `stale`, `inconclusive`, and
280
+ `unsupported`; a pass applies only to its selected Markdown statement.
281
+
251
282
  ### `docguard explain <CODE>`
252
283
 
253
284
  **Explain any finding code** (`STR001`, `ENV003`, …): what it means, how to fix
@@ -342,7 +373,7 @@ npx docguard-cli diff
342
373
 
343
374
  | Flag | Description |
344
375
  |------|-------------|
345
- | `--dir <path>` | Project directory (default: current directory) |
376
+ | `--dir <path>` | Project directory (default: current directory); explicit selection suppresses ancestor-root guidance |
346
377
  | `--format <type>` | Output format: `text` (default), `json`, `prompt` |
347
378
  | `--verbose` | Show detailed output |
348
379
  | `--profile <name>` | Compliance profile: `starter`, `standard`, `enterprise` |
@@ -356,3 +387,10 @@ npx docguard-cli diff
356
387
  | `--force` | Overwrite existing files |
357
388
  | `--help` | Show help |
358
389
  | `--version` | Show version |
390
+
391
+ Without `--dir`, a command remains scoped to the current directory. DocGuard
392
+ reads only bounded ancestors for an owning `.docguard.json`, npm `workspaces`, or
393
+ pnpm `packages` declaration. When one includes the selected package, human
394
+ stderr prints an exact `--dir` rerun; JSON/SARIF/JUnit runs receive a typed JSON
395
+ diagnostic on stderr. Stdout and the selected scan scope do not change. A nested
396
+ Git root prevents a suggestion from crossing into an outer repository.
@@ -177,7 +177,15 @@ Use explicit document roles to validate Markdown files in an existing layout. A
177
177
 
178
178
  Supported roles are architecture, dataModel, security, testSpec, environment, apiReference, and requirements. Paths must name Markdown files within the project; private directories, parent traversal, absolute paths, and symlink destinations are rejected. Several roles may reference one document. Each role's content checks still apply; a mapping is not a correctness attestation. Default roles remain unchanged unless explicitly mapped.
179
179
 
180
- This first version supports validation, scoring, and read-only planning. Legacy automatic document generation, sync writes, and repair writes refuse custom mappings before scaffolding or modifying files. This protects existing documents while writer behavior is extended and reviewed. Read-only plans identify mapped destinations; a human or agent can review the proposed work against the existing document structure.
180
+ Mapped paths use an explicit ownership contract for writes:
181
+
182
+ - A missing mapped target may be generated when exactly one role maps to it.
183
+ - An existing file with `<!-- docguard:generated true -->` grants DocGuard full-document ownership. Generation and mechanical whole-document repair keep their normal backup behavior.
184
+ - An existing human document grants bounded ownership only through one unique, well-formed `<!-- docguard:section id=<id> source=code -->` region. `generate --plan --write`, `sync --write`, and section regeneration may replace that region while preserving every surrounding byte.
185
+ - Missing, duplicate, nested, unclosed, or `source=human` markers reject the write before mutation. Several roles mapped to one file also reject whole-document generation.
186
+ - `--force` never grants ownership and cannot bypass these checks.
187
+
188
+ Broad legacy scaffolding such as `diagnose --auto` remains unavailable for a mapped layout because it cannot select an operation-specific owned target safely. Read-only plans identify mapped destinations without requiring ownership markers.
181
189
 
182
190
  The docs.dirs setting extends document inventory and explicitly opts additional directories into freshness review. Inventory membership does not mean every detector checks every file. Semantic extraction covers canonical Markdown, explicitly mapped Markdown roles, README, and AGENTS within its safety and size limits; other prose remains unverified.
183
191
 
@@ -195,9 +203,9 @@ These statuses skip currentness assertions; they do not hide structural or other
195
203
 
196
204
  ## Understanding check coverage
197
205
 
198
- Guard JSON includes checkCoverage and an applicability record per validator. States distinguish checked, partial, disabled, not-applicable, missing-prerequisite, unsupported, no-matches, and error. A passing gate means the selected policy passed; it does not mean unsupported languages or unmatched inputs were examined. CI and reports preserve this disclosure. Python import-graph analysis remains unsupported; mixed Python/JS projects disclose partial architecture coverage.
206
+ Guard JSON includes checkCoverage and an applicability record per validator. States distinguish checked, partial, disabled, not-applicable, missing-prerequisite, unsupported, no-matches, and error. A passing gate means the selected policy passed; it does not mean unsupported languages or unmatched inputs were examined. CI and reports preserve this disclosure. Python architecture analysis uses the installed Python interpreter's AST without importing project modules. It resolves unique repository-local modules in regular flat and `src/` packages plus explicit relative imports. Dynamic imports, runtime `sys.path` changes, parser failure or absence, and ambiguous modules remain partial or unsupported coverage while supported edges and findings are retained.
199
207
 
200
- Wrangler configuration supplies evidence for Worker classification. Supported typed Worker bindings participate in environment extraction without executing configuration or application code. Dynamic names, alias/dataflow tracking, and unsupported forms remain outside this bounded analysis. The existing optional Babel parser resolves lexical bindings; the fallback covers ordinary tested scopes and has lower syntax coverage.
208
+ Wrangler configuration supplies static evidence for Worker classification and untyped official handler arguments; DocGuard never executes configuration or application code. With the optional Babel parser, environment extraction recognizes module-handler `env`, exported Pages `onRequest*` `context.env`, `this.env` on classes extending an entrypoint imported from `cloudflare:workers`, and `env` imported from that module. Lexical aliases are followed and shadows are excluded. Computed non-literal keys, indirect exports, user-defined lookalike classes, and other runtimes remain outside the bounded analysis. The fallback covers ordinary handler `env` scopes and reports AST-only forms as partial coverage.
201
209
 
202
210
  ### Evidence boundaries in documentation and schema scans
203
211
 
@@ -68,7 +68,7 @@ diagnose → AI reads prompts → AI fixes docs → guard verifies
68
68
  ## Verify
69
69
 
70
70
  ```bash
71
- npx docguard-cli guard # Pass/fail check (29 validators)
71
+ npx docguard-cli guard # Pass/fail check (30 validators)
72
72
  npx docguard-cli score # 0-100 maturity score
73
73
  ```
74
74
 
@@ -30,7 +30,9 @@ npx --yes docguard-cli@latest fix --write
30
30
  ```
31
31
 
32
32
  Output lists every applied fix. Idempotent: re-running is a no-op if nothing changed.
33
- Only edits `<!-- docguard:generated true -->` docs unless `--force`.
33
+ Whole-document fixes require `<!-- docguard:generated true -->`. A mapped human
34
+ document permits only a unique `source=code` section fix; `--force` cannot grant
35
+ ownership.
34
36
 
35
37
  ### Step 2 — Identify remaining issues by kind
36
38
 
@@ -70,5 +72,5 @@ Iterate until clean (max 3 rounds; if still failing, report remaining issues).
70
72
 
71
73
  - `--write` — apply deterministic fixes in place (step 1).
72
74
  - `--doc <name>` — emit a research-grounded prompt for one specific document (step 3).
73
- - `--force` — for `--write`, edit docs that lack the generated marker.
75
+ - `--force` — for `--write`, permit supported unmarked default-path fixes; mapped ownership checks remain mandatory.
74
76
  - `--format json` — machine-readable issue list (with `fixKind`).
@@ -18,6 +18,11 @@ Two modes:
18
18
  - **`--plan`** (AI-powered, recommended) — emits a structured agent task manifest + writes the code-truth skeleton inside `<!-- docguard:section -->` markers. The AI agent then writes the prose grounded in scanned facts. Human prose is preserved.
19
19
  - **default** — purely deterministic generation: writes templated docs with TODO placeholders. Use when no AI agent is available.
20
20
 
21
+ With `docs.roles`, a missing or explicitly generated single-role target can
22
+ receive a full document. Existing human documents receive only bounded updates
23
+ inside unique `source=code` sections. Shared roles and malformed markers fail
24
+ before any mapped document is written, even with `--force`.
25
+
21
26
  ## User Input
22
27
 
23
28
  $ARGUMENTS
@@ -70,4 +75,4 @@ npx --yes docguard-cli@latest generate $ARGUMENTS
70
75
  ## Flags
71
76
 
72
77
  - `--doc <name>` — Generate a specific document only
73
- - `--dir <path>` — Run on a different directory
78
+ - `--dir <path>` — Run on a different directory; explicit selection suppresses ancestor-root guidance