docguard-cli 0.22.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +4 -4
  2. package/cli/commands/demo.mjs +1 -1
  3. package/cli/commands/diff.mjs +19 -8
  4. package/cli/commands/explain.mjs +178 -17
  5. package/cli/commands/fix.mjs +17 -2
  6. package/cli/commands/generate.mjs +2 -2
  7. package/cli/commands/guard.mjs +86 -11
  8. package/cli/commands/hooks.mjs +12 -7
  9. package/cli/commands/init.mjs +18 -6
  10. package/cli/commands/score.mjs +147 -61
  11. package/cli/commands/setup.mjs +2 -2
  12. package/cli/commands/trace.mjs +3 -101
  13. package/cli/commands/upgrade.mjs +61 -13
  14. package/cli/config.mjs +245 -0
  15. package/cli/docguard.mjs +21 -217
  16. package/cli/ensure-skills.mjs +24 -26
  17. package/cli/scanners/api-doc.mjs +17 -3
  18. package/cli/scanners/doc-tools.mjs +32 -15
  19. package/cli/scanners/frontend.mjs +24 -8
  20. package/cli/scanners/js-ast.mjs +432 -0
  21. package/cli/scanners/memory-plan.mjs +1 -1
  22. package/cli/scanners/py-ast.mjs +213 -0
  23. package/cli/scanners/routes.mjs +194 -69
  24. package/cli/scanners/schemas.mjs +97 -51
  25. package/cli/scanners/speckit.mjs +14 -0
  26. package/cli/shared-git.mjs +0 -0
  27. package/cli/shared-ignore.mjs +16 -1
  28. package/cli/shared-source.mjs +59 -2
  29. package/cli/shared-trace-patterns.mjs +118 -0
  30. package/cli/shared.mjs +60 -1
  31. package/cli/validator-markers.mjs +91 -0
  32. package/cli/validators/api-surface.mjs +37 -3
  33. package/cli/validators/canonical-sync.mjs +22 -19
  34. package/cli/validators/doc-quality.mjs +27 -44
  35. package/cli/validators/docs-coverage.mjs +13 -0
  36. package/cli/validators/docs-diff.mjs +16 -6
  37. package/cli/validators/docs-sync.mjs +4 -3
  38. package/cli/validators/drift.mjs +3 -2
  39. package/cli/validators/freshness.mjs +47 -15
  40. package/cli/validators/metadata-sync.mjs +21 -11
  41. package/cli/validators/metrics-consistency.mjs +45 -17
  42. package/cli/validators/security.mjs +13 -5
  43. package/cli/validators/structure.mjs +6 -5
  44. package/cli/validators/surface-sync.mjs +7 -5
  45. package/cli/validators/test-spec.mjs +76 -51
  46. package/cli/validators/todo-tracking.mjs +4 -2
  47. package/cli/validators/traceability.mjs +12 -54
  48. package/cli/writers/sections.mjs +32 -19
  49. package/docs/commands.md +1 -1
  50. package/docs/configuration.md +11 -0
  51. package/docs/faq.md +1 -1
  52. package/extensions/spec-kit-docguard/README.md +1 -1
  53. package/extensions/spec-kit-docguard/extension.yml +2 -2
  54. package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
  55. package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +2 -2
  56. package/extensions/spec-kit-docguard/skills/docguard-review/SKILL.md +2 -2
  57. package/extensions/spec-kit-docguard/skills/docguard-score/SKILL.md +2 -2
  58. package/extensions/spec-kit-docguard/skills/docguard-sync/SKILL.md +2 -1
  59. package/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +3 -2
  60. package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +2 -2
  61. package/package.json +5 -3
@@ -71,14 +71,48 @@ function readProjectSchemaVersion(projectDir) {
71
71
  }
72
72
  }
73
73
 
