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.
- package/README.md +4 -4
- package/cli/commands/demo.mjs +1 -1
- package/cli/commands/diff.mjs +19 -8
- package/cli/commands/explain.mjs +178 -17
- package/cli/commands/fix.mjs +17 -2
- package/cli/commands/generate.mjs +2 -2
- package/cli/commands/guard.mjs +86 -11
- package/cli/commands/hooks.mjs +12 -7
- package/cli/commands/init.mjs +18 -6
- package/cli/commands/score.mjs +147 -61
- package/cli/commands/setup.mjs +2 -2
- package/cli/commands/trace.mjs +3 -101
- package/cli/commands/upgrade.mjs +61 -13
- package/cli/config.mjs +245 -0
- package/cli/docguard.mjs +21 -217
- package/cli/ensure-skills.mjs +24 -26
- package/cli/scanners/api-doc.mjs +17 -3
- package/cli/scanners/doc-tools.mjs +32 -15
- package/cli/scanners/frontend.mjs +24 -8
- package/cli/scanners/js-ast.mjs +432 -0
- package/cli/scanners/memory-plan.mjs +1 -1
- package/cli/scanners/py-ast.mjs +213 -0
- package/cli/scanners/routes.mjs +194 -69
- package/cli/scanners/schemas.mjs +97 -51
- package/cli/scanners/speckit.mjs +14 -0
- package/cli/shared-git.mjs +0 -0
- package/cli/shared-ignore.mjs +16 -1
- package/cli/shared-source.mjs +59 -2
- package/cli/shared-trace-patterns.mjs +118 -0
- package/cli/shared.mjs +60 -1
- package/cli/validator-markers.mjs +91 -0
- package/cli/validators/api-surface.mjs +37 -3
- package/cli/validators/canonical-sync.mjs +22 -19
- package/cli/validators/doc-quality.mjs +27 -44
- package/cli/validators/docs-coverage.mjs +13 -0
- package/cli/validators/docs-diff.mjs +16 -6
- package/cli/validators/docs-sync.mjs +4 -3
- package/cli/validators/drift.mjs +3 -2
- package/cli/validators/freshness.mjs +47 -15
- package/cli/validators/metadata-sync.mjs +21 -11
- package/cli/validators/metrics-consistency.mjs +45 -17
- package/cli/validators/security.mjs +13 -5
- package/cli/validators/structure.mjs +6 -5
- package/cli/validators/surface-sync.mjs +7 -5
- package/cli/validators/test-spec.mjs +76 -51
- package/cli/validators/todo-tracking.mjs +4 -2
- package/cli/validators/traceability.mjs +12 -54
- package/cli/writers/sections.mjs +32 -19
- package/docs/commands.md +1 -1
- package/docs/configuration.md +11 -0
- package/docs/faq.md +1 -1
- package/extensions/spec-kit-docguard/README.md +1 -1
- package/extensions/spec-kit-docguard/extension.yml +2 -2
- 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 -1
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +3 -2
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +2 -2
- package/package.json +5 -3
package/cli/commands/upgrade.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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 {
|
|
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 (
|
|
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).
|