docguard-cli 0.23.0 → 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 +1 -1
- package/cli/commands/diff.mjs +1 -1
- 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 -3
- package/cli/commands/upgrade.mjs +61 -13
- package/cli/config.mjs +18 -1
- package/cli/docguard.mjs +19 -0
- 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/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 +13 -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 +2 -42
- package/cli/validators/docs-coverage.mjs +13 -0
- 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 +11 -3
- 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/ensure-skills.mjs
CHANGED
|
@@ -46,9 +46,12 @@ const __dirname = dirname(__filename);
|
|
|
46
46
|
const SKILLS_SOURCE = resolve(__dirname, '..', 'extensions', 'spec-kit-docguard', 'skills');
|
|
47
47
|
const COMMANDS_SOURCE = resolve(__dirname, '..', 'commands');
|
|
48
48
|
|
|
49
|
-
// Destination in the user's project
|
|
49
|
+
// Destination in the user's project. Commands live UNDER `.agent/` alongside
|
|
50
|
+
// skills (was root `commands/`, which polluted the project namespace and got
|
|
51
|
+
// mis-scanned as source). `.agent/commands/` is the generic spec-kit convention
|
|
52
|
+
// agents already discover, and keeps DocGuard's footprint in one place.
|
|
50
53
|
const SKILLS_DEST = '.agent/skills';
|
|
51
|
-
const COMMANDS_DEST = 'commands';
|
|
54
|
+
const COMMANDS_DEST = '.agent/commands';
|
|
52
55
|
|
|
53
56
|
// ── Agent Mode Detection ────────────────────────────────────────────────
|
|
54
57
|
|
|
@@ -195,14 +198,6 @@ export function isSpecKitInitialized(projectDir) {
|
|
|
195
198
|
|
|
196
199
|
// ── Spec-Kit Integration Gate ───────────────────────────────────────────
|
|
197
200
|
|
|
198
|
-
// Read DocGuard package version (for skill auto-update)
|
|
199
|
-
const PKG_VERSION = (() => {
|
|
200
|
-
try {
|
|
201
|
-
const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
|
|
202
|
-
return pkg.version || '0.0.0';
|
|
203
|
-
} catch { return '0.0.0'; }
|
|
204
|
-
})();
|
|
205
|
-
|
|
206
201
|
const SPEC_KIT_INSTALL_CMD = 'uv tool install specify-cli --from git+https://github.com/github/spec-kit.git';
|
|
207
202
|
|
|
208
203
|
/**
|
|
@@ -225,6 +220,13 @@ export function ensureSpecKit(projectDir, flags = {}) {
|
|
|
225
220
|
return { specKitReady: true };
|
|
226
221
|
}
|
|
227
222
|
|
|
223
|
+
// Caller opted out of the Spec Kit framework scaffold (--no-spec-kit, or the
|
|
224
|
+
// minimal `starter` profile which passes noSpecKit through). Don't auto-init
|
|
225
|
+
// and don't nag — DocGuard's own skills/commands still install below.
|
|
226
|
+
if (flags.noSpecKit) {
|
|
227
|
+
return { specKitReady: false, skipped: true };
|
|
228
|
+
}
|
|
229
|
+
|
|
228
230
|
// Spec-kit CLI available — auto-initialize
|
|
229
231
|
if (isSpecKitAvailable()) {
|
|
230
232
|
if (!silent) {
|
|
@@ -298,26 +300,22 @@ export function ensureSkills(projectDir, flags = {}) {
|
|
|
298
300
|
|
|
299
301
|
for (const skillDir of skillDirs) {
|
|
300
302
|
const destDir = resolve(projectDir, SKILLS_DEST, skillDir);
|
|
301
|
-
if (!existsSync(destDir)) {
|
|
302
|
-
mkdirSync(destDir, { recursive: true });
|
|
303
|
-
}
|
|
304
303
|
const srcSkill = resolve(SKILLS_SOURCE, skillDir, 'SKILL.md');
|
|
305
304
|
const destSkill = resolve(destDir, 'SKILL.md');
|
|
306
305
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
306
|
+
const srcContent = readFileSync(srcSkill, 'utf-8');
|
|
307
|
+
const installedContent = existsSync(destSkill) ? readFileSync(destSkill, 'utf-8') : null;
|
|
308
|
+
|
|
309
|
+
// Content-equality gate: write only when the bundled skill differs from
|
|
310
|
+
// what's on disk. Covers a fresh install AND a genuine update, but stops
|
|
311
|
+
// the per-command rewrite churn the old version-marker gate caused — a
|
|
312
|
+
// skill whose SKILL.md lacked a `docguard:version:` marker compared as
|
|
313
|
+
// '0.0.0', so it was rewritten (and announced) on EVERY command, even
|
|
314
|
+
// read-only ones like `explain`/`score` (field report, Issue D).
|
|
315
|
+
if (installedContent !== srcContent) {
|
|
316
|
+
if (!existsSync(destDir)) mkdirSync(destDir, { recursive: true });
|
|
317
|
+
writeFileSync(destSkill, srcContent, 'utf-8');
|
|
310
318
|
result.skillsInstalled = true;
|
|
311
|
-
} else {
|
|
312
|
-
// Auto-update: check if package version is newer than installed
|
|
313
|
-
const installedContent = readFileSync(destSkill, 'utf-8');
|
|
314
|
-
const versionMatch = installedContent.match(/docguard:version:\s*(\S+)/);
|
|
315
|
-
const installedVersion = versionMatch ? versionMatch[1] : '0.0.0';
|
|
316
|
-
|
|
317
|
-
if (installedVersion !== PKG_VERSION) {
|
|
318
|
-
writeFileSync(destSkill, readFileSync(srcSkill, 'utf-8'), 'utf-8');
|
|
319
|
-
result.skillsInstalled = true;
|
|
320
|
-
}
|
|
321
319
|
}
|
|
322
320
|
}
|
|
323
321
|
|
package/cli/scanners/api-doc.mjs
CHANGED
|
@@ -22,7 +22,17 @@ const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', '
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Normalize an API path for comparison.
|
|
25
|
-
* Strips decoration and collapses
|
|
25
|
+
* Strips decoration and collapses ALL dynamic-segment syntaxes to a single `{}`
|
|
26
|
+
* placeholder so an endpoint documented one way matches the same endpoint
|
|
27
|
+
* emitted another way:
|
|
28
|
+
* Express/colon `/users/:id` → `/users/{}`
|
|
29
|
+
* OpenAPI/brace `/users/{id}` → `/users/{}`
|
|
30
|
+
* Next.js bracket `/users/[id]` → `/users/{}`
|
|
31
|
+
* catch-all `/auth/[...nextauth]`, `/auth/:nextauth*` → `/auth/{}`
|
|
32
|
+
* optional c-all `/shop/[[...filters]]` → `/shop/{}`
|
|
33
|
+
* Without the bracket rule, a doc written in Next.js `[id]` syntax never matched
|
|
34
|
+
* the code-scan's `:id`, so every dynamic route double-fired as both
|
|
35
|
+
* "documented-but-absent" and "undocumented" (field test: hugocross_revamp).
|
|
26
36
|
* @param {string} raw
|
|
27
37
|
* @returns {string} normalized path (e.g. "/api/users/{}") or '' if not a path
|
|
28
38
|
*/
|
|
@@ -34,8 +44,12 @@ export function normalizePath(raw) {
|
|
|
34
44
|
// cut query string / fragment
|
|
35
45
|
p = p.split(/[?#]/)[0];
|
|
36
46
|
if (!p.startsWith('/')) return '';
|
|
37
|
-
// collapse param syntax
|
|
38
|
-
|
|
47
|
+
// collapse every param syntax to {}: Next.js [id]/[...slug]/[[...slug]],
|
|
48
|
+
// OpenAPI {param}, and colon :param (incl. catch-all :param*).
|
|
49
|
+
p = p
|
|
50
|
+
.replace(/\[{1,2}[^\]]*\]{1,2}/g, '{}')
|
|
51
|
+
.replace(/\{[^}/]+\}/g, '{}')
|
|
52
|
+
.replace(/:[^/]+/g, '{}');
|
|
39
53
|
// strip trailing slash (but keep root "/")
|
|
40
54
|
if (p.length > 1) p = p.replace(/\/+$/, '');
|
|
41
55
|
return p;
|
|
@@ -9,6 +9,21 @@
|
|
|
9
9
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
10
10
|
import { resolve, join } from 'node:path';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Read + parse a JSON file, returning null on any error (missing, unreadable,
|
|
14
|
+
* malformed). A bare `JSON.parse(readFileSync(...))` on a malformed
|
|
15
|
+
* `package.json` used to THROW out of a detector and abort the entire
|
|
16
|
+
* `detectDocTools` scan — which then made the memory plan (and every validator
|
|
17
|
+
* that compares against it) see empty "truth" and falsely pass. Fail soft.
|
|
18
|
+
*/
|
|
19
|
+
function readJsonSafe(path) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
/**
|
|
13
28
|
* Detect all documentation tools present in the project.
|
|
14
29
|
* @param {string} dir - Project root directory
|
|
@@ -57,6 +72,7 @@ export function detectOpenAPI(dir) {
|
|
|
57
72
|
endpoints: spec.endpoints,
|
|
58
73
|
schemas: spec.schemas,
|
|
59
74
|
info: spec.info,
|
|
75
|
+
parseIncomplete: spec.parseIncomplete === true,
|
|
60
76
|
};
|
|
61
77
|
}
|
|
62
78
|
}
|
|
@@ -122,6 +138,14 @@ function parseOpenAPISpec(content, filename) {
|
|
|
122
138
|
}
|
|
123
139
|
} catch { /* spec parsing failed, return empty */ }
|
|
124
140
|
|
|
141
|
+
// Honest-failure signal: a spec that clearly declares a `paths:` section but
|
|
142
|
+
// yielded ZERO endpoints means our parser couldn't extract them (anchors,
|
|
143
|
+
// unresolved $ref, folded scalars, or a YAML feature the minimal parser
|
|
144
|
+
// doesn't cover). Flag it so the caller can WARN and fall back to code
|
|
145
|
+
// scanning, instead of silently reporting "no API surface" — a false green.
|
|
146
|
+
const hasPathsKey = /(^|\n)[ \t]*["']?paths["']?[ \t]*:/.test(content);
|
|
147
|
+
result.parseIncomplete = hasPathsKey && result.endpoints.length === 0;
|
|
148
|
+
|
|
125
149
|
return result;
|
|
126
150
|
}
|
|
127
151
|
|
|
@@ -215,12 +239,9 @@ function detectTypeDoc(dir) {
|
|
|
215
239
|
}
|
|
216
240
|
|
|
217
241
|
// Check package.json devDeps
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
if (pkg.devDependencies?.typedoc) {
|
|
222
|
-
return { found: true, config: 'package.json (devDependency)' };
|
|
223
|
-
}
|
|
242
|
+
const pkg = readJsonSafe(resolve(dir, 'package.json'));
|
|
243
|
+
if (pkg?.devDependencies?.typedoc) {
|
|
244
|
+
return { found: true, config: 'package.json (devDependency)' };
|
|
224
245
|
}
|
|
225
246
|
|
|
226
247
|
return { found: false };
|
|
@@ -236,12 +257,9 @@ function detectJSDoc(dir) {
|
|
|
236
257
|
}
|
|
237
258
|
}
|
|
238
259
|
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
if (pkg.devDependencies?.jsdoc) {
|
|
243
|
-
return { found: true, config: 'package.json (devDependency)' };
|
|
244
|
-
}
|
|
260
|
+
const pkg = readJsonSafe(resolve(dir, 'package.json'));
|
|
261
|
+
if (pkg?.devDependencies?.jsdoc) {
|
|
262
|
+
return { found: true, config: 'package.json (devDependency)' };
|
|
245
263
|
}
|
|
246
264
|
|
|
247
265
|
return { found: false };
|
|
@@ -316,9 +334,8 @@ function detectRedocly(dir) {
|
|
|
316
334
|
// ── Swagger UI ─────────────────────────────────────────────────────────────
|
|
317
335
|
|
|
318
336
|
function detectSwagger(dir) {
|
|
319
|
-
const
|
|
320
|
-
if (
|
|
321
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
337
|
+
const pkg = readJsonSafe(resolve(dir, 'package.json'));
|
|
338
|
+
if (pkg) {
|
|
322
339
|
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
323
340
|
if (allDeps['swagger-ui-express'] || allDeps['@fastify/swagger'] || allDeps['swagger-jsdoc']) {
|
|
324
341
|
return {
|
|
@@ -16,12 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
18
18
|
import { resolve, join, relative, basename, extname } from 'node:path';
|
|
19
|
-
import { resolveSourceRoots, collectPackageJsons } from '../shared-source.mjs';
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
|
|
23
|
-
'.cache', '__pycache__', '.venv', 'vendor', '.turbo', '.vercel',
|
|
24
|
-
]);
|
|
19
|
+
import { resolveSourceRoots, collectPackageJsons, readScannable } from '../shared-source.mjs';
|
|
20
|
+
import { DEFAULT_IGNORE_DIRS as IGNORE_DIRS, shouldIgnore, relPosix } from '../shared-ignore.mjs';
|
|
21
|
+
import { extractJsxRouteScreens } from './js-ast.mjs';
|
|
25
22
|
const UI_EXT = new Set(['.tsx', '.jsx']);
|
|
26
23
|
|
|
27
24
|
function walk(dir, onFile, depth = 0) {
|
|
@@ -36,7 +33,7 @@ function walk(dir, onFile, depth = 0) {
|
|
|
36
33
|
}
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
function readSafe(p) {
|
|
36
|
+
function readSafe(p) { return readScannable(p) ?? ''; } // size-capped; skips bundles
|
|
40
37
|
|
|
41
38
|
/** Normalize a route path param syntax to {param} and strip trailing slash. */
|
|
42
39
|
function normRoute(p) {
|
|
@@ -111,6 +108,15 @@ function scanReactRouterScreens(roots, projectDir) {
|
|
|
111
108
|
if (!content.includes('<Route') && !content.includes('createBrowserRouter') &&
|
|
112
109
|
!content.includes('useRoutes') && !content.includes('createRoutesFrom')) return;
|
|
113
110
|
|
|
111
|
+
// AST-first: scopes each route's element JSX exactly (nested auth wrappers,
|
|
112
|
+
// layouts, and multi-line elements no longer truncate or mis-pick the
|
|
113
|
+
// screen). `null` → parse failure → the window-based regex fallback below.
|
|
114
|
+
const astScreens = extractJsxRouteScreens(content, file);
|
|
115
|
+
if (astScreens) {
|
|
116
|
+
for (const s of astScreens) add(s.path, pickScreenComponent(s.components), file);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
114
120
|
let m;
|
|
115
121
|
const re = new RegExp(pathRe.source, 'g');
|
|
116
122
|
while ((m = re.exec(content)) !== null) {
|
|
@@ -434,5 +440,15 @@ export function scanFrontend(projectDir, config = {}) {
|
|
|
434
440
|
a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
435
441
|
const i18n = scanI18n(projectDir, roots);
|
|
436
442
|
|
|
437
|
-
|
|
443
|
+
// Honor .docguardignore / config.ignore — drop entries whose source file the
|
|
444
|
+
// user excluded (e.g. a fixtures/storybook dir). Entries without a `file`
|
|
445
|
+
// (or in i18n) are unaffected.
|
|
446
|
+
const keep = (arr) => Array.isArray(arr)
|
|
447
|
+
? arr.filter(x => !x || !x.file || !shouldIgnore(relPosix(projectDir, resolve(projectDir, x.file)), config))
|
|
448
|
+
: arr;
|
|
449
|
+
return {
|
|
450
|
+
...stack, routerType,
|
|
451
|
+
screens: keep(screens), components: keep(components), stores: keep(stores),
|
|
452
|
+
hooks: keep(hooks), contexts: keep(contexts), apiCalls: keep(apiCalls), i18n,
|
|
453
|
+
};
|
|
438
454
|
}
|