74
+ /**
75
+ * Normalize the `requiredFiles` block to the canonical object shape
76
+ * (`{ canonical: [...], agentFile, changelog, driftLog }`). Validators read
77
+ * `config.requiredFiles?.canonical || []`, so a config whose `requiredFiles` is
78
+ * a bare array (a legacy/hand-edited shape) or an object that lost its
79
+ * `canonical` key makes Traceability/Structure/Freshness silently see an empty
80
+ * list and PASS — the exact false-green this tool exists to prevent.
81
+ *
82
+ * Returns `{ cfg, changed, notes }`. We only TRANSFORM the unambiguous case (a
83
+ * bare array can only ever have been the canonical doc list); any other broken
84
+ * shape is SURFACED as a note rather than guessed at.
85
+ */
86
+ function normalizeRequiredFiles(cfg) {
87
+ const rf = cfg.requiredFiles;
88
+ const notes = [];
89
+ if (rf === undefined || rf === null) {
90
+ // No requiredFiles at all — validators default to empty and check nothing.
91
+ // Don't fabricate a list (we can't know the project's docs); surface it.
92
+ notes.push('⚠ requiredFiles is missing — Traceability/Structure/Freshness have nothing to verify (silent-pass risk). Run `docguard init` to generate it.');
93
+ return { cfg, changed: false, notes };
94
+ }
95
+ if (Array.isArray(rf)) {
96
+ notes.push('Normalized legacy requiredFiles array → { canonical: [...] } so canonical-doc checks see your docs again.');
97
+ return { cfg: { ...cfg, requiredFiles: { canonical: rf } }, changed: true, notes };
98
+ }
99
+ if (typeof rf === 'object' && !Array.isArray(rf.canonical)) {
100
+ notes.push('⚠ requiredFiles.canonical is missing or not an array — canonical-doc checks will silently pass. Restore it or run `docguard init`.');
101
+ }
102
+ return { cfg, changed: false, notes };
103
+ }
104
+
74
105
  /**
75
106
  * Idempotent migration: walk the project config and add any fields introduced
76
- * since the stored schema version. Returns { changed, newConfig }.
107
+ * since the stored schema version, then normalize the requiredFiles shape.
108
+ * Returns { changed, newConfig, notes }.
77
109
  *
78
- * Each migration is keyed by the version it migrates TO. Adding a new schema
79
- * version means adding one entry here.
110
+ * Each version migration is keyed by the version it migrates TO. Adding a new
111
+ * schema version means adding one entry here. The requiredFiles normalization
112
+ * runs regardless of the version delta — a config can sit at the current
113
+ * version yet still carry a hand-edited shape that breaks validators silently.
80
114
  */
