docguard-cli 0.10.0 → 0.11.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 (44) hide show
  1. package/PHILOSOPHY.md +59 -106
  2. package/README.md +23 -1
  3. package/cli/commands/diagnose.mjs +157 -52
  4. package/cli/commands/fix.mjs +113 -1
  5. package/cli/commands/generate.mjs +91 -0
  6. package/cli/commands/hooks.mjs +40 -2
  7. package/cli/commands/score.mjs +22 -0
  8. package/cli/commands/sync.mjs +123 -0
  9. package/cli/docguard.mjs +22 -0
  10. package/cli/scanners/cdk.mjs +10 -0
  11. package/cli/scanners/frontend.mjs +438 -0
  12. package/cli/scanners/iac.mjs +235 -0
  13. package/cli/scanners/integrations.mjs +116 -0
  14. package/cli/scanners/memory-plan.mjs +242 -0
  15. package/cli/scanners/project-type.mjs +310 -0
  16. package/cli/scanners/routes.mjs +149 -0
  17. package/cli/scanners/schemas.mjs +174 -1
  18. package/cli/shared-ignore.mjs +29 -2
  19. package/cli/shared-source.mjs +2 -1
  20. package/cli/validators/api-surface.mjs +112 -37
  21. package/cli/validators/changelog.mjs +3 -2
  22. package/cli/validators/docs-coverage.mjs +125 -6
  23. package/cli/validators/docs-sync.mjs +49 -8
  24. package/cli/validators/metadata-sync.mjs +6 -1
  25. package/cli/validators/metrics-consistency.mjs +5 -2
  26. package/cli/validators/test-spec.mjs +129 -11
  27. package/cli/validators/todo-tracking.mjs +55 -2
  28. package/cli/writers/api-reference.mjs +101 -0
  29. package/cli/writers/mechanical.mjs +116 -0
  30. package/cli/writers/sections.mjs +148 -0
  31. package/commands/docguard.fix.md +19 -3
  32. package/docs/doc-sections.md +37 -0
  33. package/extensions/spec-kit-docguard/README.md +7 -4
  34. package/extensions/spec-kit-docguard/commands/fix.md +74 -0
  35. package/extensions/spec-kit-docguard/commands/generate.md +25 -2
  36. package/extensions/spec-kit-docguard/commands/sync.md +62 -0
  37. package/extensions/spec-kit-docguard/extension.yml +1 -1
  38. package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +13 -3
  39. package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
  40. package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
  41. package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
  42. package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +111 -0
  43. package/package.json +1 -1
  44. package/templates/ARCHITECTURE.md.template +52 -0
@@ -13,11 +13,66 @@
13
13
  * --auto Create skeleton files (NOT content) via init
14
14
  */
15
15
 
16
- import { existsSync, readFileSync, mkdirSync } from 'node:fs';
16
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
17
17
  import { resolve, basename, dirname } from 'node:path';
18
18
  import { execSync, execFileSync } from 'node:child_process';
19
19
  import { fileURLToPath } from 'node:url';
20
20
  import { c } from '../shared.mjs';
