docguard-cli 0.11.2 → 0.12.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 +34 -15
- package/cli/commands/guard.mjs +112 -5
- package/cli/commands/init.mjs +52 -1
- package/cli/commands/upgrade.mjs +250 -0
- package/cli/docguard.mjs +30 -3
- package/cli/shared-ignore.mjs +50 -0
- package/cli/shared.mjs +62 -0
- package/cli/validators/cross-reference.mjs +281 -0
- package/commands/docguard.guard.md +2 -2
- package/docs/quickstart.md +1 -1
- package/extensions/spec-kit-docguard/README.md +1 -1
- package/extensions/spec-kit-docguard/commands/guard.md +1 -1
- package/extensions/spec-kit-docguard/extension.yml +11 -1
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +2 -2
- package/extensions/spec-kit-docguard/skills/docguard-guard/SKILL.md +3 -3
- 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 +1 -1
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-autofix.yml +51 -0
- package/extensions/spec-kit-docguard/templates/github-workflows/docguard-guard.yml +48 -0
- package/package.json +1 -1
- package/templates/commands/docguard.guard.md +2 -2
package/cli/shared-ignore.mjs
CHANGED
|
@@ -40,6 +40,56 @@ export const DEFAULT_IGNORE_DIRS = new Set([
|
|
|
40
40
|
const ALWAYS_REJECT_PATH_RE =
|
|
41
41
|
/(?:^|[/\\])(?:node_modules|\.claude[/\\]worktrees|\.git[/\\]worktrees|\.jj)(?:[/\\]|$)/;
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Read `.docguardignore` from a project directory and return its patterns.
|
|
45
|
+
*
|
|
46
|
+
* Format: gitignore-style — one pattern per line, `#` for comments, blank lines
|
|
47
|
+
* ignored. Returned patterns are normalized but not transformed (callers
|
|
48
|
+
* decide whether to expand directory globs).
|
|
49
|
+
*
|
|
50
|
+
* Returns [] if the file is missing or unreadable — never throws.
|
|
51
|
+
*/
|
|
52
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
53
|
+
import { resolve as resolvePath } from 'node:path';
|
|
54
|
+
|
|
55
|
+
export function loadDocguardIgnore(projectDir) {
|
|
56
|
+
const p = resolvePath(projectDir, '.docguardignore');
|
|
57
|
+
if (!existsSync(p)) return [];
|
|
58
|
+
try {
|
|
59
|
+
const raw = readFileSync(p, 'utf-8');
|
|
60
|
+
return raw
|
|
61
|
+
.split(/\r?\n/)
|
|
62
|
+
.map(line => line.trim())
|
|
63
|
+
.filter(line => line && !line.startsWith('#'));
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Merge `.docguardignore` patterns into a config object's `ignore` array.
|
|
71
|
+
*
|
|
72
|
+
* Used at config-load time so every validator sees the combined set without
|
|
73
|
+
* having to know about the file. Mutates and returns the config for ergonomics.
|
|
74
|
+
*
|
|
75
|
+
* Idempotent — calling twice produces the same result. Skips duplicates.
|
|
76
|
+
*/
|
|
77
|
+
export function mergeIgnoreFile(projectDir, config) {
|
|
78
|
+
const filePatterns = loadDocguardIgnore(projectDir);
|
|
79
|
+
if (filePatterns.length === 0) return config;
|
|
80
|
+
const existing = Array.isArray(config.ignore) ? config.ignore : [];
|
|
81
|
+
const seen = new Set(existing);
|
|
82
|
+
const merged = [...existing];
|
|
83
|
+
for (const p of filePatterns) {
|
|
84
|
+
if (!seen.has(p)) {
|
|
85
|
+
merged.push(p);
|
|
86
|
+
seen.add(p);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
config.ignore = merged;
|
|
90
|
+
return config;
|
|
91
|
+
}
|
|
92
|
+
|
|
43
93
|
/**
|
|
44
94
|
* Convert a glob pattern to a RegExp.
|
|
45
95
|
* Supports: * (any chars except /), ** (any path segments), . (literal dot).
|
package/cli/shared.mjs
CHANGED
|
@@ -4,6 +4,68 @@
|
|
|
4
4
|
* All commands import from here instead of docguard.mjs.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Current .docguard.json schema version that this CLI version writes via
|
|
9
|
+
* `docguard init`. Bump this when adding fields that need migration (e.g.
|
|
10
|
+
* v0.12 adds `severity` overrides per validator).
|
|
11
|
+
*
|
|
12
|
+
* The post-guard nudge fires when an existing project's stored
|
|
13
|
+
* `.docguard.json.version` is BEHIND this constant — pointing users at
|
|
14
|
+
* `docguard upgrade` to migrate.
|
|
15
|
+
*/
|
|
16
|
+
export const CURRENT_SCHEMA_VERSION = '0.5';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Allowed severity values for per-validator `severity` overrides in
|
|
20
|
+
* `.docguard.json`. Affects EXIT-CODE behavior of `docguard guard`:
|
|
21
|
+
* - 'high': warnings from this validator fail CI (exit 1)
|
|
22
|
+
* - 'medium': default — warnings exit 2 (informational)
|
|
23
|
+
* - 'low': warnings ignored for exit code (exit 0)
|
|
24
|
+
*
|
|
25
|
+
* Display (the per-validator status lines and the summary) is unchanged
|
|
26
|
+
* regardless of severity — severity is a CI/operational knob, not a UI one.
|
|
27
|
+
*/
|
|
28
|
+
export const SEVERITY_LEVELS = new Set(['high', 'medium', 'low']);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve a validator's effective severity from config.
|
|
32
|
+
* Returns 'medium' (default) if no override is set or the override is bogus.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveSeverity(config, validatorKey) {
|
|
35
|
+
const s = config && config.severity && config.severity[validatorKey];
|
|
36
|
+
if (typeof s === 'string' && SEVERITY_LEVELS.has(s.toLowerCase())) {
|
|
37
|
+
return s.toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
return 'medium';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse a dotted-decimal version string into a tuple of integers for
|
|
44
|
+
* comparison. Tolerates extra suffixes (e.g. `0.4-beta` → [0, 4]).
|
|
45
|
+
* Returns null when the string is unparseable.
|
|
46
|
+
*/
|
|
47
|
+
export function parseVersion(v) {
|
|
48
|
+
if (!v || typeof v !== 'string') return null;
|
|
49
|
+
const m = v.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
50
|
+
if (!m) return null;
|
|
51
|
+
return [Number(m[1] || 0), Number(m[2] || 0), Number(m[3] || 0)];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compare two version strings. Returns -1 if a<b, 0 if equal, 1 if a>b.
|
|
56
|
+
* Unparseable inputs sort as equal (no nag).
|
|
57
|
+
*/
|
|
58
|
+
export function compareVersions(a, b) {
|
|
59
|
+
const pa = parseVersion(a);
|
|
60
|
+
const pb = parseVersion(b);
|
|
61
|
+
if (!pa || !pb) return 0;
|
|
62
|
+
for (let i = 0; i < 3; i++) {
|
|
63
|
+
if (pa[i] < pb[i]) return -1;
|
|
64
|
+
if (pa[i] > pb[i]) return 1;
|
|
65
|
+
}
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
7
69
|
// ── Colors (ANSI escape codes, zero deps) ──────────────────────────────────
|
|
8
70
|
export const c = {
|
|
9
71
|
reset: '\x1b[0m',
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Reference Validator — S-8 / K-7
|
|
3
|
+
*
|
|
4
|
+
* Scans canonical docs for cross-references between docs and verifies they
|
|
5
|
+
* resolve. Catches stale links when a section is renamed or removed.
|
|
6
|
+
*
|
|
7
|
+
* Reference forms supported (in rough order of frequency):
|
|
8
|
+
* - Markdown relative links: [text](./OTHER.md)
|
|
9
|
+
* [text](./OTHER.md#anchor)
|
|
10
|
+
* [text](#anchor-in-same-doc)
|
|
11
|
+
* - Bare anchor refs: see §3.2 ARCHITECTURE.md
|
|
12
|
+
* (Section 3.2 in DATA-MODEL.md)
|
|
13
|
+
* - Bracketed section refs: [Section X.Y]
|
|
14
|
+
* [ARCHITECTURE.md § Components]
|
|
15
|
+
*
|
|
16
|
+
* For each ref we extract: target_file (optional), anchor (optional). Then
|
|
17
|
+
* we check:
|
|
18
|
+
* 1. target_file exists (relative to projectDir or canonical doc dir)
|
|
19
|
+
* 2. anchor resolves to a heading or an explicit `<a name="...">` in that file
|
|
20
|
+
*
|
|
21
|
+
* Zero NPM dependencies. Pure Node.js built-ins.
|
|
22
|
+
*
|
|
23
|
+
* @req SC-K7-001 — broken markdown links between canonical docs are reported
|
|
24
|
+
* @req SC-K7-002 — broken intra-doc anchors are reported
|
|
25
|
+
* @req SC-K7-003 — external URLs (http/https) are NOT checked (those are not our problem)
|
|
26
|
+
* @req SC-K7-004 — code-fenced examples don't trigger false positives
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
30
|
+
import { resolve, join, dirname, basename } from 'node:path';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Slugify a heading the way GitHub's markdown anchors work.
|
|
34
|
+
* Mirrors GFM rules: lowercase, strip non-word chars, hyphens for spaces.
|
|
35
|
+
* Good-enough for the 95% case; not bit-perfect.
|
|
36
|
+
*/
|
|
37
|
+
export function slugifyHeading(text) {
|
|
38
|
+
return text
|
|
39
|
+
// Remove leading "## " (markdown header marker) WITHOUT trimming the
|
|
40
|
+
// leading whitespace that may follow it — that whitespace is the
|
|
41
|
+
// position where an emoji used to live and GitHub's anchor builder
|
|
42
|
+
// converts it to a leading dash. Example: `## ⚡ Quick Start` →
|
|
43
|
+
// anchor `#-quick-start` (with leading dash). Stripping it here
|
|
44
|
+
// would cause false-positive "broken anchor" warnings on any TOC
|
|
45
|
+
// generated by GitHub itself.
|
|
46
|
+
.replace(/^#+\s+/, '')
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
// Strip emoji + decorative pictographs (they leave a position which
|
|
49
|
+
// becomes a dash after whitespace-collapse below).
|
|
50
|
+
.replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE0F}]/gu, '')
|
|
51
|
+
// Strip GFM-deleted punctuation
|
|
52
|
+
.replace(/[.,;:!?'"`()[\]{}<>|\\/]/g, '')
|
|
53
|
+
// Convert whitespace runs to single dashes
|
|
54
|
+
.replace(/\s+/g, '-')
|
|
55
|
+
// Drop any remaining non-word/non-dash chars
|
|
56
|
+
.replace(/[^a-z0-9-]/g, '');
|
|
57
|
+
// NB: we do NOT collapse adjacent dashes and we do NOT strip leading/
|
|
58
|
+
// trailing dashes — GitHub's GFM keeps them. `& Academic` (with `&`
|
|
59
|
+
// removed) leaves two adjacent spaces which become `--`. README TOCs
|
|
60
|
+
// generated by GitHub itself rely on this exact behavior.
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extract all headings from a markdown file. Returns an array of
|
|
65
|
+
* { level, text, anchor } where anchor is the GFM-style slug.
|
|
66
|
+
*
|
|
67
|
+
* Skips headings inside ``` fenced code blocks to avoid false positives
|
|
68
|
+
* from example code.
|
|
69
|
+
*/
|
|
70
|
+
export function extractHeadings(content) {
|
|
71
|
+
const lines = content.split('\n');
|
|
72
|
+
const headings = [];
|
|
73
|
+
let inCodeFence = false;
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
if (line.startsWith('```')) {
|
|
76
|
+
inCodeFence = !inCodeFence;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (inCodeFence) continue;
|
|
80
|
+
const m = line.match(/^(#{1,6})\s+(.+?)\s*$/);
|
|
81
|
+
if (m) {
|
|
82
|
+
const text = m[2];
|
|
83
|
+
headings.push({ level: m[1].length, text, anchor: slugifyHeading(text) });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return headings;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Extract all candidate cross-references from a markdown file.
|
|
91
|
+
*
|
|
92
|
+
* Returns an array of { source, file, anchor, raw } where:
|
|
93
|
+
* - source = path of the document containing the reference (for reporting)
|
|
94
|
+
* - file = target file path, RELATIVE to the source file's directory.
|
|
95
|
+
* null if the link is intra-doc (just `#anchor`).
|
|
96
|
+
* - anchor = anchor part without `#`. null if no anchor.
|
|
97
|
+
* - raw = the original text matched (for error messages)
|
|
98
|
+
*
|
|
99
|
+
* Skips:
|
|
100
|
+
* - External http(s) URLs (not our problem)
|
|
101
|
+
* - mailto: links
|
|
102
|
+
* - Code-fenced blocks
|
|
103
|
+
* - Inline `code` with backticks
|
|
104
|
+
*/
|
|
105
|
+
export function extractRefs(content, sourcePath) {
|
|
106
|
+
const refs = [];
|
|
107
|
+
const lines = content.split('\n');
|
|
108
|
+
let inCodeFence = false;
|
|
109
|
+
|
|
110
|
+
// [text](target) where target is one of:
|
|
111
|
+
// ./RELATIVE.md
|
|
112
|
+
// ../OTHER.md#anchor
|
|
113
|
+
// #intra-doc-anchor
|
|
114
|
+
// We DON'T match http(s) targets here.
|
|
115
|
+
const markdownLinkRe = /\[([^\]]+)\]\(((?!https?:|mailto:)[^)]+)\)/g;
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < lines.length; i++) {
|
|
118
|
+
const line = lines[i];
|
|
119
|
+
if (line.startsWith('```')) {
|
|
120
|
+
inCodeFence = !inCodeFence;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (inCodeFence) continue;
|
|
124
|
+
|
|
125
|
+
// Strip inline `code` so we don't match links inside it
|
|
126
|
+
const stripped = line.replace(/`[^`]+`/g, '');
|
|
127
|
+
|
|
128
|
+
let m;
|
|
129
|
+
markdownLinkRe.lastIndex = 0;
|
|
130
|
+
while ((m = markdownLinkRe.exec(stripped)) !== null) {
|
|
131
|
+
const target = m[2].trim();
|
|
132
|
+
// Drop any title text: [foo](bar "title") → bar
|
|
133
|
+
const cleanTarget = target.split(/\s+/)[0];
|
|
134
|
+
const hashIdx = cleanTarget.indexOf('#');
|
|
135
|
+
let file, anchor;
|
|
136
|
+
if (hashIdx === 0) {
|
|
137
|
+
// Intra-doc anchor only
|
|
138
|
+
file = null;
|
|
139
|
+
anchor = cleanTarget.slice(1);
|
|
140
|
+
} else if (hashIdx > 0) {
|
|
141
|
+
file = cleanTarget.slice(0, hashIdx);
|
|
142
|
+
anchor = cleanTarget.slice(hashIdx + 1);
|
|
143
|
+
} else {
|
|
144
|
+
file = cleanTarget;
|
|
145
|
+
anchor = null;
|
|
146
|
+
}
|
|
147
|
+
refs.push({ source: sourcePath, file, anchor, raw: m[0], line: i + 1 });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return refs;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resolve a target file path relative to a source markdown file.
|
|
156
|
+
* Returns the absolute path or null if the file doesn't exist.
|
|
157
|
+
*/
|
|
158
|
+
function resolveTarget(sourcePath, targetRel, projectDir) {
|
|
159
|
+
if (!targetRel) return null;
|
|
160
|
+
// Try relative to source's directory first
|
|
161
|
+
const fromSource = resolve(dirname(sourcePath), targetRel);
|
|
162
|
+
if (existsSync(fromSource)) return fromSource;
|
|
163
|
+
// Also try from project root (some authors write `docs-canonical/X.md`)
|
|
164
|
+
const fromRoot = resolve(projectDir, targetRel);
|
|
165
|
+
if (existsSync(fromRoot)) return fromRoot;
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Collect canonical markdown docs to scan. Same selection logic as other
|
|
171
|
+
* validators — `docs-canonical/`, root tracking files, and AGENTS.md.
|
|
172
|
+
*/
|
|
173
|
+
function collectCanonicalDocs(projectDir) {
|
|
174
|
+
const docs = [];
|
|
175
|
+
const cdir = resolve(projectDir, 'docs-canonical');
|
|
176
|
+
if (existsSync(cdir)) {
|
|
177
|
+
try {
|
|
178
|
+
for (const f of readdirSync(cdir)) {
|
|
179
|
+
if (f.endsWith('.md')) {
|
|
180
|
+
const p = join(cdir, f);
|
|
181
|
+
if (statSync(p).isFile()) docs.push(p);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
186
|
+
for (const f of ['AGENTS.md', 'CHANGELOG.md', 'DRIFT-LOG.md', 'ROADMAP.md', 'README.md']) {
|
|
187
|
+
const p = resolve(projectDir, f);
|
|
188
|
+
if (existsSync(p)) docs.push(p);
|
|
189
|
+
}
|
|
190
|
+
return docs;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Validator entrypoint — matches the standard signature returning
|
|
195
|
+
* { errors, warnings, passed, total }.
|
|
196
|
+
*/
|
|
197
|
+
export function validateCrossReferences(projectDir, _config = {}) {
|
|
198
|
+
const errors = [];
|
|
199
|
+
const warnings = [];
|
|
200
|
+
let passed = 0;
|
|
201
|
+
let total = 0;
|
|
202
|
+
|
|
203
|
+
const docs = collectCanonicalDocs(projectDir);
|
|
204
|
+
if (docs.length === 0) {
|
|
205
|
+
return { errors, warnings, passed, total, applicable: false };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Build a map of doc path → anchor set for fast lookups during ref resolution.
|
|
209
|
+
const anchorIndex = new Map();
|
|
210
|
+
for (const d of docs) {
|
|
211
|
+
try {
|
|
212
|
+
const content = readFileSync(d, 'utf-8');
|
|
213
|
+
const headings = extractHeadings(content);
|
|
214
|
+
anchorIndex.set(d, new Set(headings.map(h => h.anchor)));
|
|
215
|
+
} catch {
|
|
216
|
+
anchorIndex.set(d, new Set());
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Walk every doc and validate each ref.
|
|
221
|
+
for (const docPath of docs) {
|
|
222
|
+
let content;
|
|
223
|
+
try { content = readFileSync(docPath, 'utf-8'); } catch { continue; }
|
|
224
|
+
const refs = extractRefs(content, docPath);
|
|
225
|
+
const docName = basename(docPath);
|
|
226
|
+
|
|
227
|
+
for (const ref of refs) {
|
|
228
|
+
total++;
|
|
229
|
+
|
|
230
|
+
// Resolve the target file (if any)
|
|
231
|
+
let targetPath = null;
|
|
232
|
+
if (ref.file) {
|
|
233
|
+
// Skip non-markdown files — we only validate doc cross-refs, not
|
|
234
|
+
// links to images / code / config files. Those have their own truth.
|
|
235
|
+
if (!ref.file.toLowerCase().endsWith('.md')) {
|
|
236
|
+
passed++;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
targetPath = resolveTarget(docPath, ref.file, projectDir);
|
|
240
|
+
if (!targetPath) {
|
|
241
|
+
warnings.push(
|
|
242
|
+
`${docName}:${ref.line} — broken link: target file "${ref.file}" not found`
|
|
243
|
+
);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
// Intra-doc anchor reference
|
|
248
|
+
targetPath = docPath;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// If there's an anchor, verify it resolves in the target doc.
|
|
252
|
+
// URL-decode the anchor first — some editors percent-encode chars
|
|
253
|
+
// like `️` (variation selector) in TOC links that wouldn't
|
|
254
|
+
// appear in the actual GitHub-rendered anchor. Decoding makes
|
|
255
|
+
// `#%EF%B8%8F-cicd-integration` compare against `#-cicd-integration`.
|
|
256
|
+
if (ref.anchor) {
|
|
257
|
+
let decodedAnchor;
|
|
258
|
+
try {
|
|
259
|
+
decodedAnchor = decodeURIComponent(ref.anchor);
|
|
260
|
+
} catch {
|
|
261
|
+
decodedAnchor = ref.anchor;
|
|
262
|
+
}
|
|
263
|
+
// After decode, re-apply the slug pipeline so we compare like-for-like.
|
|
264
|
+
const normalizedAnchor = slugifyHeading(decodedAnchor);
|
|
265
|
+
const anchors = anchorIndex.get(targetPath);
|
|
266
|
+
const matches = anchors && (anchors.has(ref.anchor) || anchors.has(normalizedAnchor));
|
|
267
|
+
if (!matches) {
|
|
268
|
+
const where = targetPath === docPath ? 'same doc' : basename(targetPath);
|
|
269
|
+
warnings.push(
|
|
270
|
+
`${docName}:${ref.line} — broken anchor: "#${ref.anchor}" in ${where} doesn't match any heading`
|
|
271
|
+
);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
passed++;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { errors, warnings, passed, total };
|
|
281
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Run DocGuard guard validation — check project documentation against CDD standards with
|
|
2
|
+
description: Run DocGuard guard validation — check project documentation against CDD standards with 21 validators
|
|
3
3
|
handoffs:
|
|
4
4
|
- label: Fix All Issues
|
|
5
5
|
agent: docguard.fix
|
|
@@ -23,7 +23,7 @@ Run the DocGuard CLI to validate all documentation against Canonical-Driven Deve
|
|
|
23
23
|
npx docguard-cli guard
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
2. **Parse the output**. Each of the
|
|
26
|
+
2. **Parse the output**. Each of the 21 validators reports ✅ (pass), ⚠️ (warning), ❌ (fail), or ➖ (N/A — nothing to validate). **A ➖ N/A is NOT a pass**: it means the validator found nothing to check (e.g. no API-REFERENCE.md, no DB schema, no layer boundaries declared). Don't read N/A as "healthy" — read it as "not assessed".
|
|
27
27
|
|
|
28
28
|
| Validator | What It Checks |
|
|
29
29
|
|-----------|---------------|
|
package/docs/quickstart.md
CHANGED
|
@@ -68,7 +68,7 @@ diagnose → AI reads prompts → AI fixes docs → guard verifies
|
|
|
68
68
|
## Verify
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
npx docguard-cli guard # Pass/fail check (
|
|
71
|
+
npx docguard-cli guard # Pass/fail check (21 validators)
|
|
72
72
|
npx docguard-cli score # 0-100 maturity score
|
|
73
73
|
```
|
|
74
74
|
|
|
@@ -4,7 +4,7 @@ Enterprise-grade Canonical-Driven Development (CDD) enforcement and **AI-readabl
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **
|
|
7
|
+
- **21 Validators** — Structure, Security, Doc Quality, Test-Spec, Drift-Comments, API-Surface, Freshness, Cross-Reference, and 13 more
|
|
8
8
|
- **Language-agnostic** — JS/TS, Python, Rust, Go, Java/Kotlin, Ruby, PHP, C#. Polyglot/monorepo-aware.
|
|
9
9
|
- **AI-powered Generate** — `generate --plan` builds the code-truth skeleton in `<!-- docguard:section -->` markers and emits a structured agent task manifest; the AI writes the prose.
|
|
10
10
|
- **Always up to date** — `sync` surgically refreshes code-truth doc sections in place, **preserves human prose**, flags prose for agent review.
|
|
@@ -14,7 +14,7 @@ handoffs:
|
|
|
14
14
|
|
|
15
15
|
# DocGuard Guard
|
|
16
16
|
|
|
17
|
-
Validate your project against its canonical documentation. Runs 160+ automated checks across
|
|
17
|
+
Validate your project against its canonical documentation. Runs 160+ automated checks across 21 validators.
|
|
18
18
|
|
|
19
19
|
## User Input
|
|
20
20
|
|
|
@@ -3,7 +3,7 @@ schema_version: "1.0"
|
|
|
3
3
|
extension:
|
|
4
4
|
id: "docguard"
|
|
5
5
|
name: "DocGuard — CDD Enforcement"
|
|
6
|
-
version: "0.
|
|
6
|
+
version: "0.12.0"
|
|
7
7
|
description: "Canonical-Driven Development enforcement as a true spec-kit extension. LLM-first design with 19 automated validators, 4 AI behavior skills, spec-kit skill chaining, and workflow hooks. Zero NPM runtime dependencies."
|
|
8
8
|
author: "Ricardo Accioly"
|
|
9
9
|
repository: "https://github.com/raccioly/docguard"
|
|
@@ -53,6 +53,16 @@ provides:
|
|
|
53
53
|
file: "commands/generate.md"
|
|
54
54
|
description: "Reverse-engineer canonical docs from existing codebase"
|
|
55
55
|
|
|
56
|
+
# GitHub Actions workflow starters — copyable templates users drop into
|
|
57
|
+
# .github/workflows/ for guard/fix/sync/score integration.
|
|
58
|
+
workflows:
|
|
59
|
+
- name: "docguard-guard"
|
|
60
|
+
file: "templates/github-workflows/docguard-guard.yml"
|
|
61
|
+
description: "Mandatory CI gate — runs all 20 validators on PR + main push"
|
|
62
|
+
- name: "docguard-autofix"
|
|
63
|
+
file: "templates/github-workflows/docguard-autofix.yml"
|
|
64
|
+
description: "PR-time auto-fix — applies mechanical doc fixes + comments summary"
|
|
65
|
+
|
|
56
66
|
# Helper scripts for CI/CD and automation
|
|
57
67
|
scripts:
|
|
58
68
|
- name: "docguard-check-docs"
|
|
@@ -6,10 +6,10 @@ description: AI-driven documentation repair with structured research workflow, t
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.
|
|
9
|
+
version: 0.12.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-fix
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.12.0 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Fix Skill
|
|
15
15
|
|
|
@@ -7,10 +7,10 @@ description: Run DocGuard guard validation against Canonical-Driven Development
|
|
|
7
7
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
8
8
|
metadata:
|
|
9
9
|
author: docguard
|
|
10
|
-
version: 0.
|
|
10
|
+
version: 0.12.0
|
|
11
11
|
source: extensions/spec-kit-docguard/skills/docguard-guard
|
|
12
12
|
---
|
|
13
|
-
<!-- docguard:version: 0.
|
|
13
|
+
<!-- docguard:version: 0.12.0 -->
|
|
14
14
|
|
|
15
15
|
# DocGuard Guard Skill
|
|
16
16
|
|
|
@@ -139,7 +139,7 @@ For each finding, provide a **specific, actionable fix** — not "fix the issue"
|
|
|
139
139
|
|
|
140
140
|
Based on the triage results:
|
|
141
141
|
|
|
142
|
-
- **If all PASS**: "All
|
|
142
|
+
- **If all PASS**: "All 21 validators passed. Project is CDD-compliant. Ready to commit."
|
|
143
143
|
- **If only MEDIUM/LOW warnings**: "Non-blocking warnings found. Safe to commit, but consider running `/docguard.fix` for automated remediation."
|
|
144
144
|
- **If HIGH or CRITICAL failures**: "Blocking issues found. Fix these before committing. Suggest running `/docguard.fix --doc [most impactful doc]` next."
|
|
145
145
|
|
|
@@ -6,10 +6,10 @@ description: Cross-document consistency analysis and quality assessment. Perform
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.
|
|
9
|
+
version: 0.12.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-review
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.12.0 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Review Skill
|
|
15
15
|
|
|
@@ -6,10 +6,10 @@ description: CDD maturity assessment with category-aware improvement roadmap. Ru
|
|
|
6
6
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
7
7
|
metadata:
|
|
8
8
|
author: docguard
|
|
9
|
-
version: 0.
|
|
9
|
+
version: 0.12.0
|
|
10
10
|
source: extensions/spec-kit-docguard/skills/docguard-score
|
|
11
11
|
---
|
|
12
|
-
<!-- docguard:version: 0.
|
|
12
|
+
<!-- docguard:version: 0.12.0 -->
|
|
13
13
|
|
|
14
14
|
# DocGuard Score Skill
|
|
15
15
|
|
|
@@ -4,7 +4,7 @@ description: Keep canonical documentation ALWAYS UP TO DATE. Refreshes code-trut
|
|
|
4
4
|
compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
|
|
5
5
|
metadata:
|
|
6
6
|
author: docguard
|
|
7
|
-
version: 0.
|
|
7
|
+
version: 0.12.0
|
|
8
8
|
source: extensions/spec-kit-docguard/skills/docguard-sync
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# DocGuard Auto-Fix — applies mechanical doc fixes on every PR.
|
|
2
|
+
#
|
|
3
|
+
# What this does on each PR:
|
|
4
|
+
# 1. Runs `docguard fix --write` (deterministic fixes: version bumps,
|
|
5
|
+
# counts, endpoint removal from API-REFERENCE, changelog stubs).
|
|
6
|
+
# 2. If anything changed, commits the diff back to the PR branch as
|
|
7
|
+
# `docguard-bot` and posts a summary comment.
|
|
8
|
+
# 3. If nothing changed, posts a "no fixes needed" comment.
|
|
9
|
+
#
|
|
10
|
+
# Prose rewrites (entire-section regenerations) still need an AI agent —
|
|
11
|
+
# run `/docguard.fix` in your editor for those.
|
|
12
|
+
#
|
|
13
|
+
# Setup:
|
|
14
|
+
# 1. Copy this file to .github/workflows/docguard-autofix.yml
|
|
15
|
+
# 2. Ensure the workflow has the permissions block below (write access).
|
|
16
|
+
# 3. (Optional) Pin to a specific DocGuard version by changing `@main` to a tag.
|
|
17
|
+
#
|
|
18
|
+
# Security note: this workflow makes commits back to the PR branch. It refuses
|
|
19
|
+
# to run on PRs from forks (where pushing back is impossible by design).
|
|
20
|
+
name: DocGuard Auto-Fix
|
|
21
|
+
|
|
22
|
+
on:
|
|
23
|
+
pull_request:
|
|
24
|
+
types: [opened, synchronize, reopened]
|
|
25
|
+
|
|
26
|
+
permissions:
|
|
27
|
+
contents: write # commit fixes back to the PR branch
|
|
28
|
+
pull-requests: write # post the summary comment
|
|
29
|
+
|
|
30
|
+
jobs:
|
|
31
|
+
autofix:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
# Skip on fork PRs — the next step would error trying to push.
|
|
34
|
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
35
|
+
steps:
|
|
36
|
+
- name: Checkout PR branch (with write token)
|
|
37
|
+
uses: actions/checkout@v4
|
|
38
|
+
with:
|
|
39
|
+
ref: ${{ github.event.pull_request.head.ref }}
|
|
40
|
+
# Default GITHUB_TOKEN has the contents:write scope granted above.
|
|
41
|
+
# For org-protected branches or commit signing, swap in a PAT/App token.
|
|
42
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
43
|
+
fetch-depth: 0
|
|
44
|
+
|
|
45
|
+
- name: Run DocGuard fix --write + auto-commit + PR comment
|
|
46
|
+
uses: raccioly/docguard@main
|
|
47
|
+
with:
|
|
48
|
+
command: fix
|
|
49
|
+
auto-commit: 'true'
|
|
50
|
+
comment-on-pr: 'true'
|
|
51
|
+
commit-message: 'docs: apply DocGuard mechanical fixes'
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# DocGuard Guard — runs all 20 validators on every PR and main push.
|
|
2
|
+
#
|
|
3
|
+
# This is the canonical CI gate. It does NOT modify your repo — it only
|
|
4
|
+
# reports. Pair with `docguard-autofix.yml` if you want mechanical fixes
|
|
5
|
+
# applied automatically.
|
|
6
|
+
#
|
|
7
|
+
# Setup:
|
|
8
|
+
# 1. Copy this file to .github/workflows/docguard-guard.yml
|
|
9
|
+
# 2. (Optional) Set `fail-on-warning: 'true'` to make warnings block the PR.
|
|
10
|
+
name: DocGuard Guard
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
branches: [main]
|
|
15
|
+
pull_request:
|
|
16
|
+
branches: [main]
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
pull-requests: write # only needed for the optional score comment below
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
guard:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
with:
|
|
28
|
+
fetch-depth: 0
|
|
29
|
+
|
|
30
|
+
- name: Run all validators
|
|
31
|
+
uses: raccioly/docguard@main
|
|
32
|
+
with:
|
|
33
|
+
command: guard
|
|
34
|
+
# Flip to 'true' once your repo is clean — turns warnings into hard failures.
|
|
35
|
+
fail-on-warning: 'false'
|
|
36
|
+
|
|
37
|
+
# Optional: post the CDD score on every PR as a tracked metric.
|
|
38
|
+
score:
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
if: github.event_name == 'pull_request'
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
|
|
44
|
+
- name: Score & comment
|
|
45
|
+
uses: raccioly/docguard@main
|
|
46
|
+
with:
|
|
47
|
+
command: score
|
|
48
|
+
format: json
|
package/package.json
CHANGED