81
- function migrateSchema(cfg, fromVersion) {
115
+ export function migrateSchema(cfg, fromVersion) {
82
116
  const migrations = {
83
117
  // v0.4 — pre-0.4 schemas (no `version` field, often `project` instead
84
118
  // of `projectName`) normalize here. Rename `project` → `projectName`
@@ -99,16 +133,24 @@ function migrateSchema(cfg, fromVersion) {
99
133
  let current = { ...cfg };
100
134
  let changed = false;
101
135
  const target = CURRENT_SCHEMA_VERSION;
102
- // No migrations yet — current schema matches the constant.
103
- if (compareVersions(fromVersion, target) >= 0) return { changed: false, newConfig: current };
104
- for (const [ver, fn] of Object.entries(migrations)) {
105
- if (compareVersions(fromVersion, ver) < 0 && compareVersions(ver, target) <= 0) {
106
- current = fn(current);
107
- changed = true;
136
+
137
+ // Version-keyed field migrations (additive). Skipped when already current.
138
+ if (compareVersions(fromVersion, target) < 0) {
139
+ for (const [ver, fn] of Object.entries(migrations)) {
140
+ if (compareVersions(fromVersion, ver) < 0 && compareVersions(ver, target) <= 0) {
141
+ current = fn(current);
142
+ changed = true;
143
+ }
108
144
  }
145
+ if (changed) current.version = target;
109
146
  }
110
- if (changed) current.version = target;
111
- return { changed, newConfig: current };
147
+
148
+ // requiredFiles shape normalization independent of the version delta.
149
+ const norm = normalizeRequiredFiles(current);
150
+ current = norm.cfg;
151
+ if (norm.changed) changed = true;
152
+
153
+ return { changed, newConfig: current, notes: norm.notes };
112
154
  }
113
155
 
114
156
  /**
@@ -291,7 +333,13 @@ export async function runUpgrade(projectDir, _config, flags) {
291
333
  if (schemaBehind && projectSchema) {
292
334
  const cfgPath = resolve(projectDir, '.docguard.json');
293
335
  const cfg = JSON.parse(readFileSync(cfgPath, 'utf-8'));
294
- const { changed, newConfig } = migrateSchema(cfg, projectSchema);
336
+ const { changed, newConfig, notes } = migrateSchema(cfg, projectSchema);
337
+ // Surface requiredFiles findings (a normalization that happened, or a
338
+ // broken shape we refuse to guess at) before reporting the version bump.
339
+ for (const n of notes || []) {
340
+ const warn = n.startsWith('⚠');
341
+ console.log(` ${warn ? c.yellow : c.dim}${warn ? '' : '• '}${n}${c.reset}`);
342
+ }
295
343
  if (changed) {
296
344
  // v0.14-P4: --pr opens a PR for review instead of in-place editing.
297
345
  // Useful when the team wants a reviewable diff or has branch-protected
package/cli/config.mjs ADDED
@@ -0,0 +1,245 @@
1
+ /**
2
+ * DocGuard — configuration loading.
3
+ *
4
+ * Extracted from docguard.mjs (v0.23.0) to break the demo.mjs → docguard.mjs
5
+ * import cycle. demo.mjs runs guard/score against a temp fixture and needs
6
+ * loadConfig, but docguard.mjs statically imports every command (including
7
+ * demo). Importing loadConfig from here — which only pulls shared.mjs and
8
+ * shared-ignore.mjs, never a command module — keeps the import graph acyclic.
9
+ */
10
+
11
+ import { existsSync, readFileSync } from 'node:fs';
12
+ import { resolve, basename } from 'node:path';
13
+ import { c, PROFILES, SEVERITY_LEVELS } from './shared.mjs';
14
+ import { mergeIgnoreFile } from './shared-ignore.mjs';
15
+
16
+ export function loadConfig(projectDir) {
17
+ const configPath = resolve(projectDir, '.docguard.json');
18
+ const defaults = {
19
+ projectName: basename(projectDir),
20
+ // Legacy/unversioned fallback ONLY — the value a config is ASSUMED to be
21
+ // when its file has no `version` field. NOT the current schema version
22
+ // (that's CURRENT_SCHEMA_VERSION in shared.mjs, written by `init`). Kept low
23
+ // on purpose so a versionless (pre-0.4) config still trips the upgrade nudge.
24
+ version: '0.2',
25
+ profile: 'standard',
26
+ requiredFiles: {
27
+ canonical: [
28
+ 'docs-canonical/ARCHITECTURE.md',
29
+ 'docs-canonical/DATA-MODEL.md',
30
+ 'docs-canonical/SECURITY.md',
31
+ 'docs-canonical/TEST-SPEC.md',
32
+ 'docs-canonical/ENVIRONMENT.md',
33
+ ],
34
+ agentFile: ['AGENTS.md', 'CLAUDE.md'],
35
+ changelog: 'CHANGELOG.md',
36
+ driftLog: 'DRIFT-LOG.md',
37
+ },
38
+ // All CDD document types — required vs optional
39
+ documentTypes: {
40
+ // Canonical (design intent) — required by default
41
+ 'docs-canonical/ARCHITECTURE.md': { required: true, category: 'canonical', description: 'System design, components, layer boundaries' },
42
+ 'docs-canonical/DATA-MODEL.md': { required: true, category: 'canonical', description: 'Database schemas, entities, relationships' },
43
+ 'docs-canonical/SECURITY.md': { required: true, category: 'canonical', description: 'Authentication, authorization, secrets management' },
44
+ 'docs-canonical/TEST-SPEC.md': { required: true, category: 'canonical', description: 'Test categories, coverage rules, service-to-test map' },
45
+ 'docs-canonical/ENVIRONMENT.md': { required: true, category: 'canonical', description: 'Environment variables, setup steps, prerequisites' },
46
+ 'docs-canonical/DEPLOYMENT.md': { required: false, category: 'canonical', description: 'Infrastructure, CI/CD pipeline, DNS, monitoring' },
47
+ 'docs-canonical/ADR.md': { required: false, category: 'canonical', description: 'Architecture Decision Records with rationale' },
48
+ // Implementation (current state) — optional by default
49
+ 'docs-implementation/KNOWN-GOTCHAS.md': { required: false, category: 'implementation', description: 'Lessons learned — symptom/gotcha/fix format' },
50
+ 'docs-implementation/TROUBLESHOOTING.md': { required: false, category: 'implementation', description: 'Error diagnosis guides by category' },
51
+ 'docs-implementation/RUNBOOKS.md': { required: false, category: 'implementation', description: 'Operational procedures (deploy, rollback, backup)' },
52
+ 'docs-implementation/CURRENT-STATE.md': { required: false, category: 'implementation', description: 'Deployment status, feature completion, tech debt' },
53
+ 'docs-implementation/VENDOR-BUGS.md': { required: false, category: 'implementation', description: 'Third-party bug tracker with workarounds' },
54
+ // Root files
55
+ 'AGENTS.md': { required: true, category: 'agent', description: 'AI agent behavior rules and project context' },
56
+ 'CHANGELOG.md': { required: true, category: 'tracking', description: 'All notable changes per Keep a Changelog format' },
57
+ 'DRIFT-LOG.md': { required: true, category: 'tracking', description: 'Documented deviations from canonical docs' },
58
+ 'ROADMAP.md': { required: false, category: 'tracking', description: 'Project phases, feature tracking, vision' },
59
+ },
60
+ sourcePatterns: {
61
+ services: 'src/services/**/*.{ts,js,py,java}',
62
+ routes: 'src/routes/**/*.{ts,js,py,java}',
63
+ tests: 'tests/**/*.test.{ts,js,py,java}',
64
+ },
65
+ validators: {
66
+ structure: true,
67
+ docsSync: true,
68
+ drift: true,
69
+ changelog: true,
70
+ architecture: false,
71
+ testSpec: true,
72
+ security: false,
73
+ environment: true,
74
+ freshness: true,
75
+ },
76
+ };
77
+
78
+ if (existsSync(configPath)) {
79
+ try {
80
+ const userConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
81
+
82
+ // Apply profile presets BEFORE merging user config
83
+ // Profile sets the baseline, user config can override anything
84
+ const profileName = userConfig.profile || defaults.profile;
85
+ const profilePreset = PROFILES[profileName];
86
+ const withProfile = profilePreset
87
+ ? deepMerge(defaults, profilePreset)
88
+ : defaults;
89
+
90
+ // v0.17-P4: normalize validator/severity keys before merging so the
91
+ // user can write either kebab-case (`test-spec`) or camelCase (`testSpec`)
92
+ // and the internal lookups (always camelCase) still hit.
93
+ const merged = deepMerge(withProfile, normalizeConfig(userConfig));
94
+ merged.profile = profileName;
95
+
96
+ // v0.24: severity accepts only high|medium|low and changes EXIT-CODE
97
+ // weight — it never mutes a warning from display. A value like "off"
98
+ // silently fell back to "medium", so users who wrote severity:{k:"off"}
99
+ // expecting silence still saw the warning and got no feedback (field
100
+ // report). Surface the misconfig and point at the real disable switch.
101
+ if (merged.severity && typeof merged.severity === 'object') {
102
+ for (const [key, val] of Object.entries(merged.severity)) {
103
+ if (typeof val === 'string' && !SEVERITY_LEVELS.has(val.toLowerCase())) {
104
+ console.error(`${c.yellow}⚠ .docguard.json: severity.${key} = "${val}" is not a valid level${c.reset} ${c.dim}(use high | medium | low). To silence a validator entirely, set ${c.reset}${c.cyan}validators.${key}: false${c.dim}.${c.reset}`);
105
+ }
106
+ }
107
+ }
108
+
109
+ // Auto-detect project type if not set
110
+ if (!merged.projectType) {
111
+ merged.projectType = autoDetectProjectType(projectDir);
112
+ }
113
+ // Ensure projectTypeConfig has sensible defaults based on type
114
+ merged.projectTypeConfig = {
115
+ ...getProjectTypeDefaults(merged.projectType),
116
+ ...(merged.projectTypeConfig || {}),
117
+ };
118
+ // Normalize testPattern (string) → testPatterns (array) for backward compat
119
+ if (merged.testPattern && !merged.testPatterns) {
120
+ merged.testPatterns = [merged.testPattern];
121
+ } else if (merged.testPattern && merged.testPatterns) {
122
+ // Both set — merge, deduplicate
123
+ if (!merged.testPatterns.includes(merged.testPattern)) {
124
+ merged.testPatterns.push(merged.testPattern);
125
+ }
126
+ }
127
+ // Merge .docguardignore patterns into config.ignore so every validator
128
+ // honors them without having to know about the file.
129
+ mergeIgnoreFile(projectDir, merged);
130
+ return merged;
131
+ } catch (e) {
132
+ console.error(`${c.red}Error parsing .docguard.json: ${e.message}${c.reset}`);
133
+ process.exit(1);
134
+ }
135
+ }
136
+
137
+ // No config file — auto-detect everything
138
+ defaults.projectType = autoDetectProjectType(projectDir);
139
+ defaults.projectTypeConfig = getProjectTypeDefaults(defaults.projectType);
140
+ // .docguardignore is read even when no .docguard.json exists — keeps
141
+ // ignore-only projects (no config but want to skip paths) working.
142
+ mergeIgnoreFile(projectDir, defaults);
143
+ return defaults;
144
+ }
145
+
146
+ // PROFILES is exported from shared.mjs (re-exported at line 43)
147
+
148
+ /**
149
+ * Auto-detect project type from package.json and file structure.
150
+ * Returns: 'cli' | 'library' | 'webapp' | 'api' | 'unknown'
151
+ */
152
+ function autoDetectProjectType(dir) {
153
+ const pkgPath = resolve(dir, 'package.json');
154
+ if (existsSync(pkgPath)) {
155
+ try {
156
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
157
+ const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
158
+
159
+ // CLI tool: has "bin" field
160
+ if (pkg.bin) return 'cli';
161
+
162
+ // Web app: has a frontend framework
163
+ if (allDeps.next || allDeps.react || allDeps.vue || allDeps['@angular/core'] ||
164
+ allDeps.svelte || allDeps.nuxt || allDeps['@sveltejs/kit']) return 'webapp';
165
+
166
+ // API: has a server framework but no frontend
167
+ if (allDeps.express || allDeps.fastify || allDeps.hono || allDeps.koa) return 'api';
168
+
169
+ // Library: has "main" or "exports" and no framework
170
+ if (pkg.main || pkg.exports || pkg.module) return 'library';
171
+ } catch { /* fall through */ }
172
+ }
173
+
174
+ // Python project
175
+ if (existsSync(resolve(dir, 'manage.py'))) return 'webapp';
176
+ if (existsSync(resolve(dir, 'setup.py')) || existsSync(resolve(dir, 'pyproject.toml'))) return 'library';
177
+
178
+ return 'unknown';
179
+ }
180
+
181
+ /**
182
+ * Get default projectTypeConfig for a given project type.
183
+ */
184
+ function getProjectTypeDefaults(type) {
185
+ const defaults = {
186
+ cli: { needsEnvVars: false, needsEnvExample: false, needsE2E: false, needsDatabase: false, testFramework: 'node:test', runCommand: null },
187
+ library: { needsEnvVars: false, needsEnvExample: false, needsE2E: false, needsDatabase: false, testFramework: 'vitest', runCommand: null },
188
+ webapp: { needsEnvVars: true, needsEnvExample: true, needsE2E: true, needsDatabase: true, testFramework: 'vitest', runCommand: 'npm run dev' },
189
+ api: { needsEnvVars: true, needsEnvExample: true, needsE2E: false, needsDatabase: true, testFramework: 'vitest', runCommand: 'npm run dev' },
190
+ unknown: { needsEnvVars: true, needsEnvExample: true, needsE2E: false, needsDatabase: true, testFramework: null, runCommand: null },
191
+ };
192
+ return defaults[type] || defaults.unknown;
193
+ }
194
+
195
+ /**
196
+ * v0.17-P4: normalize validator-key naming so users can write either
197
+ * `validators: { "test-spec": true }` (kebab-case, matches CLI display)
198
+ * or `validators: { testSpec: true }` (camelCase, matches JSON internals)
199
+ * in `.docguard.json`. We normalize the WHOLE config tree's known validator
200
+ * keys to camelCase before merging. Same treatment applied to `severity`.
201
+ *
202
+ * Non-validator keys are left alone. Unknown keys (forward-compat) are
203
+ * normalized blindly: kebab-case→camelCase always.
204
+ */
205
+ const _KNOWN_VALIDATORS = [
206
+ 'structure', 'docsSync', 'drift', 'changelog', 'testSpec', 'environment',
207
+ 'security', 'architecture', 'freshness', 'traceability', 'docsDiff',
208
+ 'apiSurface', 'metadataSync', 'docsCoverage', 'docQuality', 'todoTracking',
209
+ 'schemaSync', 'specKit', 'crossReference', 'generatedStaleness',
210
+ 'canonicalSync', 'surfaceSync', 'metricsConsistency',
211
+ ];
212
+
213
+ function _kebabToCamel(k) {
214
+ return k.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
215
+ }
216
+
217
+ function _normalizeValidatorKeys(map) {
218
+ if (!map || typeof map !== 'object' || Array.isArray(map)) return map;
219
+ const out = {};
220
+ for (const [k, v] of Object.entries(map)) {
221
+ const normalized = k.includes('-') ? _kebabToCamel(k) : k;
222
+ out[normalized] = v;
223
+ }
224
+ return out;
225
+ }
226
+
227
+ function normalizeConfig(cfg) {
228
+ if (!cfg || typeof cfg !== 'object') return cfg;
229
+ const out = { ...cfg };
230
+ if (out.validators) out.validators = _normalizeValidatorKeys(out.validators);
231
+ if (out.severity) out.severity = _normalizeValidatorKeys(out.severity);
232
+ return out;
233
+ }
234
+
235
+ function deepMerge(target, source) {
236
+ const result = { ...target };
237
+ for (const key of Object.keys(source)) {
238
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
239
+ result[key] = deepMerge(target[key] || {}, source[key]);
240
+ } else {
241
+ result[key] = source[key];
242
+ }
243
+ }
244
+ return result;
245
+ }
package/cli/docguard.mjs CHANGED
@@ -49,224 +49,9 @@ import { ensureSkills } from './ensure-skills.mjs';
49
49
 
50
50
  // ── Shared constants (imported to break circular dependencies) ──────────
51
51
  import { c, PROFILES } from './shared.mjs';
52
- import { mergeIgnoreFile } from './shared-ignore.mjs';
52
+ import { loadConfig } from './config.mjs';
53
53
  export { c, PROFILES };
54
54
 
55
- // ── Config Loading ─────────────────────────────────────────────────────────
56
- export function loadConfig(projectDir) {
57
- const configPath = resolve(projectDir, '.docguard.json');
58
- const defaults = {
59
- projectName: basename(projectDir),
60
- version: '0.2',
61
- profile: 'standard',
62
- requiredFiles: {
63
- canonical: [
64
- 'docs-canonical/ARCHITECTURE.md',
65
- 'docs-canonical/DATA-MODEL.md',
66
- 'docs-canonical/SECURITY.md',
67
- 'docs-canonical/TEST-SPEC.md',
68
- 'docs-canonical/ENVIRONMENT.md',
69
- ],
70
- agentFile: ['AGENTS.md', 'CLAUDE.md'],
71
- changelog: 'CHANGELOG.md',
72
- driftLog: 'DRIFT-LOG.md',
73
- },
74
- // All CDD document types — required vs optional
75
- documentTypes: {
76
- // Canonical (design intent) — required by default
77
- 'docs-canonical/ARCHITECTURE.md': { required: true, category: 'canonical', description: 'System design, components, layer boundaries' },
78
- 'docs-canonical/DATA-MODEL.md': { required: true, category: 'canonical', description: 'Database schemas, entities, relationships' },
79
- 'docs-canonical/SECURITY.md': { required: true, category: 'canonical', description: 'Authentication, authorization, secrets management' },
80
- 'docs-canonical/TEST-SPEC.md': { required: true, category: 'canonical', description: 'Test categories, coverage rules, service-to-test map' },
81
- 'docs-canonical/ENVIRONMENT.md': { required: true, category: 'canonical', description: 'Environment variables, setup steps, prerequisites' },
82
- 'docs-canonical/DEPLOYMENT.md': { required: false, category: 'canonical', description: 'Infrastructure, CI/CD pipeline, DNS, monitoring' },
83
- 'docs-canonical/ADR.md': { required: false, category: 'canonical', description: 'Architecture Decision Records with rationale' },
84
- // Implementation (current state) — optional by default
85
- 'docs-implementation/KNOWN-GOTCHAS.md': { required: false, category: 'implementation', description: 'Lessons learned — symptom/gotcha/fix format' },
86
- 'docs-implementation/TROUBLESHOOTING.md': { required: false, category: 'implementation', description: 'Error diagnosis guides by category' },
87
- 'docs-implementation/RUNBOOKS.md': { required: false, category: 'implementation', description: 'Operational procedures (deploy, rollback, backup)' },
88
- 'docs-implementation/CURRENT-STATE.md': { required: false, category: 'implementation', description: 'Deployment status, feature completion, tech debt' },
89
- 'docs-implementation/VENDOR-BUGS.md': { required: false, category: 'implementation', description: 'Third-party bug tracker with workarounds' },
90
- // Root files
91
- 'AGENTS.md': { required: true, category: 'agent', description: 'AI agent behavior rules and project context' },
92
- 'CHANGELOG.md': { required: true, category: 'tracking', description: 'All notable changes per Keep a Changelog format' },
93
- 'DRIFT-LOG.md': { required: true, category: 'tracking', description: 'Documented deviations from canonical docs' },
94
- 'ROADMAP.md': { required: false, category: 'tracking', description: 'Project phases, feature tracking, vision' },
95
- },
96
- sourcePatterns: {
97
- services: 'src/services/**/*.{ts,js,py,java}',
98
- routes: 'src/routes/**/*.{ts,js,py,java}',
99
- tests: 'tests/**/*.test.{ts,js,py,java}',
100
- },
101
- validators: {
102
- structure: true,
103
- docsSync: true,
104
- drift: true,
105
- changelog: true,
106
- architecture: false,
107
- testSpec: true,
108
- security: false,
109
- environment: true,
110
- freshness: true,
111
- },
112
- };
113
-
114
- if (existsSync(configPath)) {
115
- try {
116
- const userConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
117
-
118
- // Apply profile presets BEFORE merging user config
119
- // Profile sets the baseline, user config can override anything
120
- const profileName = userConfig.profile || defaults.profile;
121
- const profilePreset = PROFILES[profileName];
122
- const withProfile = profilePreset
123
- ? deepMerge(defaults, profilePreset)
124
- : defaults;
125
-
126
- // v0.17-P4: normalize validator/severity keys before merging so the
127
- // user can write either kebab-case (`test-spec`) or camelCase (`testSpec`)
128
- // and the internal lookups (always camelCase) still hit.
129
- const merged = deepMerge(withProfile, normalizeConfig(userConfig));
130
- merged.profile = profileName;
131
-
132
- // Auto-detect project type if not set
133
- if (!merged.projectType) {
134
- merged.projectType = autoDetectProjectType(projectDir);
135
- }
136
- // Ensure projectTypeConfig has sensible defaults based on type
137
- merged.projectTypeConfig = {
138
- ...getProjectTypeDefaults(merged.projectType),
139
- ...(merged.projectTypeConfig || {}),
140
- };
141
- // Normalize testPattern (string) → testPatterns (array) for backward compat
142
- if (merged.testPattern && !merged.testPatterns) {
143
- merged.testPatterns = [merged.testPattern];
144
- } else if (merged.testPattern && merged.testPatterns) {
145
- // Both set — merge, deduplicate
146
- if (!merged.testPatterns.includes(merged.testPattern)) {
147
- merged.testPatterns.push(merged.testPattern);
148
- }
149
- }
150
- // Merge .docguardignore patterns into config.ignore so every validator
151
- // honors them without having to know about the file.
152
- mergeIgnoreFile(projectDir, merged);
153
- return merged;
154
- } catch (e) {
155
- console.error(`${c.red}Error parsing .docguard.json: ${e.message}${c.reset}`);
156
- process.exit(1);
157
- }
158
- }
159
-
160
- // No config file — auto-detect everything
161
- defaults.projectType = autoDetectProjectType(projectDir);
162
- defaults.projectTypeConfig = getProjectTypeDefaults(defaults.projectType);
163
- // .docguardignore is read even when no .docguard.json exists — keeps
164
- // ignore-only projects (no config but want to skip paths) working.
165
- mergeIgnoreFile(projectDir, defaults);
166
- return defaults;
167
- }
168
-
169
- // PROFILES is exported from shared.mjs (re-exported at line 43)
170
-
171
- /**
172
- * Auto-detect project type from package.json and file structure.
173
- * Returns: 'cli' | 'library' | 'webapp' | 'api' | 'unknown'
174
- */
175
- function autoDetectProjectType(dir) {
176
- const pkgPath = resolve(dir, 'package.json');
177
- if (existsSync(pkgPath)) {
178
- try {
179
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
180
- const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
181
-
182
- // CLI tool: has "bin" field
183
- if (pkg.bin) return 'cli';
184
-
185
- // Web app: has a frontend framework
186
- if (allDeps.next || allDeps.react || allDeps.vue || allDeps['@angular/core'] ||
187
- allDeps.svelte || allDeps.nuxt || allDeps['@sveltejs/kit']) return 'webapp';
188
-
189
- // API: has a server framework but no frontend
190
- if (allDeps.express || allDeps.fastify || allDeps.hono || allDeps.koa) return 'api';
191
-
192
- // Library: has "main" or "exports" and no framework
193
- if (pkg.main || pkg.exports || pkg.module) return 'library';
194
- } catch { /* fall through */ }
195
- }
196
-
197
- // Python project
198
- if (existsSync(resolve(dir, 'manage.py'))) return 'webapp';
199
- if (existsSync(resolve(dir, 'setup.py')) || existsSync(resolve(dir, 'pyproject.toml'))) return 'library';
200
-
201
- return 'unknown';
202
- }
203
-
204
- /**
205
- * Get default projectTypeConfig for a given project type.
206
- */
207
- function getProjectTypeDefaults(type) {
208
- const defaults = {
209
- cli: { needsEnvVars: false, needsEnvExample: false, needsE2E: false, needsDatabase: false, testFramework: 'node:test', runCommand: null },
210
- library: { needsEnvVars: false, needsEnvExample: false, needsE2E: false, needsDatabase: false, testFramework: 'vitest', runCommand: null },
211
- webapp: { needsEnvVars: true, needsEnvExample: true, needsE2E: true, needsDatabase: true, testFramework: 'vitest', runCommand: 'npm run dev' },
212
- api: { needsEnvVars: true, needsEnvExample: true, needsE2E: false, needsDatabase: true, testFramework: 'vitest', runCommand: 'npm run dev' },
213
- unknown: { needsEnvVars: true, needsEnvExample: true, needsE2E: false, needsDatabase: true, testFramework: null, runCommand: null },
214
- };
215
- return defaults[type] || defaults.unknown;
216
- }
217
-
218
- /**
219
- * v0.17-P4: normalize validator-key naming so users can write either
220
- * `validators: { "test-spec": true }` (kebab-case, matches CLI display)
221
- * or `validators: { testSpec: true }` (camelCase, matches JSON internals)
222
- * in `.docguard.json`. We normalize the WHOLE config tree's known validator
223
- * keys to camelCase before merging. Same treatment applied to `severity`.
224
- *
225
- * Non-validator keys are left alone. Unknown keys (forward-compat) are
226
- * normalized blindly: kebab-case→camelCase always.
227
- */
228
- const _KNOWN_VALIDATORS = [
229
- 'structure', 'docsSync', 'drift', 'changelog', 'testSpec', 'environment',
230
- 'security', 'architecture', 'freshness', 'traceability', 'docsDiff',
231
- 'apiSurface', 'metadataSync', 'docsCoverage', 'docQuality', 'todoTracking',
232
- 'schemaSync', 'specKit', 'crossReference', 'generatedStaleness',
233
- 'canonicalSync', 'surfaceSync', 'metricsConsistency',
234
- ];
235
-
236
- function _kebabToCamel(k) {
237
- return k.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
238
- }
239
-
240
- function _normalizeValidatorKeys(map) {
241
- if (!map || typeof map !== 'object' || Array.isArray(map)) return map;
242
- const out = {};
243
- for (const [k, v] of Object.entries(map)) {
244
- const normalized = k.includes('-') ? _kebabToCamel(k) : k;
245
- out[normalized] = v;
246
- }
247
- return out;
248
- }
249
-
250
- function normalizeConfig(cfg) {
251
- if (!cfg || typeof cfg !== 'object') return cfg;
252
- const out = { ...cfg };
253
- if (out.validators) out.validators = _normalizeValidatorKeys(out.validators);
254
- if (out.severity) out.severity = _normalizeValidatorKeys(out.severity);
255
- return out;
256
- }
257
-
258
- function deepMerge(target, source) {
259
- const result = { ...target };
260
- for (const key of Object.keys(source)) {
261
- if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
262
- result[key] = deepMerge(target[key] || {}, source[key]);
263
- } else {
264
- result[key] = source[key];
265
- }
266
- }
267
- return result;
268
- }
269
-
270
55
  // ── Banner ─────────────────────────────────────────────────────────────────
271
56
  function printBanner() {
272
57
  console.log(`
@@ -288,7 +73,7 @@ ${c.bold}First-time? Try the demo (no install, no setup):${c.reset}
288
73
 
289
74
  ${c.bold}The Daily 5${c.reset} ${c.dim}— what you'll reach for 95% of the time${c.reset}
290
75
  ${c.green}init${c.reset} Bootstrap a project — auto-detects existing code and scans (${c.cyan}--skeleton${c.reset} for blank templates, ${c.cyan}--wizard${c.reset} for guided, ${c.cyan}--with <name>${c.reset} for scaffolders)
291
- ${c.green}guard${c.reset} Validate against canonical docs (23 validators)
76
+ ${c.green}guard${c.reset} Validate against canonical docs (all validators)
292
77
  ${c.green}diff${c.reset} Show gaps between docs and code (add ${c.cyan}--since <ref>${c.reset} for changed-file impact)
293
78
  ${c.green}sync${c.reset} Refresh code-truth doc sections — keeps memory always up to date
294
79
  ${c.green}score${c.reset} CDD maturity score (0-100; ${c.cyan}--diff${c.reset} for delta between refs)
@@ -456,6 +241,12 @@ async function main() {
456
241
  // Default stays on (discoverability), but lets minimalist library
457
242
  // projects skip the .specify/.agent/commands scaffolding.
458
243
  flags.noSpecKit = true;
244
+ } else if (args[i] === '--spec-kit') {
245
+ // v0.24: explicit opt-IN to the Spec Kit framework scaffold. The
246
+ // `starter` profile skips that scaffold by default (minimal, for side
247
+ // projects — it would otherwise drop ~30 files); pass --spec-kit to
248
+ // include it anyway. No effect on other profiles (already on by default).
249
+ flags.specKit = true;
459
250
  } else if (args[i] === '--pin') {
460
251
  // v0.17-P1: `docguard guard --pin` records the running CLI version
461
252
  // into .docguard.json (`docguardVersion` field) after a successful run.
@@ -513,6 +304,11 @@ async function main() {
513
304
  flags.debate = true;
514
305
  } else if (args[i] === '--stdout') {
515
306
  flags.stdout = true;
307
+ } else if (args[i] === '--help' || args[i] === '-h') {
308
+ // v0.24: capture --help anywhere on the line, not just as the bare
309
+ // command. Previously `docguard generate --help` fell through the parser
310
+ // and executed generate, scaffolding files into the cwd (field report).
311
+ flags.help = true;
516
312
  }
517
313
  }
518
314
 
@@ -528,6 +324,14 @@ async function main() {
528
324
  process.exit(0);
529
325
  }
530
326
 
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).
330
+ if (flags.help) {
331
+ printHelp();
332
+ process.exit(0);
333
+ }
334
+
531
335
  // In JSON mode the entire stdout MUST be parseable JSON. The banner and
532
336
  // ensureSkills' install message would corrupt the output for any
533
337
  // programmatic consumer (CI, dashboards, the Score-on-PR Action recipe).