21
+ import { computeApiSurfaceDrift } from '../validators/api-surface.mjs';
22
+ import { removeEndpoints, hasGeneratedMarker } from '../writers/api-reference.mjs';
23
+ import { applyMechanicalFixes } from '../writers/mechanical.mjs';
24
+ import { runGuardInternal } from './guard.mjs';
25
+
26
+ const API_DOC = 'docs-canonical/API-REFERENCE.md';
27
+
28
+ /**
29
+ * Apply DETERMINISTIC, no-LLM API-surface fixes: remove endpoints documented in
30
+ * API-REFERENCE.md that the OpenAPI spec confirms no longer exist. Removes the
31
+ * summary-table row and the detail block. Never rewrites prose.
32
+ *
33
+ * Safety: only edits a doc carrying the `<!-- docguard:generated true -->`
34
+ * marker, unless `force` is set. Idempotent.
35
+ *
36
+ * @returns {{ applied: boolean, removed: Array<{method,path}>, skipped?: string }}
37
+ */
38
+ export function applyApiSurfaceWrites(projectDir, config, { force = false } = {}) {
39
+ const drift = computeApiSurfaceDrift(projectDir, config);
40
+ // Only spec-confirmed absences are safe to delete deterministically.
41
+ const removable = drift.confidence === 'spec' ? drift.documentedButAbsent : [];
42
+ if (removable.length === 0) return { applied: false, removed: [] };
43
+
44
+ const apiDocPath = resolve(projectDir, API_DOC);
45
+ if (!existsSync(apiDocPath)) return { applied: false, removed: [] };
46
+
47
+ const content = readFileSync(apiDocPath, 'utf-8');
48
+ if (!hasGeneratedMarker(content) && !force) {
49
+ return {
50
+ applied: false,
51
+ removed: [],
52
+ skipped: `${API_DOC} is not marked '<!-- docguard:generated true -->'. ` +
53
+ `Re-run with --force to edit it, or fix it via an AI agent (/docguard.fix --doc api-reference).`,
54
+ };
55
+ }
56
+
57
+ const { content: newContent, removed } = removeEndpoints(content, removable);
58
+ if (removed.length === 0 || newContent === content) {
59
+ return { applied: false, removed: [] }; // idempotent no-op
60
+ }
61
+
62
+ writeFileSync(apiDocPath, newContent, 'utf-8');
63
+ // Map removed keys back to {method,path} for reporting.
64
+ const removedEndpoints = removable.filter(e => removed.includes(`${e.method.toUpperCase()} ${normalizeForKey(e.path)}`));
65
+ return { applied: true, removed: removedEndpoints.length ? removedEndpoints : removable };
66
+ }
67
+
68
+ // Local mirror of api-doc normalizePath for matching removed keys (avoids an
69
+ // extra import cycle); only used for display reconciliation.
70
+ function normalizeForKey(p) {
71
+ let s = String(p).trim().replace(/^[|`'"\s]+/, '').replace(/[|`'"\s]+$/, '').split(/[?#]/)[0];
72
+ s = s.replace(/\{[^}/]+\}/g, '{}').replace(/:[^/]+/g, '{}');
73
+ if (s.length > 1) s = s.replace(/\/+$/, '');
74
+ return s;
75
+ }
21
76
 
22
77
  // ── Document Quality Definitions ───────────────────────────────────────────
23
78
  // What each doc SHOULD contain, and what to look for in the codebase
@@ -207,6 +262,57 @@ IMPORTANT: A new contributor should be able to follow this doc and have the proj
207
262
  },
208
263
  };
209
264
 
265
+ // ── Deterministic --write mode ───────────────────────────────────────────────
266
+
267
+ /**
268
+ * Collect every structured mechanical fix surfaced by the validators and apply
269
+ * them deterministically (no LLM). Covers: remove-endpoint (API-Surface),
270
+ * replace-count (Metrics-Consistency), replace-version (Metadata-Sync),
271
+ * insert-changelog-unreleased (Changelog).
272
+ * @returns {{ applied: object[], skipped: object[], total: number }}
273
+ */
274
+ export function applyAllMechanicalFixes(projectDir, config, { force = false } = {}) {
275
+ const guardData = runGuardInternal(projectDir, config);
276
+ const fixes = [];
277
+ for (const v of guardData.validators) {
278
+ if (Array.isArray(v.fixes)) fixes.push(...v.fixes);
279
+ }
280
+ const { applied, skipped } = applyMechanicalFixes(projectDir, fixes, { force });
281
+ return { applied, skipped, total: fixes.length };
282
+ }
283
+
284
+ function runWriteMode(projectDir, config, flags) {
285
+ const isJson = flags.format === 'json';
286
+ const { applied, skipped, total } = applyAllMechanicalFixes(projectDir, config, { force: flags.force });
287
+
288
+ if (isJson) {
289
+ console.log(JSON.stringify({
290
+ status: applied.length ? 'applied' : (total ? 'skipped' : 'clean'),
291
+ applied,
292
+ skipped,
293
+ }, null, 2));
294
+ return;
295
+ }
296
+
297
+ console.log(`${c.bold}🔧 DocGuard Fix --write — ${config.projectName}${c.reset}\n`);
298
+ if (total === 0) {
299
+ console.log(` ${c.green}✅ No mechanical fixes needed — the docs match the code.${c.reset}\n`);
300
+ return;
301
+ }
302
+ if (applied.length === 0) {
303
+ console.log(` ${c.dim}Nothing applied (idempotent or gated).${c.reset}`);
304
+ for (const s of skipped) console.log(` ${c.yellow}⚠ ${s.type}: ${s.reason}${c.reset}`);
305
+ console.log('');
306
+ return;
307
+ }
308
+ console.log(` ${c.green}✅ Applied ${applied.length} deterministic fix(es):${c.reset}`);
309
+ for (const a of applied) console.log(` ${c.green}✔ ${a.detail}${c.reset}`);
310
+ if (skipped.length) {
311
+ for (const s of skipped) console.log(` ${c.yellow}⚠ ${s.type}: ${s.reason}${c.reset}`);
312
+ }
313
+ console.log(`\n ${c.dim}Verify with ${c.cyan}docguard guard${c.dim}, then commit. Prose rewrites still need an AI agent (${c.cyan}/docguard.fix${c.dim}).${c.reset}\n`);
314
+ }
315
+
210
316
  // ── Main Entry ─────────────────────────────────────────────────────────────
