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.
- package/PHILOSOPHY.md +59 -106
- package/README.md +23 -1
- package/cli/commands/diagnose.mjs +157 -52
- package/cli/commands/fix.mjs +113 -1
- package/cli/commands/generate.mjs +91 -0
- package/cli/commands/hooks.mjs +40 -2
- package/cli/commands/score.mjs +22 -0
- package/cli/commands/sync.mjs +123 -0
- package/cli/docguard.mjs +22 -0
- package/cli/scanners/cdk.mjs +10 -0
- package/cli/scanners/frontend.mjs +438 -0
- package/cli/scanners/iac.mjs +235 -0
- package/cli/scanners/integrations.mjs +116 -0
- package/cli/scanners/memory-plan.mjs +242 -0
- package/cli/scanners/project-type.mjs +310 -0
- package/cli/scanners/routes.mjs +149 -0
- package/cli/scanners/schemas.mjs +174 -1
- package/cli/shared-ignore.mjs +29 -2
- package/cli/shared-source.mjs +2 -1
- package/cli/validators/api-surface.mjs +112 -37
- package/cli/validators/changelog.mjs +3 -2
- package/cli/validators/docs-coverage.mjs +125 -6
- package/cli/validators/docs-sync.mjs +49 -8
- package/cli/validators/metadata-sync.mjs +6 -1
- package/cli/validators/metrics-consistency.mjs +5 -2
- package/cli/validators/test-spec.mjs +129 -11
- package/cli/validators/todo-tracking.mjs +55 -2
- package/cli/writers/api-reference.mjs +101 -0
- package/cli/writers/mechanical.mjs +116 -0
- package/cli/writers/sections.mjs +148 -0
- package/commands/docguard.fix.md +19 -3
- package/docs/doc-sections.md +37 -0
- package/extensions/spec-kit-docguard/README.md +7 -4
- package/extensions/spec-kit-docguard/commands/fix.md +74 -0
- package/extensions/spec-kit-docguard/commands/generate.md +25 -2
- package/extensions/spec-kit-docguard/commands/sync.md +62 -0
- package/extensions/spec-kit-docguard/extension.yml +1 -1
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +13 -3
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +111 -0
- package/package.json +1 -1
- package/templates/ARCHITECTURE.md.template +52 -0
package/cli/commands/fix.mjs
CHANGED
|
@@ -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`);
|
package/cli/commands/hooks.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
package/cli/commands/score.mjs
CHANGED
|
@@ -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';
|