docguard-cli 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PHILOSOPHY.md +59 -106
- package/README.md +23 -1
- package/cli/commands/diagnose.mjs +157 -52
- package/cli/commands/fix.mjs +113 -1
- package/cli/commands/generate.mjs +91 -0
- package/cli/commands/hooks.mjs +40 -2
- package/cli/commands/score.mjs +22 -0
- package/cli/commands/sync.mjs +123 -0
- package/cli/docguard.mjs +22 -0
- package/cli/scanners/cdk.mjs +10 -0
- package/cli/scanners/frontend.mjs +438 -0
- package/cli/scanners/iac.mjs +235 -0
- package/cli/scanners/integrations.mjs +116 -0
- package/cli/scanners/memory-plan.mjs +242 -0
- package/cli/scanners/project-type.mjs +310 -0
- package/cli/scanners/routes.mjs +149 -0
- package/cli/scanners/schemas.mjs +174 -1
- package/cli/shared-ignore.mjs +29 -2
- package/cli/shared-source.mjs +2 -1
- package/cli/validators/api-surface.mjs +112 -37
- package/cli/validators/changelog.mjs +3 -2
- package/cli/validators/docs-coverage.mjs +125 -6
- package/cli/validators/docs-sync.mjs +49 -8
- package/cli/validators/metadata-sync.mjs +6 -1
- package/cli/validators/metrics-consistency.mjs +5 -2
- package/cli/validators/test-spec.mjs +129 -11
- package/cli/validators/todo-tracking.mjs +55 -2
- package/cli/writers/api-reference.mjs +101 -0
- package/cli/writers/mechanical.mjs +116 -0
- package/cli/writers/sections.mjs +148 -0
- package/commands/docguard.fix.md +19 -3
- package/docs/doc-sections.md +37 -0
- package/extensions/spec-kit-docguard/README.md +7 -4
- package/extensions/spec-kit-docguard/commands/fix.md +74 -0
- package/extensions/spec-kit-docguard/commands/generate.md +25 -2
- package/extensions/spec-kit-docguard/commands/sync.md +62 -0
- package/extensions/spec-kit-docguard/extension.yml +1 -1
- package/extensions/spec-kit-docguard/skills/docguard-fix/SKILL.md +13 -3
- 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 +111 -0
- package/package.json +1 -1
- package/templates/ARCHITECTURE.md.template +52 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API-REFERENCE.md Writer — deterministic, structural edits only.
|
|
3
|
+
*
|
|
4
|
+
* Used by `docguard fix --write` to MECHANICALLY remove endpoints that are
|
|
5
|
+
* documented but no longer exist in the actual API surface. This performs NO
|
|
6
|
+
* content rewriting (that needs an LLM) — it only deletes the structural pieces
|
|
7
|
+
* that document a now-absent endpoint:
|
|
8
|
+
* 1. its summary-table row: | `GET` | `/api/...` | ... |
|
|
9
|
+
* 2. its detail block: #### GET `/api/...` … up to the next heading
|
|
10
|
+
*
|
|
11
|
+
* Pure string transform — idempotent, no disk I/O here.
|
|
12
|
+
*
|
|
13
|
+
* Zero NPM dependencies — pure Node.js built-ins only.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { normalizePath, endpointKey } from '../scanners/api-doc.mjs';
|
|
17
|
+
|
|
18
|
+
const HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']);
|
|
19
|
+
const HEADING_RE = /^#{1,6}\s/;
|
|
20
|
+
// An endpoint detail heading: "#### GET `/path`" (backticks optional, any level).
|
|
21
|
+
const ENDPOINT_HEADING_RE = /^#{2,6}\s+`?(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)`?\s+`?(\/[^\s`|]+)`?/i;
|
|
22
|
+
|
|
23
|
+
/** True if the doc is DocGuard-generated (safe for `--write` to edit). */
|
|
24
|
+
export function hasGeneratedMarker(content) {
|
|
25
|
+
return /<!--\s*docguard:generated\s+true\s*-->/i.test(content || '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* If a markdown line is an API summary-table row, return its endpoint key.
|
|
30
|
+
* Row shape: | `GET` | `/api/...` | ... |
|
|
31
|
+
*/
|
|
32
|
+
function tableRowKey(line) {
|
|
33
|
+
if (!line.includes('|')) return null;
|
|
34
|
+
const cells = line.split('|').map(s => s.trim()).filter(s => s.length > 0);
|
|
35
|
+
if (cells.length < 2) return null;
|
|
36
|
+
const method = cells[0].replace(/`/g, '').trim().toUpperCase();
|
|
37
|
+
if (!HTTP_METHODS.has(method)) return null;
|
|
38
|
+
for (let i = 1; i < cells.length; i++) {
|
|
39
|
+
const cand = cells[i].replace(/`/g, '').trim();
|
|
40
|
+
if (cand.startsWith('/')) return endpointKey(method, cand);
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** If a line is an endpoint detail heading, return its endpoint key. */
|
|
46
|
+
function headingKey(line) {
|
|
47
|
+
const m = line.match(ENDPOINT_HEADING_RE);
|
|
48
|
+
if (!m) return null;
|
|
49
|
+
return endpointKey(m[1], m[2]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Remove the table row(s) and detail block(s) for the given endpoints.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} content - API-REFERENCE.md content
|
|
56
|
+
* @param {Array<{method:string,path:string}>} endpoints - endpoints to remove
|
|
57
|
+
* @returns {{ content: string, removed: string[] }} new content + removed keys
|
|
58
|
+
*/
|
|
59
|
+
export function removeEndpoints(content, endpoints) {
|
|
60
|
+
const targets = new Set((endpoints || []).map(e => endpointKey(e.method, e.path)));
|
|
61
|
+
if (targets.size === 0) return { content, removed: [] };
|
|
62
|
+
|
|
63
|
+
const lines = content.split('\n');
|
|
64
|
+
const out = [];
|
|
65
|
+
const removed = new Set();
|
|
66
|
+
let skippingBlock = false;
|
|
67
|
+
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
const isHeading = HEADING_RE.test(line);
|
|
70
|
+
|
|
71
|
+
if (isHeading) {
|
|
72
|
+
// Any heading terminates a block we were skipping.
|
|
73
|
+
const hk = headingKey(line);
|
|
74
|
+
if (hk && targets.has(hk)) {
|
|
75
|
+
// Start (or continue into a new) skipped detail block.
|
|
76
|
+
skippingBlock = true;
|
|
77
|
+
removed.add(hk);
|
|
78
|
+
continue; // drop the heading line itself
|
|
79
|
+
}
|
|
80
|
+
// A non-target heading ends skipping and is kept.
|
|
81
|
+
skippingBlock = false;
|
|
82
|
+
out.push(line);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (skippingBlock) continue; // inside a removed detail block
|
|
87
|
+
|
|
88
|
+
// Not skipping: drop a matching summary-table row.
|
|
89
|
+
const rk = tableRowKey(line);
|
|
90
|
+
if (rk && targets.has(rk)) {
|
|
91
|
+
removed.add(rk);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
out.push(line);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { content: out.join('\n'), removed: [...removed] };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { normalizePath };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mechanical Fix Registry — applies deterministic, no-LLM fixes in place.
|
|
3
|
+
*
|
|
4
|
+
* Validators surface structured `fixes[]` actions; this module knows how to
|
|
5
|
+
* apply each TYPE safely and idempotently. These are surgical token/structure
|
|
6
|
+
* edits the validator already located precisely — never prose rewrites.
|
|
7
|
+
*
|
|
8
|
+
* Fix types:
|
|
9
|
+
* - replace-count : stale "N validators/checks" → actual count (Metrics-Consistency)
|
|
10
|
+
* - replace-version : stale version ref → current version (Metadata-Sync)
|
|
11
|
+
* - insert-changelog-unreleased : add a `## [Unreleased]` header (Changelog)
|
|
12
|
+
* - remove-endpoint : delete a documented-but-absent endpoint (API-Surface; delegated)
|
|
13
|
+
*
|
|
14
|
+
* Pure file edits, no LLM. Zero NPM dependencies.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { resolve } from 'node:path';
|
|
19
|
+
import { removeEndpoints, hasGeneratedMarker } from './api-reference.mjs';
|
|
20
|
+
|
|
21
|
+
const esc = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
22
|
+
|
|
23
|
+
/** replace-count: "<found> <label>" → "<actual> <label>" in the file. */
|
|
24
|
+
function applyReplaceCount(projectDir, fix) {
|
|
25
|
+
const full = resolve(projectDir, fix.file);
|
|
26
|
+
if (!existsSync(full)) return { applied: false };
|
|
27
|
+
const content = readFileSync(full, 'utf-8');
|
|
28
|
+
const re = new RegExp(`\\b${esc(fix.found)}(\\s+(?:automated\\s+)?${esc(fix.label)}\\b)`, 'g');
|
|
29
|
+
const next = content.replace(re, `${fix.actual}$1`);
|
|
30
|
+
if (next === content) return { applied: false };
|
|
31
|
+
writeFileSync(full, next, 'utf-8');
|
|
32
|
+
return { applied: true, detail: `${fix.file}: "${fix.found} ${fix.label}" → "${fix.actual} ${fix.label}"` };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** replace-version: stale version → current, ONLY in actionable contexts. */
|
|
36
|
+
function applyReplaceVersion(projectDir, fix) {
|
|
37
|
+
const full = resolve(projectDir, fix.file);
|
|
38
|
+
if (!existsSync(full)) return { applied: false };
|
|
39
|
+
const content = readFileSync(full, 'utf-8');
|
|
40
|
+
const f = esc(fix.found);
|
|
41
|
+
// Mirror metadata-sync's actionable detection so we never touch prose.
|
|
42
|
+
const patterns = [
|
|
43
|
+
new RegExp(`((?:archive|tags|releases|download)\\/v?)${f}`, 'g'),
|
|
44
|
+
new RegExp(`(@)${f}`, 'g'),
|
|
45
|
+
new RegExp(`(version:\\s*["']?)${f}`, 'g'),
|
|
46
|
+
];
|
|
47
|
+
let next = content;
|
|
48
|
+
for (const re of patterns) next = next.replace(re, `$1${fix.actual}`);
|
|
49
|
+
if (next === content) return { applied: false };
|
|
50
|
+
writeFileSync(full, next, 'utf-8');
|
|
51
|
+
return { applied: true, detail: `${fix.file}: v${fix.found} → v${fix.actual}` };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** insert-changelog-unreleased: add `## [Unreleased]` after the title/intro. */
|
|
55
|
+
function applyInsertChangelogUnreleased(projectDir, fix) {
|
|
56
|
+
const full = resolve(projectDir, fix.file);
|
|
57
|
+
if (!existsSync(full)) return { applied: false };
|
|
58
|
+
const content = readFileSync(full, 'utf-8');
|
|
59
|
+
if (/\[unreleased\]/i.test(content)) return { applied: false }; // idempotent
|
|
60
|
+
const lines = content.split('\n');
|
|
61
|
+
// Insert before the first version heading `## [x.y.z]`, else after the H1, else top.
|
|
62
|
+
let idx = lines.findIndex(l => /^##\s*\[\d/.test(l));
|
|
63
|
+
if (idx < 0) {
|
|
64
|
+
const h1 = lines.findIndex(l => /^#\s/.test(l));
|
|
65
|
+
idx = h1 >= 0 ? h1 + 1 : 0;
|
|
66
|
+
}
|
|
67
|
+
const block = idx > 0 && lines[idx - 1].trim() !== '' ? ['', '## [Unreleased]', ''] : ['## [Unreleased]', ''];
|
|
68
|
+
lines.splice(idx, 0, ...block);
|
|
69
|
+
writeFileSync(full, lines.join('\n'), 'utf-8');
|
|
70
|
+
return { applied: true, detail: `${fix.file}: added ## [Unreleased]` };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** remove-endpoint: delegate to the API-REFERENCE writer (marker-gated). */
|
|
74
|
+
function applyRemoveEndpoint(projectDir, fix, { force = false } = {}) {
|
|
75
|
+
const full = resolve(projectDir, fix.doc || 'docs-canonical/API-REFERENCE.md');
|
|
76
|
+
if (!existsSync(full)) return { applied: false };
|
|
77
|
+
const content = readFileSync(full, 'utf-8');
|
|
78
|
+
if (!hasGeneratedMarker(content) && !force) {
|
|
79
|
+
return { applied: false, skipped: `${fix.doc} not docguard:generated (use --force)` };
|
|
80
|
+
}
|
|
81
|
+
const { content: next, removed } = removeEndpoints(content, [{ method: fix.method, path: fix.path }]);
|
|
82
|
+
if (removed.length === 0 || next === content) return { applied: false };
|
|
83
|
+
writeFileSync(full, next, 'utf-8');
|
|
84
|
+
return { applied: true, detail: `${fix.doc}: removed ${fix.method} ${fix.path}` };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const APPLIERS = {
|
|
88
|
+
'replace-count': applyReplaceCount,
|
|
89
|
+
'replace-version': applyReplaceVersion,
|
|
90
|
+
'insert-changelog-unreleased': applyInsertChangelogUnreleased,
|
|
91
|
+
'remove-endpoint': applyRemoveEndpoint,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const MECHANICAL_FIX_TYPES = Object.keys(APPLIERS);
|
|
95
|
+
|
|
96
|
+
/** Apply a single structured fix. Returns { applied, detail?, skipped? }. */
|
|
97
|
+
export function applyMechanicalFix(projectDir, fix, opts = {}) {
|
|
98
|
+
const fn = APPLIERS[fix.type];
|
|
99
|
+
if (!fn) return { applied: false, skipped: `unknown fix type: ${fix.type}` };
|
|
100
|
+
return fn(projectDir, fix, opts);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Apply a batch of fixes; returns a summary.
|
|
105
|
+
* @returns {{ applied: object[], skipped: object[] }}
|
|
106
|
+
*/
|
|
107
|
+
export function applyMechanicalFixes(projectDir, fixes, opts = {}) {
|
|
108
|
+
const applied = [];
|
|
109
|
+
const skipped = [];
|
|
110
|
+
for (const fix of fixes) {
|
|
111
|
+
const r = applyMechanicalFix(projectDir, fix, opts);
|
|
112
|
+
if (r.applied) applied.push({ ...fix, detail: r.detail });
|
|
113
|
+
else if (r.skipped) skipped.push({ ...fix, reason: r.skipped });
|
|
114
|
+
}
|
|
115
|
+
return { applied, skipped };
|
|
116
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section-addressable docs — the foundation for surgical, non-destructive doc
|
|
3
|
+
* maintenance.
|
|
4
|
+
*
|
|
5
|
+
* Canonical docs mix two kinds of content:
|
|
6
|
+
* - CODE-DERIVED sections (endpoint tables, entity lists, env-var tables) that
|
|
7
|
+
* DocGuard can regenerate from the codebase, and
|
|
8
|
+
* - HUMAN prose (rationale, "why", design intent) that must NEVER be clobbered.
|
|
9
|
+
*
|
|
10
|
+
* We mark the regenerable regions with HTML comments so the doc stays plain,
|
|
11
|
+
* readable markdown:
|
|
12
|
+
*
|
|
13
|
+
* <!-- docguard:section id=api-endpoints source=code -->
|
|
14
|
+
* | `GET` | `/api/x` | … |
|
|
15
|
+
* <!-- /docguard:section -->
|
|
16
|
+
*
|
|
17
|
+
* DocGuard rewrites ONLY the bytes between a section's open/close markers.
|
|
18
|
+
* Everything outside any marker (the human writing) is preserved exactly.
|
|
19
|
+
*
|
|
20
|
+
* Pure string transforms — idempotent, no disk I/O. Zero NPM dependencies.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const OPEN_RE = /^[ \t]*<!--\s*docguard:section\b([^>]*?)-->[ \t]*$/;
|
|
24
|
+
const CLOSE_RE = /^[ \t]*<!--\s*\/docguard:section\s*-->[ \t]*$/;
|
|
25
|
+
|
|
26
|
+
/** Parse `id=foo source=code` style attributes from an open-marker tail. */
|
|
27
|
+
function parseAttrs(attrStr) {
|
|
28
|
+
const attrs = {};
|
|
29
|
+
const re = /(\w[\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"']+))/g;
|
|
30
|
+
let m;
|
|
31
|
+
while ((m = re.exec(attrStr)) !== null) {
|
|
32
|
+
attrs[m[1]] = m[2] ?? m[3] ?? m[4] ?? '';
|
|
33
|
+
}
|
|
34
|
+
return attrs;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse all well-formed sections in a document.
|
|
39
|
+
* A section is a line matching the open marker, then content lines, then a
|
|
40
|
+
* close-marker line. An open with no matching close is ignored (not corrupted).
|
|
41
|
+
* @returns {Array<{ id, source, attrs, openLine, closeLine, body }>}
|
|
42
|
+
*/
|
|
43
|
+
export function parseSections(content) {
|
|
44
|
+
const lines = String(content).split('\n');
|
|
45
|
+
const sections = [];
|
|
46
|
+
let open = null;
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
const line = lines[i];
|
|
50
|
+
if (open === null) {
|
|
51
|
+
const om = line.match(OPEN_RE);
|
|
52
|
+
if (om) {
|
|
53
|
+
const attrs = parseAttrs(om[1] || '');
|
|
54
|
+
open = { attrs, openLine: i };
|
|
55
|
+
}
|
|
56
|
+
} else if (CLOSE_RE.test(line)) {
|
|
57
|
+
sections.push({
|
|
58
|
+
id: open.attrs.id || '',
|
|
59
|
+
source: open.attrs.source || 'code',
|
|
60
|
+
attrs: open.attrs,
|
|
61
|
+
openLine: open.openLine,
|
|
62
|
+
closeLine: i,
|
|
63
|
+
body: lines.slice(open.openLine + 1, i).join('\n'),
|
|
64
|
+
});
|
|
65
|
+
open = null;
|
|
66
|
+
}
|
|
67
|
+
// Note: a second open before a close just extends the search for a close;
|
|
68
|
+
// we keep the FIRST open's start, so malformed nesting can't corrupt content.
|
|
69
|
+
}
|
|
70
|
+
return sections;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Get a single section by id, or null. */
|
|
74
|
+
export function getSection(content, id) {
|
|
75
|
+
return parseSections(content).find(s => s.id === id) || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** List section ids present in a document. */
|
|
79
|
+
export function listSections(content) {
|
|
80
|
+
return parseSections(content).map(s => s.id);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Render a full marked section block (open marker + body + close marker). */
|
|
84
|
+
export function renderSection(id, body, { source = 'code' } = {}) {
|
|
85
|
+
const inner = String(body).replace(/^\n+/, '').replace(/\n+$/, '');
|
|
86
|
+
return `<!-- docguard:section id=${id} source=${source} -->\n${inner}\n<!-- /docguard:section -->`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Replace ONLY the body of an existing section, preserving its markers and all
|
|
91
|
+
* surrounding content. Idempotent: if the new body matches, returns unchanged.
|
|
92
|
+
* @returns {{ content: string, replaced: boolean }}
|
|
93
|
+
*/
|
|
94
|
+
export function replaceSection(content, id, newBody) {
|
|
95
|
+
const lines = String(content).split('\n');
|
|
96
|
+
const sections = parseSections(content);
|
|
97
|
+
const sec = sections.find(s => s.id === id);
|
|
98
|
+
if (!sec) return { content, replaced: false };
|
|
99
|
+
|
|
100
|
+
const inner = String(newBody).replace(/^\n+/, '').replace(/\n+$/, '');
|
|
101
|
+
if (sec.body === inner) return { content, replaced: false }; // idempotent no-op
|
|
102
|
+
|
|
103
|
+
const before = lines.slice(0, sec.openLine + 1);
|
|
104
|
+
const after = lines.slice(sec.closeLine);
|
|
105
|
+
const next = [...before, ...inner.split('\n'), ...after].join('\n');
|
|
106
|
+
return { content: next, replaced: true };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Replace a section's body if it exists; otherwise INSERT a new section.
|
|
111
|
+
* Insert position: `after:<id>` (after another section), 'top' (after the H1 /
|
|
112
|
+
* first heading), or 'end' (default, appended).
|
|
113
|
+
* @returns {{ content: string, action: 'replaced'|'inserted'|'unchanged' }}
|
|
114
|
+
*/
|
|
115
|
+
export function upsertSection(content, id, newBody, { source = 'code', position = 'end' } = {}) {
|
|
116
|
+
if (getSection(content, id)) {
|
|
117
|
+
const r = replaceSection(content, id, newBody);
|
|
118
|
+
return { content: r.content, action: r.replaced ? 'replaced' : 'unchanged' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const block = renderSection(id, newBody, { source });
|
|
122
|
+
const lines = String(content).split('\n');
|
|
123
|
+
|
|
124
|
+
// Insert after a named section.
|
|
125
|
+
const afterMatch = /^after:(.+)$/.exec(position);
|
|
126
|
+
if (afterMatch) {
|
|
127
|
+
const target = parseSections(content).find(s => s.id === afterMatch[1].trim());
|
|
128
|
+
if (target) {
|
|
129
|
+
const before = lines.slice(0, target.closeLine + 1);
|
|
130
|
+
const after = lines.slice(target.closeLine + 1);
|
|
131
|
+
return { content: [...before, '', block, ...after].join('\n'), action: 'inserted' };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Insert just after the first heading (keeps the doc title on top).
|
|
136
|
+
if (position === 'top') {
|
|
137
|
+
const hIdx = lines.findIndex(l => /^#{1,6}\s/.test(l));
|
|
138
|
+
if (hIdx >= 0) {
|
|
139
|
+
const before = lines.slice(0, hIdx + 1);
|
|
140
|
+
const after = lines.slice(hIdx + 1);
|
|
141
|
+
return { content: [...before, '', block, ...after].join('\n'), action: 'inserted' };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Default: append at end (single trailing newline).
|
|
146
|
+
const base = String(content).replace(/\n+$/, '');
|
|
147
|
+
return { content: `${base}\n\n${block}\n`, action: 'inserted' };
|
|
148
|
+
}
|
package/commands/docguard.fix.md
CHANGED
|
@@ -11,11 +11,27 @@ handoffs:
|
|
|
11
11
|
|
|
12
12
|
# DocGuard Fix — AI-Assisted Documentation Repair
|
|
13
13
|
|
|
14
|
-
Generate or repair canonical documentation
|
|
14
|
+
Generate or repair canonical documentation. DocGuard splits fixes into two kinds:
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
- **Mechanical (deterministic, no AI):** structural edits DocGuard applies itself with
|
|
17
|
+
`docguard fix --write` — e.g. removing an endpoint from `docs-canonical/API-REFERENCE.md`
|
|
18
|
+
that the OpenAPI spec confirms no longer exists (its table row + detail block are deleted).
|
|
19
|
+
- **Agent (needs an AI):** content rewrites that require judgment — e.g. replacing an
|
|
20
|
+
X-Ray prose section with CloudWatch, or writing a new endpoint's request/response block.
|
|
21
|
+
These use the research-prompt workflow below.
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
## Apply mechanical fixes first (fast, safe)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx docguard-cli fix --write # removes stale documented endpoints; idempotent
|
|
27
|
+
```
|
|
28
|
+
- Only edits docs marked `<!-- docguard:generated true -->` (use `--force` to override).
|
|
29
|
+
- Prints exactly what it removed. Re-run is a no-op if nothing changed.
|
|
30
|
+
- Run `docguard guard` afterward; whatever remains is agent work (below).
|
|
31
|
+
|
|
32
|
+
## What to do (agent work)
|
|
33
|
+
|
|
34
|
+
1. **Identify what needs fixing** (each issue is tagged `mechanical` or `agent`):
|
|
19
35
|
```bash
|
|
20
36
|
npx docguard-cli diagnose
|
|
21
37
|
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Section-Addressable Docs
|
|
2
|
+
|
|
3
|
+
DocGuard maintains canonical docs *surgically*: it regenerates the parts that are
|
|
4
|
+
derived from code while never touching the prose a human wrote. It does this with
|
|
5
|
+
HTML-comment markers that keep the document plain, readable markdown.
|
|
6
|
+
|
|
7
|
+
## The marker format
|
|
8
|
+
|
|
9
|
+
```markdown
|
|
10
|
+
<!-- docguard:section id=api-endpoints source=code -->
|
|
11
|
+
| `GET` | `/api/users` | … |
|
|
12
|
+
<!-- /docguard:section -->
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- **`id`** — a stable identifier for the section (e.g. `api-endpoints`, `entities`,
|
|
16
|
+
`env-vars`, `screens`). DocGuard addresses sections by id.
|
|
17
|
+
- **`source`** — `code` means DocGuard owns and may regenerate this block from the
|
|
18
|
+
codebase; `human` means it is author-owned and DocGuard will not rewrite it.
|
|
19
|
+
|
|
20
|
+
Markers must each sit on their own line. An open marker with no matching close is
|
|
21
|
+
ignored (DocGuard never corrupts a malformed doc).
|
|
22
|
+
|
|
23
|
+
## What DocGuard does and does not touch
|
|
24
|
+
|
|
25
|
+
- It rewrites **only** the bytes between a `source=code` section's open and close
|
|
26
|
+
markers when the underlying code changes.
|
|
27
|
+
- **Everything outside any marker — and any `source=human` section — is preserved
|
|
28
|
+
exactly.** Your rationale, "why" notes, and design intent are safe.
|
|
29
|
+
|
|
30
|
+
## Why this matters
|
|
31
|
+
|
|
32
|
+
This is the foundation for two things:
|
|
33
|
+
|
|
34
|
+
1. **Complete generation** — `docguard generate` writes code-derived sections inside
|
|
35
|
+
markers, then an AI agent fills the prose around them.
|
|
36
|
+
2. **Always up to date** — `docguard sync` refreshes just the affected section when
|
|
37
|
+
code changes, instead of regenerating (and clobbering) the whole document.
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# DocGuard — CDD Enforcement Extension for Spec Kit
|
|
2
2
|
|
|
3
|
-
Enterprise-grade Canonical-Driven Development (CDD) enforcement for [Spec Kit](https://github.com/github/spec-kit).
|
|
3
|
+
Enterprise-grade Canonical-Driven Development (CDD) enforcement and **AI-readable project memory** for [Spec Kit](https://github.com/github/spec-kit). DocGuard builds a complete, language-aware documentation memory of any codebase (`generate --plan`), keeps it always up to date as code changes (`sync`), and verifies it (`guard`) — with deterministic mechanical fixes (`fix --write`) where it can and grounded agent prompts where prose is needed.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **20 Validators** — Structure, Security, Doc Quality, Test-Spec, Drift-Comments, API-Surface, Freshness, and 13 more
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
8
|
+
- **Language-agnostic** — JS/TS, Python, Rust, Go, Java/Kotlin, Ruby, PHP, C#. Polyglot/monorepo-aware.
|
|
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
|
+
- **Always up to date** — `sync` surgically refreshes code-truth doc sections in place, **preserves human prose**, flags prose for agent review.
|
|
11
|
+
- **Mechanical `fix --write`** — deterministic, no-LLM: remove stale documented endpoints, refresh stale "N validators" counts, replace stale version refs, insert missing `## [Unreleased]`.
|
|
12
|
+
- **5 AI Skills** — docguard-fix, docguard-guard, docguard-sync, docguard-review, docguard-score (enterprise-grade behavior protocols, not just step-lists)
|
|
13
|
+
- **Workflow Chaining** — YAML handoffs enable guard → sync → fix → review → score flows
|
|
11
14
|
- **Spec Kit Hooks** — Quality gate integrations at implement, tasks, and review phases
|
|
12
15
|
- **Zero Dependencies** — Pure Node.js built-ins only
|
|
13
16
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Fix documentation drift — mechanical (no AI) or AI-driven research, depending on the issue
|
|
3
|
+
allowed-tools: Bash, Read, Edit
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DocGuard Fix
|
|
7
|
+
|
|
8
|
+
DocGuard splits drift into two kinds and is explicit about which is which.
|
|
9
|
+
|
|
10
|
+
- **Mechanical** (deterministic, no AI): apply with `docguard fix --write`. Covers
|
|
11
|
+
removing endpoints documented but absent in code, refreshing stale "N validators"
|
|
12
|
+
counts, replacing stale version references, inserting a missing `## [Unreleased]`.
|
|
13
|
+
- **Agent** (needs judgment): content rewrites — e.g. updating an X-Ray section to
|
|
14
|
+
CloudWatch, writing a new endpoint's request/response block.
|
|
15
|
+
|
|
16
|
+
## User Input
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
$ARGUMENTS
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
You **MUST** consider the user input before proceeding (if not empty).
|
|
23
|
+
|
|
24
|
+
## Execution
|
|
25
|
+
|
|
26
|
+
### Step 1 — Apply mechanical fixes (fast, safe, no AI)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx --yes docguard-cli@latest fix --write
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Output lists every applied fix. Idempotent: re-running is a no-op if nothing changed.
|
|
33
|
+
Only edits `<!-- docguard:generated true -->` docs unless `--force`.
|
|
34
|
+
|
|
35
|
+
### Step 2 — Identify remaining issues by kind
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx --yes docguard-cli@latest diagnose --format json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Each issue is tagged `fixKind: mechanical` (mostly handled by step 1) or
|
|
42
|
+
`fixKind: agent`. Focus on the agent ones.
|
|
43
|
+
|
|
44
|
+
### Step 3 — Use the deep doc prompt for content rewrites
|
|
45
|
+
|
|
46
|
+
For each affected canonical doc, get a research-grounded prompt:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx --yes docguard-cli@latest fix --doc architecture
|
|
50
|
+
npx --yes docguard-cli@latest fix --doc data-model
|
|
51
|
+
npx --yes docguard-cli@latest fix --doc api-reference
|
|
52
|
+
npx --yes docguard-cli@latest fix --doc security
|
|
53
|
+
npx --yes docguard-cli@latest fix --doc test-spec
|
|
54
|
+
npx --yes docguard-cli@latest fix --doc environment
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Execute the research steps in each prompt: read actual code files, map modules,
|
|
58
|
+
trace routes, extract real schemas, identify real auth patterns. Then write the
|
|
59
|
+
sections — using real file paths, real module names, real dependencies. No placeholders.
|
|
60
|
+
|
|
61
|
+
### Step 4 — Verify
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx --yes docguard-cli@latest guard
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Iterate until clean (max 3 rounds; if still failing, report remaining issues).
|
|
68
|
+
|
|
69
|
+
## Flags
|
|
70
|
+
|
|
71
|
+
- `--write` — apply deterministic fixes in place (step 1).
|
|
72
|
+
- `--doc <name>` — emit a research-grounded prompt for one specific document (step 3).
|
|
73
|
+
- `--force` — for `--write`, edit docs that lack the generated marker.
|
|
74
|
+
- `--format json` — machine-readable issue list (with `fixKind`).
|
|
@@ -11,7 +11,12 @@ handoffs:
|
|
|
11
11
|
|
|
12
12
|
# DocGuard Generate
|
|
13
13
|
|
|
14
|
-
Scans your codebase and generates canonical documentation: ARCHITECTURE.md, DATA-MODEL.md, TEST-SPEC.md, SECURITY.md, ENVIRONMENT.md,
|
|
14
|
+
Scans your codebase (JS/TS, Python, Rust, Go, Java/Kotlin, Ruby, PHP, C# — polyglot/monorepo-aware) and generates the canonical documentation memory: ARCHITECTURE.md, DATA-MODEL.md, TEST-SPEC.md, SECURITY.md, ENVIRONMENT.md, API-REFERENCE.md, SCREENS.md.
|
|
15
|
+
|
|
16
|
+
Two modes:
|
|
17
|
+
|
|
18
|
+
- **`--plan`** (AI-powered, recommended) — emits a structured agent task manifest + writes the code-truth skeleton inside `<!-- docguard:section -->` markers. The AI agent then writes the prose grounded in scanned facts. Human prose is preserved.
|
|
19
|
+
- **default** — purely deterministic generation: writes templated docs with TODO placeholders. Use when no AI agent is available.
|
|
15
20
|
|
|
16
21
|
## User Input
|
|
17
22
|
|
|
@@ -19,7 +24,25 @@ $ARGUMENTS
|
|
|
19
24
|
|
|
20
25
|
## Steps
|
|
21
26
|
|
|
22
|
-
1.
|
|
27
|
+
1. **Preview the plan** — what code-truth facts were captured + what the agent will write:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx --yes docguard-cli@latest generate --plan $ARGUMENTS
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
2. **Scaffold the skeleton docs** (marked sections filled with code-truth, prose sections as agent-task placeholders):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx --yes docguard-cli@latest generate --plan --write $ARGUMENTS
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
3. **Or get the machine-readable manifest** to drive an agent:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx --yes docguard-cli@latest generate --plan --format json $ARGUMENTS
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
4. **Fallback** (no AI, deterministic generation):
|
|
23
46
|
|
|
24
47
|
```bash
|
|
25
48
|
npx --yes docguard-cli@latest generate $ARGUMENTS
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Keep canonical docs always up to date — refresh code-truth sections in place, preserve human prose
|
|
3
|
+
allowed-tools: Bash, Read, Edit
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DocGuard Sync
|
|
7
|
+
|
|
8
|
+
Keep the documentation memory ALWAYS UP TO DATE. Sync re-derives every code-truth
|
|
9
|
+
section (endpoints, entities, screens, tech stack, env vars) and refreshes the
|
|
10
|
+
matching `source=code` sections of existing canonical docs in place. **Human prose
|
|
11
|
+
is never touched** — it lives outside markers or in `source=human` sections.
|
|
12
|
+
|
|
13
|
+
When a code section changes, the prose sections in that doc are flagged for agent review.
|
|
14
|
+
|
|
15
|
+
## User Input
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
$ARGUMENTS
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
You **MUST** consider the user input before proceeding (if not empty).
|
|
22
|
+
|
|
23
|
+
## Execution
|
|
24
|
+
|
|
25
|
+
1. Preview what will change (dry run, never writes):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx --yes docguard-cli@latest sync $ARGUMENTS
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
2. If only `source=code` sections are stale, apply the mechanical refresh:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx --yes docguard-cli@latest sync --write $ARGUMENTS
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
3. For each "prose to review" line, **read the affected doc and update the
|
|
38
|
+
surrounding human-written section to match the new code reality** (e.g. if the
|
|
39
|
+
endpoints table grew, update the API overview prose). Then re-run guard:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx --yes docguard-cli@latest guard
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Flags
|
|
46
|
+
|
|
47
|
+
- `--since <ref>` — also report which code files changed since this git ref (context for prose updates).
|
|
48
|
+
- `--write` — apply the mechanical refreshes. Default is a dry-run preview.
|
|
49
|
+
- `--force` — sync docs even without the `<!-- docguard:generated true -->` marker.
|
|
50
|
+
- `--format json` — machine-readable output (`updates`, `reviews`, `skipped`).
|
|
51
|
+
|
|
52
|
+
## When to use
|
|
53
|
+
|
|
54
|
+
- **Before opening a PR:** `docguard sync --since main` to refresh any stale sections.
|
|
55
|
+
- **On a pre-commit hook:** auto-keep the memory current.
|
|
56
|
+
- **After a big refactor:** confirm the doc map still matches the territory.
|
|
57
|
+
|
|
58
|
+
## Triage glyphs
|
|
59
|
+
|
|
60
|
+
- `↻` mechanically refreshed (code section updated)
|
|
61
|
+
- `•` stale (will refresh with `--write`)
|
|
62
|
+
- `🤖` prose to review — open the doc, update the relevant human-written section
|
|
@@ -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.11.1"
|
|
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"
|