211
317
 
212
318
  export function runFix(projectDir, config, flags) {
@@ -215,6 +321,12 @@ export function runFix(projectDir, config, flags) {
215
321
  const autoFix = flags.auto || false;
216
322
  const specificDoc = flags.doc || null;
217
323
 
324
+ // --write: deterministically APPLY mechanical fixes (no LLM). Currently:
325
+ // remove API-REFERENCE.md endpoints the OpenAPI spec confirms no longer exist.
326
+ if (flags.write) {
327
+ return runWriteMode(projectDir, config, flags);
328
+ }
329
+
218
330
  // If --doc flag is provided, generate a deep prompt for that specific document
219
331
  if (specificDoc) {
220
332
  return generateDocPrompt(projectDir, config, specificDoc);
@@ -11,6 +11,8 @@ import { c } from '../shared.mjs';
11
11
  import { detectDocTools } from '../scanners/doc-tools.mjs';
12
12
  import { scanRoutesDeep } from '../scanners/routes.mjs';
13
13
  import { scanSchemasDeep, generateERDiagram } from '../scanners/schemas.mjs';
14
+ import { buildMemoryPlan } from '../scanners/memory-plan.mjs';
15
+ import { upsertSection } from '../writers/sections.mjs';
14
16
 
15
17
  const IGNORE_DIRS = new Set([
16
18
  'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
@@ -111,7 +113,96 @@ function appendStandardsCitation(content, docName) {
111
113
  return content.trimEnd() + '\n' + footer;
112
114
  }
113
115
 
116
+ /**
117
+ * `docguard generate --plan` — AI-powered Generate.
118
+ * Builds the code-truth skeleton (marked sections) and emits the agent task
119
+ * manifest. `--format json` → machine manifest for an agent; text → summary.
120
+ * `--write` → scaffold the skeleton docs (code sections filled; prose sections
121
+ * inserted as agent-task placeholders), respecting human prose via markers.
122
+ */
123
+ export function runGeneratePlan(projectDir, config, flags) {
124
+ const plan = buildMemoryPlan(projectDir, config);
125
+
126
+ if (flags.format === 'json') {
127
+ console.log(JSON.stringify({
128
+ project: config.projectName,
129
+ profile: {
130
+ languages: plan.profile.languages,
131
+ frameworks: plan.profile.frameworks,
132
+ polyglot: plan.profile.polyglot,
133
+ kind: plan.profile.kind,
134
+ ecosystems: plan.profile.ecosystems.map(e => ({ dir: e.dir, language: e.language, framework: e.framework, kind: e.kind })),
135
+ },
136
+ surface: {
137
+ endpoints: plan.surface.endpoints.length,
138
+ entities: plan.surface.entities.length,
139
+ screens: plan.surface.screens.length,
140
+ components: plan.surface.components.length,
141
+ envVars: plan.surface.envVars.length,
142
+ },
143
+ docs: plan.docs.map(d => ({
144
+ path: d.path,
145
+ sections: d.sections.map(s => s.source === 'code'
146
+ ? { id: s.id, source: 'code' }
147
+ : { id: s.id, source: 'human', task: s.task, grounding: s.grounding }),
148
+ })),
149
+ agentTasks: plan.agentTasks,
150
+ timestamp: new Date().toISOString(),
151
+ }, null, 2));
152
+ return;
153
+ }
154
+
155
+ // --write: scaffold the skeleton docs with code sections + agent-task placeholders.
156
+ if (flags.write) {
157
+ const docsDir = resolve(projectDir, 'docs-canonical');
158
+ if (!existsSync(docsDir)) mkdirSync(docsDir, { recursive: true });
159
+ let wrote = 0;
160
+ for (const doc of plan.docs) {
161
+ const full = resolve(projectDir, doc.path);
162
+ const title = basename(doc.path, '.md').replace(/-/g, ' ');
163
+ let content = existsSync(full)
164
+ ? readFileSync(full, 'utf-8')
165
+ : `# ${title}\n\n<!-- docguard:generated true -->\n`;
166
+ for (const sec of doc.sections) {
167
+ const body = sec.source === 'code'
168
+ ? sec.body
169
+ : `> **AI task:** ${sec.task}\n<!-- docguard:pending agent writes this section -->`;
170
+ content = upsertSection(content, sec.id, body, { source: sec.source }).content;
171
+ }
172
+ writeFileSync(full, content, 'utf-8');
173
+ wrote++;
174
+ }
175
+ console.log(`${c.bold}🔮 DocGuard Generate --plan --write — ${config.projectName}${c.reset}`);
176
+ console.log(` ${c.green}✅ Scaffolded ${wrote} doc(s)${c.reset} with code-truth sections + ${plan.agentTasks.length} agent task(s).`);
177
+ console.log(` ${c.dim}Now run your AI agent (/docguard.fix) to write the prose sections, then ${c.cyan}docguard guard${c.dim}.${c.reset}\n`);
178
+ return;
179
+ }
180
+
181
+ // Text summary.
182
+ console.log(`${c.bold}🔮 DocGuard Generate Plan — ${config.projectName}${c.reset}`);
183
+ console.log(`${c.dim} ${plan.profile.polyglot ? 'Polyglot' : 'Single-language'}: ${plan.profile.languages.join(', ')} | frameworks: ${plan.profile.frameworks.join(', ') || '—'} | kind: ${plan.profile.kind}${c.reset}\n`);
184
+ console.log(` ${c.bold}Code-truth surface:${c.reset} ${plan.surface.endpoints.length} endpoints · ${plan.surface.entities.length} entities · ${plan.surface.screens.length} screens · ${plan.surface.components.length} components · ${plan.surface.envVars.length} env vars\n`);
185
+ console.log(` ${c.bold}Documents to build (${plan.docs.length}):${c.reset}`);
186
+ for (const d of plan.docs) {
187
+ const code = d.sections.filter(s => s.source === 'code').length;
188
+ const prose = d.sections.filter(s => s.source === 'human').length;
189
+ console.log(` ${c.cyan}${d.path}${c.reset} ${c.dim}(${code} code section(s), ${prose} agent task(s))${c.reset}`);
190
+ }
191
+ console.log(`\n ${c.bold}🤖 Agent tasks (${plan.agentTasks.length}):${c.reset} ${c.dim}prose the AI must write, grounded in scanned facts.${c.reset}`);
192
+ for (const t of plan.agentTasks) {
193
+ console.log(` ${c.dim}• [${t.doc} → ${t.sectionId}] ${t.instruction}${c.reset}`);
194
+ }
195
+ console.log(`\n ${c.dim}Scaffold the skeleton: ${c.cyan}docguard generate --plan --write${c.dim} · Machine manifest: ${c.cyan}--plan --format json${c.reset}\n`);
196
+ }
197
+
114
198
  export function runGenerate(projectDir, config, flags) {
199
+ // --plan: emit the AI-powered "memory plan" — the agent task manifest. The CLI
200
+ // builds the code-truth skeleton (marked sections) + tells the agent exactly
201
+ // what prose to write per section. This is the language-aware Generate path.
202
+ if (flags.plan) {
203
+ return runGeneratePlan(projectDir, config, flags);
204
+ }
205
+
115
206
  console.log(`${c.bold}🔮 DocGuard Generate — ${config.projectName}${c.reset}`);
116
207
  console.log(`${c.dim} Directory: ${projectDir}${c.reset}`);
117
208
  console.log(`${c.dim} Scanning codebase to generate canonical documentation...${c.reset}\n`);
@@ -128,6 +128,40 @@ exit 0
128
128
  },
129
129
  };
130
130
 
131
+ // Auto-fix variant of the pre-commit hook: apply deterministic fixes, re-stage,
132
+ // then validate. Installed with: docguard hooks --type pre-commit --auto-fix
133
+ const PRE_COMMIT_AUTOFIX = `#!/bin/sh
134
+ # DocGuard pre-commit hook (auto-fix mode)
135
+ # Applies deterministic (no-LLM) fixes, then validates.
136
+ # Install: docguard hooks --type pre-commit --auto-fix
137
+ # Remove: rm .git/hooks/pre-commit
138
+
139
+ RUN="npx docguard-cli"
140
+ if command -v docguard >/dev/null 2>&1; then RUN="docguard"; fi
141
+
142
+ echo "🛡️ DocGuard: applying mechanical fixes…"
143
+ # 1. Deterministically remove stale documented endpoints (safe, no AI).
144
+ $RUN fix --write
145
+ # 2. Re-stage anything DocGuard rewrote so the fix is part of THIS commit.
146
+ git add docs-canonical/ 2>/dev/null
147
+
148
+ # 3. Validate.
149
+ $RUN guard
150
+ EXIT_CODE=$?
151
+
152
+ if [ $EXIT_CODE -eq 1 ]; then
153
+ echo ""
154
+ echo "❌ DocGuard guard FAILED — commit blocked."
155
+ echo " Remaining issues need an AI agent (content rewrites, not mechanical):"
156
+ echo " Run: $RUN diagnose (emits ready-to-paste agent fix prompts)"
157
+ echo " To skip: git commit --no-verify"
158
+ exit 1
159
+ elif [ $EXIT_CODE -eq 2 ]; then
160
+ echo "⚠️ DocGuard guard found warnings — commit allowed"
161
+ fi
162
+ exit 0
163
+ `;
164
+
131
165
  export function runHooks(projectDir, config, flags) {
132
166
  console.log(`${c.bold}🪝 DocGuard Hooks — ${config.projectName}${c.reset}`);
133
167
  console.log(`${c.dim} Directory: ${projectDir}${c.reset}\n`);
@@ -208,9 +242,13 @@ export function runHooks(projectDir, config, flags) {
208
242
  continue;
209
243
  }
210
244
 
211
- writeFileSync(hookPath, HOOKS[name].content, 'utf-8');
245
+ // pre-commit supports an auto-fix variant (applies mechanical fixes first).
246
+ const useAutofix = name === 'pre-commit' && flags.autoFix;
247
+ const content = useAutofix ? PRE_COMMIT_AUTOFIX : HOOKS[name].content;
248
+ writeFileSync(hookPath, content, 'utf-8');
212
249
  chmodSync(hookPath, 0o755); // Make executable
213
- console.log(` ${c.green}✅ ${name}${c.reset}: ${HOOKS[name].description}`);
250
+ const desc = useAutofix ? 'Apply mechanical fixes (fix --write) then guard' : HOOKS[name].description;
251
+ console.log(` ${c.green}✅ ${name}${c.reset}: ${desc}`);
214
252
  installed++;
215
253
  }
216
254
 
@@ -26,12 +26,30 @@ export function runScore(projectDir, config, flags) {
26
26
 
27
27
  const { scores, totalScore, grade, details } = calcAllScores(projectDir, config);
28
28
 
29
+ // ── "Memory" framing: split signals into Completeness vs Accuracy ──
30
+ // Completeness = "is the memory whole?" Accuracy = "does it match code?"
31
+ // No weight changes — just a derived view of the existing per-category scores.
32
+ const COMPLETENESS = new Set(['structure', 'docQuality']);
33
+ const memory = (() => {
34
+ let cW = 0, cP = 0, aW = 0, aP = 0;
35
+ for (const [cat, s] of Object.entries(scores)) {
36
+ const w = WEIGHTS[cat] || 0;
37
+ if (COMPLETENESS.has(cat)) { cW += w; cP += s * w; }
38
+ else { aW += w; aP += s * w; }
39
+ }
40
+ return {
41
+ completeness: cW ? Math.round(cP / cW) : 0,
42
+ accuracy: aW ? Math.round(aP / aW) : 0,
43
+ };
44
+ })();
45
+
29
46
  // ── Display Results ──
30
47
  if (flags.format === 'json') {
31
48
  const result = {
32
49
  project: config.projectName,
33
50
  score: totalScore,
34
51
  grade,
52
+ memory,
35
53
  categories: {},
36
54
  };
37
55
  for (const [cat, score] of Object.entries(scores)) {
@@ -39,6 +57,7 @@ export function runScore(projectDir, config, flags) {
39
57
  score,
40
58
  weight: WEIGHTS[cat],
41
59
  weighted: Math.round((score / 100) * WEIGHTS[cat]),
60
+ axis: COMPLETENESS.has(cat) ? 'completeness' : 'accuracy',
42
61
  };
43
62
  }
44
63
  console.log(JSON.stringify(result, null, 2));
@@ -60,6 +79,9 @@ export function runScore(projectDir, config, flags) {
60
79
 
61
80
  const gradeColor = totalScore >= 80 ? c.green : totalScore >= 60 ? c.yellow : c.red;
62
81
  console.log(` ${gradeColor}${c.bold}CDD Maturity Score: ${totalScore}/100 (${grade})${c.reset}`);
82
+ // Memory framing: is the documentation memory COMPLETE and ACCURATE?
83
+ const memColor = (s) => s >= 80 ? c.green : s >= 60 ? c.yellow : c.red;
84
+ console.log(` ${c.dim}Memory:${c.reset} ${memColor(memory.completeness)}Completeness ${memory.completeness}%${c.reset} ${c.dim}·${c.reset} ${memColor(memory.accuracy)}Accuracy ${memory.accuracy}%${c.reset}`);
63
85
 
64
86
  // Grade description
65
87
  const descriptions = {
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Sync Command — keep the documentation memory ALWAYS UP TO DATE.
3
+ *
4
+ * Re-derives the code-truth surface (endpoints, entities, screens, tech-stack,
5
+ * env vars) and refreshes the matching `source=code` sections of existing
6
+ * canonical docs IN PLACE — mechanically, no LLM, idempotent. Human prose is
7
+ * never touched (it lives outside markers / in `source=human` sections).
8
+ *
9
+ * When a code section changes, the prose sections in that doc are flagged for
10
+ * agent review (e.g. "endpoints changed → re-read the API overview").
11
+ *
12
+ * Default is a DRY RUN (preview); `--write` applies. `--since <ref>` adds the
13
+ * git diff as context. Only edits docguard:generated docs unless `--force`.
14
+ */
15
+
16
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
17
+ import { resolve } from 'node:path';
18
+ import { execFileSync } from 'node:child_process';
19
+ import { c } from '../shared.mjs';
20
+ import { buildMemoryPlan } from '../scanners/memory-plan.mjs';
21
+ import { getSection, replaceSection } from '../writers/sections.mjs';
22
+ import { hasGeneratedMarker } from '../writers/api-reference.mjs';
23
+
24
+ function gitChangedFiles(projectDir, since) {
25
+ const run = (args) => {
26
+ try {
27
+ return execFileSync('git', args, { cwd: projectDir, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] })
28
+ .split('\n').map(s => s.trim()).filter(Boolean);
29
+ } catch { return null; }
30
+ };
31
+ const committed = run(['diff', '--name-only', `${since}...HEAD`]);
32
+ if (committed === null) return null;
33
+ const working = run(['diff', '--name-only', since]) || [];
34
+ return [...new Set([...committed, ...working])];
35
+ }
36
+
37
+ export function runSync(projectDir, config, flags) {
38
+ const plan = buildMemoryPlan(projectDir, config);
39
+ const apply = !!flags.write;
40
+ const isJson = flags.format === 'json';
41
+ const changed = flags.since ? gitChangedFiles(projectDir, flags.since) : null;
42
+
43
+ const updates = []; // { doc, section, status }
44
+ const reviews = []; // { doc, section, reason }
45
+ const skipped = []; // { doc, reason }
46
+
47
+ for (const doc of plan.docs) {
48
+ const full = resolve(projectDir, doc.path);
49
+ if (!existsSync(full)) {
50
+ skipped.push({ doc: doc.path, reason: 'not present — run `generate --plan --write` to create it' });
51
+ continue;
52
+ }
53
+ let content = readFileSync(full, 'utf-8');
54
+ if (!hasGeneratedMarker(content) && !flags.force) {
55
+ skipped.push({ doc: doc.path, reason: 'not marked docguard:generated (use --force to sync anyway)' });
56
+ continue;
57
+ }
58
+
59
+ let docChanged = false;
60
+ let codeSectionChanged = false;
61
+ for (const sec of doc.sections) {
62
+ if (sec.source !== 'code') continue;
63
+ const existing = getSection(content, sec.id);
64
+ if (!existing) continue; // sync refreshes sections that already exist
65
+ if (existing.body.trim() === String(sec.body).trim()) continue; // already current
66
+ codeSectionChanged = true;
67
+ updates.push({ doc: doc.path, section: sec.id, status: apply ? 'updated' : 'stale' });
68
+ if (apply) { content = replaceSection(content, sec.id, sec.body).content; docChanged = true; }
69
+ }
70
+
71
+ // If code changed, the prose around it may need an agent's eyes.
72
+ if (codeSectionChanged) {
73
+ for (const sec of doc.sections) {
74
+ if (sec.source === 'human') {
75
+ reviews.push({ doc: doc.path, section: sec.id, reason: 'a code section in this doc changed — review the prose' });
76
+ }
77
+ }
78
+ }
79
+
80
+ if (apply && docChanged) writeFileSync(full, content, 'utf-8');
81
+ }
82
+
83
+ if (isJson) {
84
+ console.log(JSON.stringify({
85
+ project: config.projectName,
86
+ since: flags.since || null,
87
+ changedFiles: changed,
88
+ applied: apply,
89
+ updates,
90
+ reviews,
91
+ skipped,
92
+ timestamp: new Date().toISOString(),
93
+ }, null, 2));
94
+ return;
95
+ }
96
+
97
+ console.log(`${c.bold}🔄 DocGuard Sync — ${config.projectName}${c.reset}`);
98
+ if (flags.since) {
99
+ const n = changed === null ? 'git unavailable' : `${changed.length} file(s) changed since ${flags.since}`;
100
+ console.log(`${c.dim} ${n}${c.reset}`);
101
+ }
102
+ console.log(`${c.dim} ${apply ? 'Applying' : 'Dry run (use --write to apply)'}${c.reset}\n`);
103
+
104
+ if (updates.length === 0) {
105
+ console.log(` ${c.green}✅ Documentation memory is up to date — no code-truth sections drifted.${c.reset}\n`);
106
+ } else {
107
+ console.log(` ${apply ? c.green : c.yellow}${apply ? '✅ Refreshed' : '⚠️ Stale'} ${updates.length} code-truth section(s):${c.reset}`);
108
+ for (const u of updates) console.log(` ${apply ? c.green : c.yellow}${apply ? '↻' : '•'} ${u.doc} → ${u.section}${c.reset}`);
109
+ if (reviews.length > 0) {
110
+ console.log(`\n ${c.bold}🤖 Prose to review (${reviews.length}) — code changed near these sections:${c.reset}`);
111
+ for (const r of reviews) console.log(` ${c.dim}• ${r.doc} → ${r.section}${c.reset}`);
112
+ console.log(` ${c.dim}Run your AI agent (/docguard.fix) to refresh the prose, then ${c.cyan}docguard guard${c.dim}.${c.reset}`);
113
+ }
114
+ if (!apply) console.log(`\n ${c.dim}Apply mechanical refreshes: ${c.cyan}docguard sync --write${c.reset}`);
115
+ console.log('');
116
+ }
117
+
118
+ if (skipped.length > 0 && flags.verbose) {
119
+ console.log(` ${c.dim}Skipped:${c.reset}`);
120
+ for (const s of skipped) console.log(` ${c.dim}- ${s.doc}: ${s.reason}${c.reset}`);
121
+ console.log('');
122
+ }
123
+ }
package/cli/docguard.mjs CHANGED
@@ -35,6 +35,7 @@ import { runCI } from './commands/ci.mjs';
35
35
  import { runFix } from './commands/fix.mjs';
36
36
  import { runWatch } from './commands/watch.mjs';
37
37
  import { runDiagnose } from './commands/diagnose.mjs';
38
+ import { runSync } from './commands/sync.mjs';
38
39
  import { runPublish } from './commands/publish.mjs';
39
40
  import { runTrace } from './commands/trace.mjs';
40
41
  import { runLlms } from './commands/llms.mjs';
@@ -236,6 +237,10 @@ ${c.bold}Enforcement:${c.reset}
236
237
  ${c.green}guard${c.reset} Validate project against canonical docs (51+ checks)
237
238
  ${c.green}diagnose${c.reset} AI orchestrator — guard → fix in one command
238
239
 
240
+ ${c.bold}Memory (build & maintain docs):${c.reset}
241
+ ${c.green}generate --plan${c.reset} AI-powered: scan any project, emit agent task manifest + skeleton
242
+ ${c.green}sync${c.reset} Refresh code-truth doc sections to match current code (always up to date)
243
+
239
244
  ${c.bold}Analysis:${c.reset}
240
245
  ${c.green}score${c.reset} CDD maturity score (0-100)
241
246
  ${c.green}trace${c.reset} Requirements traceability matrix
@@ -267,6 +272,13 @@ ${c.bold}Options:${c.reset}
267
272
  --threshold <n> Minimum score for CI pass (used with ci command)
268
273
  --fail-on-warning Fail CI on warnings (used with ci command)
269
274
  --auto Auto-fix what's possible (used with fix command)
275
+ --write Apply deterministic fixes in place (fix command): removes
276
+ documented endpoints the OpenAPI spec confirms are gone.
277
+ Only edits docguard:generated docs unless --force.
278
+ --plan AI-powered Generate (generate command): scan any project
279
+ (JS/Python/Rust/Go/Java/…), emit the agent task manifest +
280
+ code-truth skeleton. Add --write to scaffold, --format json
281
+ for the machine-readable manifest.
270
282
  --doc <name> Generate AI prompt for specific doc (architecture, security, etc.)
271
283
  --profile <p> Compliance profile: starter, standard, enterprise (init command)
272
284
  --tax Show estimated documentation maintenance cost (with score)
@@ -346,6 +358,13 @@ async function main() {
346
358
  flags.failOnWarning = true;
347
359
  } else if (args[i] === '--auto') {
348
360
  flags.auto = true;
361
+ } else if (args[i] === '--write') {
362
+ flags.write = true;
363
+ } else if (args[i] === '--plan') {
364
+ flags.plan = true;
365
+ } else if (args[i] === '--since' && args[i + 1]) {
366
+ flags.since = args[i + 1];
367
+ i++;
349
368
  } else if (args[i] === '--doc' && args[i + 1]) {
350
369
  flags.doc = args[i + 1];
351
370
  i++;
@@ -443,6 +462,9 @@ async function main() {
443
462
  case 'watch':
444
463
  runWatch(projectDir, config, flags);
445
464
  break;
465
+ case 'sync':
466
+ runSync(projectDir, config, flags);
467
+ break;
446
468
  case 'publish':
447
469
  case 'pub':
448
470
  runPublish(projectDir, config, flags);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * CDK Detector — Re-export shim.
3
+ *
4
+ * The CDK-specific detector has been generalized into a multi-tool IaC
5
+ * detector at cli/scanners/iac.mjs covering CDK, Terraform, Pulumi, SAM,
6
+ * and Serverless Framework. This module re-exports the CDK-only API for
7
+ * backward compatibility. New code should import from iac.mjs directly.
8
+ */
9
+
10
+ export { detectCDK, hasInfrastructureHeading } from './iac.mjs';