docguard-cli 0.24.0 → 0.25.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/cli/commands/generate.mjs +67 -1
- package/cli/commands/init.mjs +6 -2
- package/cli/commands/sync.mjs +6 -0
- package/cli/docguard.mjs +141 -6
- package/cli/scanners/project-type.mjs +11 -4
- package/cli/shared-ignore.mjs +7 -1
- package/cli/shared.mjs +32 -0
- package/cli/validators/generated-staleness.mjs +16 -1
- package/extensions/spec-kit-docguard/extension.yml +1 -5
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
- 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 +2 -2
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +2 -2
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +2 -2
- package/package.json +1 -1
|
@@ -40,10 +40,40 @@ function backupFile(filePath) {
|
|
|
40
40
|
* Call this instead of raw writeFileSync when generating docs.
|
|
41
41
|
*/
|
|
42
42
|
function safeWrite(filePath, content) {
|
|
43
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
43
44
|
backupFile(filePath);
|
|
44
45
|
writeFileSync(filePath, content, 'utf-8');
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* B7 (field report): after generate emits canonical docs, register them in
|
|
50
|
+
* `.docguard.json` requiredFiles.canonical so `guard` doesn't immediately flag
|
|
51
|
+
* the generator's OWN output as an "orphaned" doc ("exists but not in your
|
|
52
|
+
* requiredFiles"). Only ADDS (never removes/deletes), only docs-canonical/*.md
|
|
53
|
+
* that actually exist on disk, and only when a config file already exists (init
|
|
54
|
+
* owns config creation). Idempotent — a second run with nothing new is a no-op.
|
|
55
|
+
* @returns {number} count of paths newly registered.
|
|
56
|
+
*/
|
|
57
|
+
function registerGeneratedCanonicalDocs(projectDir, candidatePaths) {
|
|
58
|
+
const cfgPath = resolve(projectDir, '.docguard.json');
|
|
59
|
+
if (!existsSync(cfgPath)) return 0;
|
|
60
|
+
let cfg;
|
|
61
|
+
try { cfg = JSON.parse(readFileSync(cfgPath, 'utf-8')); } catch { return 0; }
|
|
62
|
+
const canon = [...new Set(candidatePaths)].filter(p =>
|
|
63
|
+
p.startsWith('docs-canonical/') && p.endsWith('.md') && existsSync(resolve(projectDir, p))
|
|
64
|
+
);
|
|
65
|
+
if (canon.length === 0) return 0;
|
|
66
|
+
if (!cfg.requiredFiles || typeof cfg.requiredFiles !== 'object') cfg.requiredFiles = {};
|
|
67
|
+
const existing = Array.isArray(cfg.requiredFiles.canonical) ? cfg.requiredFiles.canonical : [];
|
|
68
|
+
const seen = new Set(existing);
|
|
69
|
+
let added = 0;
|
|
70
|
+
for (const p of canon) if (!seen.has(p)) { existing.push(p); seen.add(p); added++; }
|
|
71
|
+
if (added === 0) return 0;
|
|
72
|
+
cfg.requiredFiles.canonical = existing;
|
|
73
|
+
try { writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n', 'utf-8'); } catch { return 0; }
|
|
74
|
+
return added;
|
|
75
|
+
}
|
|
76
|
+
|
|
47
77
|
const CODE_EXTENSIONS = new Set([
|
|
48
78
|
'.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
|
|
49
79
|
'.py', '.java', '.go', '.rs', '.rb', '.php', '.cs',
|
|
@@ -113,6 +143,19 @@ function appendStandardsCitation(content, docName) {
|
|
|
113
143
|
return content.trimEnd() + '\n' + footer;
|
|
114
144
|
}
|
|
115
145
|
|
|
146
|
+
/**
|
|
147
|
+
* F1 (field report): web-shaped surface (HTTP endpoints, SDK deps, routes)
|
|
148
|
+
* auto-extracted from a cli/library/unknown-kind project is often pattern-
|
|
149
|
+
* matches in the project's OWN source (e.g. a scanner/linter whose code mentions
|
|
150
|
+
* express, boto3, jwt as detection strings), not real usage. We do NOT suppress
|
|
151
|
+
* it — that could hide a real surface (a false-green) — we flag it 'low'
|
|
152
|
+
* confidence so the surface is verified before being documented. Web kinds
|
|
153
|
+
* (webapp/api/service) stay 'normal'.
|
|
154
|
+
*/
|
|
155
|
+
function surfaceConfidence(kind) {
|
|
156
|
+
return ['webapp', 'api', 'service'].includes(kind) ? 'normal' : 'low';
|
|
157
|
+
}
|
|
158
|
+
|
|
116
159
|
/**
|
|
117
160
|
* `docguard generate --plan` — AI-powered Generate.
|
|
118
161
|
* Builds the code-truth skeleton (marked sections) and emits the agent task
|
|
@@ -139,6 +182,7 @@ export function runGeneratePlan(projectDir, config, flags) {
|
|
|
139
182
|
screens: plan.surface.screens.length,
|
|
140
183
|
components: plan.surface.components.length,
|
|
141
184
|
envVars: plan.surface.envVars.length,
|
|
185
|
+
confidence: surfaceConfidence(plan.profile.kind),
|
|
142
186
|
},
|
|
143
187
|
docs: plan.docs.map(d => ({
|
|
144
188
|
path: d.path,
|
|
@@ -169,11 +213,18 @@ export function runGeneratePlan(projectDir, config, flags) {
|
|
|
169
213
|
: `> **AI task:** ${sec.task}\n<!-- docguard:pending agent writes this section -->`;
|
|
170
214
|
content = upsertSection(content, sec.id, body, { source: sec.source }).content;
|
|
171
215
|
}
|
|
172
|
-
|
|
216
|
+
// Route through safeWrite: creates the parent dir (docs-implementation/ may
|
|
217
|
+
// not exist yet — was an ENOENT crash) and snapshots a .bak before writing.
|
|
218
|
+
safeWrite(full, content);
|
|
173
219
|
wrote++;
|
|
174
220
|
}
|
|
221
|
+
// B7: register the scaffolded canonical docs so guard doesn't flag them.
|
|
222
|
+
const registered = registerGeneratedCanonicalDocs(projectDir, plan.docs.map(d => d.path));
|
|
175
223
|
console.log(`${c.bold}🔮 DocGuard Generate --plan --write — ${config.projectName}${c.reset}`);
|
|
176
224
|
console.log(` ${c.green}✅ Scaffolded ${wrote} doc(s)${c.reset} with code-truth sections + ${plan.agentTasks.length} agent task(s).`);
|
|
225
|
+
if (registered > 0) {
|
|
226
|
+
console.log(` ${c.dim}Registered ${registered} canonical doc(s) in .docguard.json requiredFiles.${c.reset}`);
|
|
227
|
+
}
|
|
177
228
|
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
229
|
return;
|
|
179
230
|
}
|
|
@@ -182,6 +233,10 @@ export function runGeneratePlan(projectDir, config, flags) {
|
|
|
182
233
|
console.log(`${c.bold}🔮 DocGuard Generate Plan — ${config.projectName}${c.reset}`);
|
|
183
234
|
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
235
|
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`);
|
|
236
|
+
const webSurface = plan.surface.endpoints.length + plan.surface.entities.length + plan.surface.screens.length + plan.surface.components.length;
|
|
237
|
+
if (surfaceConfidence(plan.profile.kind) === 'low' && webSurface > 0) {
|
|
238
|
+
console.log(` ${c.yellow}⚠️ Low-confidence surface:${c.reset} ${c.dim}this looks like a ${plan.profile.kind} (not a web app), so the HTTP/SDK/route surface above may be pattern-matches in your OWN source — not real usage. Verify before documenting; pin any corrected code section with ${c.cyan}pinned="reason"${c.dim}.${c.reset}\n`);
|
|
239
|
+
}
|
|
185
240
|
console.log(` ${c.bold}Documents to build (${plan.docs.length}):${c.reset}`);
|
|
186
241
|
for (const d of plan.docs) {
|
|
187
242
|
const code = d.sections.filter(s => s.source === 'code').length;
|
|
@@ -301,8 +356,19 @@ export function runGenerate(projectDir, config, flags) {
|
|
|
301
356
|
created += rootResults.created;
|
|
302
357
|
skipped += rootResults.skipped;
|
|
303
358
|
|
|
359
|
+
// B7: keep guard coherent — register the canonical docs we emitted so the
|
|
360
|
+
// traceability validator doesn't flag the generator's own output.
|
|
361
|
+
const registered = registerGeneratedCanonicalDocs(projectDir, [
|
|
362
|
+
'docs-canonical/ARCHITECTURE.md', 'docs-canonical/API-REFERENCE.md',
|
|
363
|
+
'docs-canonical/DATA-MODEL.md', 'docs-canonical/ENVIRONMENT.md',
|
|
364
|
+
'docs-canonical/TEST-SPEC.md', 'docs-canonical/SECURITY.md',
|
|
365
|
+
]);
|
|
366
|
+
|
|
304
367
|
console.log(`\n${c.bold} ─────────────────────────────────────${c.reset}`);
|
|
305
368
|
console.log(` ${c.green}Generated: ${created}${c.reset} Skipped: ${skipped} (already exist)`);
|
|
369
|
+
if (registered > 0) {
|
|
370
|
+
console.log(` ${c.dim}Registered ${registered} canonical doc(s) in .docguard.json requiredFiles.${c.reset}`);
|
|
371
|
+
}
|
|
306
372
|
if (docTools._detected.length > 0) {
|
|
307
373
|
console.log(` ${c.dim}Leveraged: ${docTools._detected.join(', ')} (existing tools detected)${c.reset}`);
|
|
308
374
|
}
|
package/cli/commands/init.mjs
CHANGED
|
@@ -118,6 +118,7 @@ function shouldRunGenerate(projectDir, flags) {
|
|
|
118
118
|
if (flags.skipPrompts) return false; // non-interactive (CI) keeps deterministic skeleton path
|
|
119
119
|
if (flags.wizard) return false; // wizard has its own scan step
|
|
120
120
|
if (flags.profile) return false; // explicit profile = user knows what they want
|
|
121
|
+
if (flags.fix) return false; // --fix = deterministic create-missing-from-templates (headless)
|
|
121
122
|
|
|
122
123
|
// If canonical docs already exist, this is a re-init, not a first-run.
|
|
123
124
|
const canonicalDir = resolve(projectDir, 'docs-canonical');
|
|
@@ -198,8 +199,11 @@ export async function runInit(projectDir, config, flags) {
|
|
|
198
199
|
|
|
199
200
|
let selectedDocs;
|
|
200
201
|
|
|
201
|
-
if (flags.skipPrompts || flags.force) {
|
|
202
|
-
// Non-interactive — use profile defaults
|
|
202
|
+
if (flags.skipPrompts || flags.force || flags.fix) {
|
|
203
|
+
// Non-interactive — use profile defaults. `--fix` lands here too: its
|
|
204
|
+
// documented contract is "auto-create missing files from templates", so it
|
|
205
|
+
// must never block on prompts (CI / headless / agent use). The create-loop
|
|
206
|
+
// below already skips existing files, so --fix only fills gaps.
|
|
203
207
|
const profileCanonical = profile.requiredFiles?.canonical || allDocs.map(d => d.file);
|
|
204
208
|
selectedDocs = allDocs.filter(d => profileCanonical.includes(d.file));
|
|
205
209
|
console.log(` ${c.dim}Non-interactive mode — using ${profileName} profile defaults${c.reset}\n`);
|
package/cli/commands/sync.mjs
CHANGED
|
@@ -104,6 +104,12 @@ export function runSync(projectDir, config, flags) {
|
|
|
104
104
|
if (sec.source !== 'code') continue;
|
|
105
105
|
const existing = getSection(content, sec.id);
|
|
106
106
|
if (!existing) continue; // sync refreshes sections that already exist
|
|
107
|
+
// B5: a pinned section is intentionally hand-maintained — never revert it.
|
|
108
|
+
// (Pairs with the Generated-Staleness exemption for the same marker.)
|
|
109
|
+
if (existing.attrs?.pinned !== undefined) {
|
|
110
|
+
skipped.push({ doc: doc.path, reason: `section ${sec.id} is pinned (hand-maintained) — not synced` });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
107
113
|
if (existing.body.trim() === String(sec.body).trim()) continue; // already current
|
|
108
114
|
// L-1: when --since is provided, only update sections whose underlying
|
|
109
115
|
// source files appear in the changed set. Avoids spurious updates when
|
package/cli/docguard.mjs
CHANGED
|
@@ -125,7 +125,7 @@ ${c.bold}Options:${c.reset}
|
|
|
125
125
|
code-truth skeleton. Add --write to scaffold, --format json
|
|
126
126
|
for the machine-readable manifest.
|
|
127
127
|
--doc <name> Generate AI prompt for specific doc (architecture, security, etc.)
|
|
128
|
-
--profile <p>
|
|
128
|
+
--profile <p> Profile: starter, standard, cli, library, enterprise (init command)
|
|
129
129
|
--tax Show estimated documentation maintenance cost (with score)
|
|
130
130
|
--help Show this help message
|
|
131
131
|
--version Show version
|
|
@@ -133,6 +133,8 @@ ${c.bold}Options:${c.reset}
|
|
|
133
133
|
${c.bold}Profiles:${c.reset}
|
|
134
134
|
${c.green}starter${c.reset} Minimal CDD — just ARCHITECTURE.md + CHANGELOG (side projects)
|
|
135
135
|
${c.green}standard${c.reset} Full CDD — all 5 canonical docs (default, team projects)
|
|
136
|
+
${c.green}cli${c.reset} CLI tool — no HTTP API / DB (ARCHITECTURE, TEST-SPEC, SECURITY, ENVIRONMENT)
|
|
137
|
+
${c.green}library${c.reset} Library — public API, no HTTP/DB (ARCHITECTURE, API-REFERENCE, TEST-SPEC)
|
|
136
138
|
${c.green}enterprise${c.reset} Strict CDD — all docs + all validators + freshness enforced
|
|
137
139
|
|
|
138
140
|
${c.bold}Examples:${c.reset}
|
|
@@ -158,6 +160,135 @@ ${c.bold}Learn more:${c.reset}
|
|
|
158
160
|
`);
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
// ── Per-command help (v0.24, field report B6) ───────────────────────────────
|
|
164
|
+
// `docguard <command> --help` now prints the flags + examples for THAT command,
|
|
165
|
+
// so a flag like `generate --plan --write` or `init --skeleton` is discoverable.
|
|
166
|
+
// Only flags that genuinely apply to each command are listed (derived from the
|
|
167
|
+
// vetted global help above). Commands without a focused entry fall back to the
|
|
168
|
+
// global help, which still lists them.
|
|
169
|
+
const COMMAND_HELP = {
|
|
170
|
+
init: {
|
|
171
|
+
summary: 'Bootstrap CDD docs — smart-scans existing code, or writes blank templates.',
|
|
172
|
+
usage: 'docguard init [--skeleton|--wizard] [--profile <p>] [--with <name>] [--fix]',
|
|
173
|
+
flags: [
|
|
174
|
+
['--skeleton', 'Blank templates instead of the smart scan-and-propose'],
|
|
175
|
+
['--wizard', 'Guided interactive onboarding'],
|
|
176
|
+
['--with <name>', 'Add a scaffolder: agents, hooks, ci, badge, llms, publish'],
|
|
177
|
+
['--profile <p>', 'starter | standard | cli | library | enterprise (default: standard)'],
|
|
178
|
+
['--skip-prompts', 'Non-interactive; create the profile defaults (CI)'],
|
|
179
|
+
['--fix', 'Headless: create any missing required docs from templates'],
|
|
180
|
+
['--force', 'Overwrite existing files (.bak backup kept)'],
|
|
181
|
+
],
|
|
182
|
+
examples: ['docguard init', 'docguard init --skeleton', 'docguard init --profile starter --skip-prompts', 'docguard init --with ci'],
|
|
183
|
+
},
|
|
184
|
+
generate: {
|
|
185
|
+
summary: 'Reverse-engineer canonical docs from existing code.',
|
|
186
|
+
usage: 'docguard generate [--plan [--write] [--format json]] [--force]',
|
|
187
|
+
flags: [
|
|
188
|
+
['--plan', 'AI scan: emit the agent task manifest + code-truth skeleton'],
|
|
189
|
+
['--write', 'With --plan: scaffold the skeleton docs to disk'],
|
|
190
|
+
['--format json', 'With --plan: machine-readable manifest'],
|
|
191
|
+
['--force', 'Overwrite existing docs (.bak backup kept)'],
|
|
192
|
+
],
|
|
193
|
+
examples: ['docguard generate', 'docguard generate --plan', 'docguard generate --plan --write', 'docguard generate --plan --format json'],
|
|
194
|
+
},
|
|
195
|
+
guard: {
|
|
196
|
+
summary: 'Validate code against canonical docs (all validators).',
|
|
197
|
+
usage: 'docguard guard [--format json] [--changed-only] [--fail-on-warning]',
|
|
198
|
+
flags: [
|
|
199
|
+
['--format json', 'Machine-readable results for CI'],
|
|
200
|
+
['--changed-only', 'Only validate docs/code touched in the working tree'],
|
|
201
|
+
['--fail-on-warning', 'Exit non-zero on warnings (strict CI)'],
|
|
202
|
+
],
|
|
203
|
+
examples: ['docguard guard', 'docguard guard --format json'],
|
|
204
|
+
},
|
|
205
|
+
score: {
|
|
206
|
+
summary: 'CDD maturity score (0–100).',
|
|
207
|
+
usage: 'docguard score [--diff] [--tax] [--format json]',
|
|
208
|
+
flags: [
|
|
209
|
+
['--diff', 'Delta between two refs'],
|
|
210
|
+
['--tax', 'Estimated documentation maintenance cost'],
|
|
211
|
+
['--format json', 'Machine-readable score'],
|
|
212
|
+
],
|
|
213
|
+
examples: ['docguard score', 'docguard score --tax'],
|
|
214
|
+
},
|
|
215
|
+
diff: {
|
|
216
|
+
summary: 'Show gaps between docs and code.',
|
|
217
|
+
usage: 'docguard diff [--since <ref>]',
|
|
218
|
+
flags: [['--since <ref>', 'Restrict to files changed since <ref> (impact mode)']],
|
|
219
|
+
examples: ['docguard diff', 'docguard diff --since HEAD~5'],
|
|
220
|
+
},
|
|
221
|
+
sync: {
|
|
222
|
+
summary: 'Refresh code-truth doc sections (preview by default).',
|
|
223
|
+
usage: 'docguard sync [--write] [--since <ref>]',
|
|
224
|
+
flags: [
|
|
225
|
+
['--write', 'Apply the refresh (default is a dry-run preview)'],
|
|
226
|
+
['--since <ref>', 'Only sync sections whose source files changed since <ref>'],
|
|
227
|
+
],
|
|
228
|
+
examples: ['docguard sync', 'docguard sync --write'],
|
|
229
|
+
},
|
|
230
|
+
fix: {
|
|
231
|
+
summary: 'Generate AI fix instructions for docs (or apply deterministic fixes).',
|
|
232
|
+
usage: 'docguard fix [--doc <name>] [--auto] [--write] [--force]',
|
|
233
|
+
flags: [
|
|
234
|
+
['--doc <name>', 'Target a specific doc (architecture, security, …)'],
|
|
235
|
+
['--auto', 'Auto-fix what is mechanically possible'],
|
|
236
|
+
['--write', 'Apply deterministic fixes in place (docguard:generated docs)'],
|
|
237
|
+
['--force', 'Allow edits outside docguard:generated docs'],
|
|
238
|
+
],
|
|
239
|
+
examples: ['docguard fix --doc architecture', 'docguard diagnose'],
|
|
240
|
+
},
|
|
241
|
+
trace: {
|
|
242
|
+
summary: 'Requirements traceability matrix.',
|
|
243
|
+
usage: 'docguard trace [--reverse]',
|
|
244
|
+
flags: [['--reverse', 'Code→doc map instead of doc→code']],
|
|
245
|
+
examples: ['docguard trace', 'docguard trace --reverse'],
|
|
246
|
+
},
|
|
247
|
+
upgrade: {
|
|
248
|
+
summary: 'Migrate .docguard.json schema + CLI.',
|
|
249
|
+
usage: 'docguard upgrade [--apply] [--pr]',
|
|
250
|
+
flags: [
|
|
251
|
+
['--apply', 'Write the migration (default is a preview)'],
|
|
252
|
+
['--pr', 'Open a team-wide PR with the migration'],
|
|
253
|
+
],
|
|
254
|
+
examples: ['docguard upgrade', 'docguard upgrade --apply'],
|
|
255
|
+
},
|
|
256
|
+
ci: {
|
|
257
|
+
summary: 'Generate CI / pipeline config.',
|
|
258
|
+
usage: 'docguard ci [--threshold <n>] [--fail-on-warning]',
|
|
259
|
+
flags: [
|
|
260
|
+
['--threshold <n>', 'Minimum score for CI pass'],
|
|
261
|
+
['--fail-on-warning', 'Fail CI on warnings'],
|
|
262
|
+
],
|
|
263
|
+
examples: ['docguard ci'],
|
|
264
|
+
},
|
|
265
|
+
memory: {
|
|
266
|
+
summary: 'Show what DocGuard remembers about the project.',
|
|
267
|
+
usage: 'docguard memory [--diff]',
|
|
268
|
+
flags: [['--diff', 'Drill into drift between memory and code']],
|
|
269
|
+
examples: ['docguard memory', 'docguard memory --diff'],
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
function printCommandHelp(command) {
|
|
274
|
+
const h = COMMAND_HELP[command];
|
|
275
|
+
if (!h) { printHelp(); return; } // no focused entry — global help still lists it
|
|
276
|
+
printBanner();
|
|
277
|
+
console.log(`${c.bold}docguard ${command}${c.reset} — ${h.summary}\n`);
|
|
278
|
+
console.log(`${c.bold}Usage:${c.reset}\n ${h.usage}\n`);
|
|
279
|
+
if (h.flags?.length) {
|
|
280
|
+
console.log(`${c.bold}Options:${c.reset}`);
|
|
281
|
+
for (const [flag, desc] of h.flags) console.log(` ${c.cyan}${flag.padEnd(18)}${c.reset} ${desc}`);
|
|
282
|
+
console.log('');
|
|
283
|
+
}
|
|
284
|
+
if (h.examples?.length) {
|
|
285
|
+
console.log(`${c.bold}Examples:${c.reset}`);
|
|
286
|
+
for (const ex of h.examples) console.log(` ${c.dim}${ex}${c.reset}`);
|
|
287
|
+
console.log('');
|
|
288
|
+
}
|
|
289
|
+
console.log(`${c.dim}All commands: ${c.cyan}docguard --help${c.reset}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
161
292
|
// ── Main ───────────────────────────────────────────────────────────────────
|
|
162
293
|
async function main() {
|
|
163
294
|
const args = process.argv.slice(2);
|
|
@@ -324,11 +455,11 @@ async function main() {
|
|
|
324
455
|
process.exit(0);
|
|
325
456
|
}
|
|
326
457
|
|
|
327
|
-
// v0.24: `docguard <command> --help` shows
|
|
328
|
-
//
|
|
329
|
-
//
|
|
458
|
+
// v0.24: `docguard <command> --help` shows that command's own flags +
|
|
459
|
+
// examples (field report B6); commands without a focused entry fall back to
|
|
460
|
+
// the global help. Non-destructive (generate no longer scaffolds on --help).
|
|
330
461
|
if (flags.help) {
|
|
331
|
-
|
|
462
|
+
printCommandHelp(command);
|
|
332
463
|
process.exit(0);
|
|
333
464
|
}
|
|
334
465
|
|
|
@@ -338,8 +469,12 @@ async function main() {
|
|
|
338
469
|
// Headless flags (`--write`, `--check-only`, `--auto`) also suppress chrome.
|
|
339
470
|
// v0.16-P5: --quiet (-q) joins the headless club for users who want
|
|
340
471
|
// banner-free output without committing to a specific machine format.
|
|
472
|
+
// v0.24 (field report): `--plan` is a read-only preview — "show me, don't
|
|
473
|
+
// touch" — so it joins the club to suppress the banner AND ensureSkills'
|
|
474
|
+
// .agent/.specify writes, which were a surprising side effect of a bare
|
|
475
|
+
// `generate --plan` (and were already suppressed for `--plan --write`).
|
|
341
476
|
const jsonMode = flags.format === 'json';
|
|
342
|
-
const headless = jsonMode || flags.write || flags.checkOnly || flags.changedOnly || flags.quiet;
|
|
477
|
+
const headless = jsonMode || flags.write || flags.checkOnly || flags.changedOnly || flags.quiet || flags.plan;
|
|
343
478
|
|
|
344
479
|
if (!headless) printBanner();
|
|
345
480
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
17
17
|
import { resolve, join, relative, dirname, basename } from 'node:path';
|
|
18
|
+
import { shouldIgnore, relPosix } from '../shared-ignore.mjs';
|
|
18
19
|
|
|
19
20
|
const IGNORE_DIRS = new Set([
|
|
20
21
|
'node_modules', '.git', '.next', 'dist', 'build', 'coverage', 'target',
|
|
@@ -42,7 +43,8 @@ function readSafe(p) { try { return readFileSync(p, 'utf-8'); } catch { return '
|
|
|
42
43
|
function readJson(p) { try { return JSON.parse(readFileSync(p, 'utf-8')); } catch { return null; } }
|
|
43
44
|
|
|
44
45
|
/** Recursively find manifest files (bounded depth, ignoring vendor dirs). */
|
|
45
|
-
function findManifests(projectDir, maxDepth = 4) {
|
|
46
|
+
function findManifests(projectDir, maxDepth = 4, config = {}) {
|
|
47
|
+
const root = resolve(projectDir);
|
|
46
48
|
const found = []; // { absDir, file, lang }
|
|
47
49
|
const walk = (dir, depth) => {
|
|
48
50
|
if (depth > maxDepth) return;
|
|
@@ -51,15 +53,20 @@ function findManifests(projectDir, maxDepth = 4) {
|
|
|
51
53
|
for (const e of entries) {
|
|
52
54
|
if (e.isDirectory()) {
|
|
53
55
|
if (IGNORE_DIRS.has(e.name) || e.name.startsWith('.')) continue;
|
|
56
|
+
// Honor config.ignore / .docguardignore: a user who excludes tests/ or
|
|
57
|
+
// base-research/ must not have those dirs' manifests (e.g. a fixture
|
|
58
|
+
// package.json declaring express) misclassify the project's stack.
|
|
59
|
+
if (shouldIgnore(relPosix(root, join(dir, e.name)), config)) continue;
|
|
54
60
|
walk(join(dir, e.name), depth + 1);
|
|
55
61
|
} else if (e.isFile()) {
|
|
62
|
+
if (shouldIgnore(relPosix(root, join(dir, e.name)), config)) continue;
|
|
56
63
|
const m = MANIFESTS.find(x => x.file === e.name);
|
|
57
64
|
if (m) found.push({ absDir: dir, file: e.name, lang: m.lang });
|
|
58
65
|
else if (e.name.endsWith('.csproj')) found.push({ absDir: dir, file: e.name, lang: 'C#' });
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
};
|
|
62
|
-
walk(
|
|
69
|
+
walk(root, 0);
|
|
63
70
|
return found;
|
|
64
71
|
}
|
|
65
72
|
|
|
@@ -255,8 +262,8 @@ function buildEcosystem(projectDir, m) {
|
|
|
255
262
|
* Multiple manifests in the same dir+language merge into one ecosystem.
|
|
256
263
|
* @returns {Array<{ language, manifest, dir, framework, kind, deps, entryPoints }>}
|
|
257
264
|
*/
|
|
258
|
-
export function detectEcosystems(projectDir,
|
|
259
|
-
const manifests = findManifests(projectDir);
|
|
265
|
+
export function detectEcosystems(projectDir, config = {}) {
|
|
266
|
+
const manifests = findManifests(projectDir, 4, config);
|
|
260
267
|
const byKey = new Map(); // `${dir}::${lang-family}` → ecosystem
|
|
261
268
|
|
|
262
269
|
// Group Python manifests (pyproject/requirements/setup/Pipfile) in same dir.
|
package/cli/shared-ignore.mjs
CHANGED
|
@@ -113,7 +113,13 @@ export function mergeIgnoreFile(projectDir, config) {
|
|
|
113
113
|
* @returns {RegExp}
|
|
114
114
|
*/
|
|
115
115
|
function globToRegex(pattern) {
|
|
116
|
-
|
|
116
|
+
// gitignore-style trailing slash ("dir/") means "this directory and everything
|
|
117
|
+
// under it". Strip it so "dir/" matches identically to "dir" — otherwise the
|
|
118
|
+
// escaped pattern keeps the slash and the alternation below can only match a
|
|
119
|
+
// literal "dir//" (double slash), so the pattern silently matches nothing.
|
|
120
|
+
// `|| pattern` guards the degenerate all-slashes case (e.g. "/") from emptying.
|
|
121
|
+
const normalized = pattern.replace(/\/+$/, '') || pattern;
|
|
122
|
+
const escaped = normalized
|
|
117
123
|
.replace(/\./g, '\\.')
|
|
118
124
|
.replace(/\*\*/g, '§§') // temp placeholder for **
|
|
119
125
|
.replace(/\*/g, '[^/]*')
|
package/cli/shared.mjs
CHANGED
|
@@ -183,6 +183,38 @@ export const PROFILES = {
|
|
|
183
183
|
freshness: true,
|
|
184
184
|
},
|
|
185
185
|
},
|
|
186
|
+
// F3 (field report): non-web-centric profiles. The default doc set assumes an
|
|
187
|
+
// HTTP API + database; for a CLI or library that structure fights the project.
|
|
188
|
+
// These drop the HTTP/DB-shaped requirements. (A bespoke CLI-REFERENCE doc type
|
|
189
|
+
// is a separate, larger follow-up; until then API-REFERENCE doubles as the
|
|
190
|
+
// library's module-API reference.)
|
|
191
|
+
cli: {
|
|
192
|
+
description: 'CLI / command-line tool — no HTTP API or database assumed.',
|
|
193
|
+
requiredFiles: {
|
|
194
|
+
canonical: [
|
|
195
|
+
'docs-canonical/ARCHITECTURE.md',
|
|
196
|
+
'docs-canonical/TEST-SPEC.md',
|
|
197
|
+
'docs-canonical/SECURITY.md',
|
|
198
|
+
'docs-canonical/ENVIRONMENT.md',
|
|
199
|
+
],
|
|
200
|
+
agentFile: ['AGENTS.md', 'CLAUDE.md'],
|
|
201
|
+
changelog: 'CHANGELOG.md',
|
|
202
|
+
driftLog: 'DRIFT-LOG.md',
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
library: {
|
|
206
|
+
description: 'Library / package — public API matters; no HTTP server or DB assumed.',
|
|
207
|
+
requiredFiles: {
|
|
208
|
+
canonical: [
|
|
209
|
+
'docs-canonical/ARCHITECTURE.md',
|
|
210
|
+
'docs-canonical/API-REFERENCE.md',
|
|
211
|
+
'docs-canonical/TEST-SPEC.md',
|
|
212
|
+
],
|
|
213
|
+
agentFile: ['AGENTS.md', 'CLAUDE.md'],
|
|
214
|
+
changelog: 'CHANGELOG.md',
|
|
215
|
+
driftLog: 'DRIFT-LOG.md',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
186
218
|
'enterprise-ai': {
|
|
187
219
|
description: 'EU AI Act compliance — Annex IV documentation requirements, ALCOA+ alignment, strict freshness. For AI/ML projects under regulatory scrutiny.',
|
|
188
220
|
requiredFiles: {
|
|
@@ -173,6 +173,19 @@ export function validateGeneratedStaleness(projectDir, config = {}) {
|
|
|
173
173
|
if (!onDisk) continue;
|
|
174
174
|
|
|
175
175
|
result.total++;
|
|
176
|
+
|
|
177
|
+
// B5 (field report): a `pinned` attribute on the section's open marker
|
|
178
|
+
// <!-- docguard:section id=… source=code pinned="reason" -->
|
|
179
|
+
// marks the section as intentionally hand-maintained — the scanner
|
|
180
|
+
// mislabeled this surface (e.g. a scanner/tool repo whose source contains
|
|
181
|
+
// framework-like strings; see F1). Exempt it from staleness and count it
|
|
182
|
+
// as a pass, mirroring the docguard:quality opt-out marker. This is the
|
|
183
|
+
// escape hatch from the "stale forever / sync --write reverts it" trap.
|
|
184
|
+
if (onDisk.attrs?.pinned !== undefined) {
|
|
185
|
+
result.passed++;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
176
189
|
const expected = String(sec.body || '').trim();
|
|
177
190
|
const actual = String(onDisk.body || '').trim();
|
|
178
191
|
|
|
@@ -194,7 +207,9 @@ export function validateGeneratedStaleness(projectDir, config = {}) {
|
|
|
194
207
|
: '';
|
|
195
208
|
|
|
196
209
|
result.warnings.push(
|
|
197
|
-
`${basename(doc.path)} → section "${sec.id}" is stale${hint}. Run \`docguard sync --write\` to refresh code-truth sections
|
|
210
|
+
`${basename(doc.path)} → section "${sec.id}" is stale${hint}. Run \`docguard sync --write\` to refresh code-truth sections. ` +
|
|
211
|
+
`If this section is intentionally hand-maintained (the scanner mislabeled it), pin it: ` +
|
|
212
|
+
`add \`pinned="reason"\` to its \`<!-- docguard:section id=${sec.id} … -->\` marker.`
|
|
198
213
|
);
|
|
199
214
|
// v0.14-P3: structured fix so `docguard fix --write` can fix this
|
|
200
215
|
// mechanically (no AI needed — scanner already produced the right body).
|
|
@@ -3,7 +3,7 @@ schema_version: "1.0"
|
|
|
3
3
|
extension:
|
|
4
4
|
id: "docguard"
|
|
5
5
|
name: "DocGuard — CDD Enforcement"
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.25.1"
|
|
7
7
|
description: "Canonical-Driven Development enforcement as a true spec-kit extension. LLM-first design with automated validators, 4 AI behavior skills, spec-kit skill chaining, and workflow hooks. One pinned runtime dependency (@babel/parser); pure Node.js otherwise."
|
|
8
8
|
author: "Ricardo Accioly"
|
|
9
9
|
repository: "https://github.com/raccioly/docguard"
|
|
@@ -28,22 +28,18 @@ provides:
|
|
|
28
28
|
- name: "speckit.docguard.guard"
|
|
29
29
|
file: "commands/guard.md"
|
|
30
30
|
description: "Run 19-validator quality gate with severity triage and remediation plan"
|
|
31
|
-
aliases: ["speckit.docguard.guard"]
|
|
32
31
|
|
|
33
32
|
- name: "speckit.docguard.fix"
|
|
34
33
|
file: "commands/generate.md"
|
|
35
34
|
description: "AI-driven documentation repair with codebase research and validation loops"
|
|
36
|
-
aliases: ["speckit.docguard.fix"]
|
|
37
35
|
|
|
38
36
|
- name: "speckit.docguard.review"
|
|
39
37
|
file: "commands/diagnose.md"
|
|
40
38
|
description: "Cross-document semantic consistency analysis (read-only)"
|
|
41
|
-
aliases: ["speckit.docguard.review"]
|
|
42
39
|
|
|
43
40
|
- name: "speckit.docguard.score"
|
|
44
41
|
file: "commands/score.md"
|
|
45
42
|
description: "CDD maturity score with ROI-based improvement roadmap"
|
|
46
|
-
aliases: ["speckit.docguard.score"]
|
|
47
43
|
|
|
48
44
|
- name: "speckit.docguard.diagnose"
|
|
49
45
|
file: "commands/diagnose.md"
|
|
@@ -6,10 +6,10 @@ description: AI-driven documentation repair with structured research workflow, t
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.
|
|
9
|
+
version: 0.25.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-fix
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.25.0 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Fix Skill
|
|
15
15
|
|
|
@@ -7,10 +7,10 @@ description: Run DocGuard guard validation against Canonical-Driven Development
|
|
|
7
7
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
8
8
|
metadata:
|
|
9
9
|
author: docguard
|
|
10
|
-
version: 0.
|
|
10
|
+
version: 0.25.0
|
|
11
11
|
source: extensions/spec-kit-docguard/skills/docguard-guard
|
|
12
12
|
---
|
|
13
|
-
<!-- docguard:version: 0.
|
|
13
|
+
<!-- docguard:version: 0.25.0 -->
|
|
14
14
|
|
|
15
15
|
# DocGuard Guard Skill
|
|
16
16
|
|
|
@@ -6,10 +6,10 @@ description: Cross-document consistency analysis and quality assessment. Perform
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.
|
|
9
|
+
version: 0.25.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-review
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.25.0 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Review Skill
|
|
15
15
|
|
|
@@ -6,10 +6,10 @@ description: CDD maturity assessment with category-aware improvement roadmap. Ru
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.
|
|
9
|
+
version: 0.25.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-score
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.25.0 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Score Skill
|
|
15
15
|
|
|
@@ -4,10 +4,10 @@ description: Keep canonical documentation ALWAYS UP TO DATE. Refreshes code-trut
|
|
|
4
4
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
5
5
|
metadata:
|
|
6
6
|
author: docguard
|
|
7
|
-
version: 0.
|
|
7
|
+
version: 0.25.0
|
|
8
8
|
source: extensions/spec-kit-docguard/skills/docguard-sync
|
|
9
9
|
---
|
|
10
|
-
<!-- docguard:version: 0.
|
|
10
|
+
<!-- docguard:version: 0.25.0 -->
|
|
11
11
|
|
|
12
12
|
# DocGuard Sync Skill
|
|
13
13
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
# Setup:
|
|
14
14
|
# 1. Copy this file to .github/workflows/docguard-autofix.yml
|
|
15
15
|
# 2. Ensure the workflow has the permissions block below (write access).
|
|
16
|
-
# 3. Pinned to the `@v0.
|
|
16
|
+
# 3. Pinned to the `@v0.25.0` release tag for reproducible CI. Change it to a
|
|
17
17
|
# newer tag to upgrade, or to `@main` to always track the latest (unpinned).
|
|
18
18
|
#
|
|
19
19
|
# Security note: this workflow makes commits back to the PR branch. It refuses
|
|
@@ -44,7 +44,7 @@ jobs:
|
|
|
44
44
|
fetch-depth: 0
|
|
45
45
|
|
|
46
46
|
- name: Run DocGuard fix --write + auto-commit + PR comment
|
|
47
|
-
uses: raccioly/docguard@v0.
|
|
47
|
+
uses: raccioly/docguard@v0.25.0
|
|
48
48
|
with:
|
|
49
49
|
command: fix
|
|
50
50
|
auto-commit: 'true'
|
|
@@ -28,7 +28,7 @@ jobs:
|
|
|
28
28
|
fetch-depth: 0
|
|
29
29
|
|
|
30
30
|
- name: Run all validators
|
|
31
|
-
uses: raccioly/docguard@v0.
|
|
31
|
+
uses: raccioly/docguard@v0.25.0
|
|
32
32
|
with:
|
|
33
33
|
command: guard
|
|
34
34
|
# Flip to 'true' once your repo is clean — turns warnings into hard failures.
|
|
@@ -42,7 +42,7 @@ jobs:
|
|
|
42
42
|
- uses: actions/checkout@v4
|
|
43
43
|
|
|
44
44
|
- name: Score & comment
|
|
45
|
-
uses: raccioly/docguard@v0.
|
|
45
|
+
uses: raccioly/docguard@v0.25.0
|
|
46
46
|
with:
|
|
47
47
|
command: score
|
|
48
48
|
format: json
|
package/package.json
CHANGED