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.
@@ -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
- writeFileSync(full, content, 'utf-8');
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
  }
@@ -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`);
@@ -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> Compliance profile: starter, standard, enterprise (init command)
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 usage instead of running the
328
- // command. There is no per-command help yet, so global help is correct — and,
329
- // unlike before, non-destructive (generate no longer scaffolds on --help).
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
- printHelp();
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(resolve(projectDir), 0);
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, _config = {}) {
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.
@@ -113,7 +113,13 @@ export function mergeIgnoreFile(projectDir, config) {
113
113
  * @returns {RegExp}
114
114
  */
115
115
  function globToRegex(pattern) {
116
- const escaped = pattern
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.24.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.24.0
9
+ version: 0.25.0
10
10
  source: extensions/spec-kit-docguard/skills/docguard-fix
11
11
  ---
12
- <!-- docguard:version: 0.24.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.24.0
10
+ version: 0.25.0
11
11
  source: extensions/spec-kit-docguard/skills/docguard-guard
12
12
  ---
13
- <!-- docguard:version: 0.24.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.24.0
9
+ version: 0.25.0
10
10
  source: extensions/spec-kit-docguard/skills/docguard-review
11
11
  ---
12
- <!-- docguard:version: 0.24.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.24.0
9
+ version: 0.25.0
10
10
  source: extensions/spec-kit-docguard/skills/docguard-score
11
11
  ---
12
- <!-- docguard:version: 0.24.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.24.0
7
+ version: 0.25.0
8
8
  source: extensions/spec-kit-docguard/skills/docguard-sync
9
9
  ---
10
- <!-- docguard:version: 0.24.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.24.0` release tag for reproducible CI. Change it to a
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.24.0
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.24.0
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.24.0
45
+ uses: raccioly/docguard@v0.25.0
46
46
  with:
47
47
  command: score
48
48
  format: json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.24.0",
3
+ "version": "0.25.1",
4
4
  "description": "The enforcement tool for Canonical-Driven Development (CDD). Audit, generate, and guard your project documentation.",
5
5
  "type": "module",
6
6
  "bin": {