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.
- package/README.md +43 -20
- package/cli/commands/agent.mjs +47 -1
- package/cli/commands/explain.mjs +16 -0
- package/cli/commands/fix.mjs +13 -11
- package/cli/commands/generate.mjs +52 -18
- package/cli/commands/guard.mjs +13 -2
- package/cli/commands/mcp.mjs +22 -2
- package/cli/commands/score.mjs +13 -1
- package/cli/commands/sync.mjs +20 -7
- package/cli/commands/verify.mjs +65 -2
- package/cli/config.mjs +3 -0
- package/cli/docguard.mjs +33 -12
- package/cli/evidence/adapters.mjs +200 -0
- package/cli/evidence/evaluate.mjs +185 -0
- package/cli/evidence/manifest.mjs +194 -0
- package/cli/evidence/markdown.mjs +107 -0
- package/cli/findings.mjs +31 -0
- package/cli/release-pr-policy.mjs +107 -0
- package/cli/repository-root.mjs +159 -0
- package/cli/scanners/py-ast.mjs +39 -2
- package/cli/scanners/task-context.mjs +312 -0
- package/cli/shared-doc-roles.mjs +44 -1
- package/cli/shared-source.mjs +101 -28
- package/cli/validators/architecture.mjs +186 -13
- package/cli/validators/environment.mjs +14 -1
- package/cli/validators/evidence.mjs +52 -0
- package/cli/validators/todo-tracking.mjs +45 -2
- package/cli/writers/doc-generators.mjs +31 -17
- package/cli/writers/mechanical.mjs +44 -14
- package/cli/writers/sections.mjs +31 -3
- package/docs/ai-integration.md +31 -6
- package/docs/commands.md +43 -5
- package/docs/configuration.md +11 -3
- package/docs/quickstart.md +1 -1
- package/extensions/spec-kit-docguard/commands/fix.md +4 -2
- package/extensions/spec-kit-docguard/commands/generate.md +6 -1
- package/extensions/spec-kit-docguard/commands/guard.md +3 -2
- package/extensions/spec-kit-docguard/commands/sync.md +1 -1
- package/extensions/spec-kit-docguard/extension.yml +1 -1
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +14 -3
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +16 -5
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +8 -3
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +3 -2
- package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +6 -3
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +2 -2
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +4 -4
- package/package.json +2 -1
- package/schemas/docguard-agent-context-benchmark.schema.json +92 -0
- package/schemas/docguard-agent-context-result.schema.json +95 -0
- package/schemas/docguard-config.schema.json +1 -0
- package/schemas/docguard-evidence.schema.json +169 -0
- package/schemas/docguard-task-context.schema.json +144 -0
- package/templates/AGENTS.md.template +9 -4
- package/templates/ci/github-actions.yml +4 -4
- package/templates/commands/docguard.guard.md +5 -1
- package/templates/commands/docguard.review.md +6 -1
- 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 =
|
|
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 \\\`
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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**
|
|
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
|
-
|
|
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
|
|
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
|
|
784
|
-
- Test requirements in
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
+
safeWrite(full, next);
|
|
198
228
|
return { applied: true, detail: `${fix.doc}: #${fix.from} → #${fix.to}` };
|
|
199
229
|
}
|
|
200
230
|
|
package/cli/writers/sections.mjs
CHANGED
|
@@ -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
|
-
|
|
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. */
|
package/docs/ai-integration.md
CHANGED
|
@@ -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
|
-
|
|
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 --
|
|
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. **
|
|
187
|
-
|
|
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
|
-
|
|
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.
|
|
174
|
-
|
|
175
|
-
|
|
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`,
|
|
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.
|
package/docs/configuration.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
package/docs/quickstart.md
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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`,
|
|
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
|