docguard-cli 0.8.2 → 0.9.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/README.md +13 -6
- package/cli/commands/guard.mjs +8 -0
- package/cli/commands/llms.mjs +159 -0
- package/cli/commands/score.mjs +259 -11
- package/cli/docguard.mjs +6 -0
- package/cli/scanners/speckit.mjs +234 -0
- package/cli/shared.mjs +35 -0
- package/cli/validators/doc-quality.mjs +629 -0
- package/cli/validators/docs-sync.mjs +53 -0
- package/cli/validators/schema-sync.mjs +219 -0
- package/cli/validators/test-spec.mjs +51 -4
- package/cli/validators/todo-tracking.mjs +295 -0
- package/cli/validators/traceability.mjs +194 -8
- package/package.json +1 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec Kit Scanner — Detect and integrate with GitHub Spec Kit artifacts
|
|
3
|
+
*
|
|
4
|
+
* Auto-detects .specify/ directory (Spec Kit projects) and maps Spec Kit
|
|
5
|
+
* artifacts to CDD canonical docs. Enables DocGuard to work seamlessly
|
|
6
|
+
* with Spec Kit-managed projects.
|
|
7
|
+
*
|
|
8
|
+
* Spec Kit artifact mapping:
|
|
9
|
+
* .specify/ → Project uses Spec Kit
|
|
10
|
+
* specs/[name]/spec.md → Requirements (maps to REQUIREMENTS.md / docs-canonical/)
|
|
11
|
+
* specs/[name]/plan.md → Design decisions (maps to ARCHITECTURE.md)
|
|
12
|
+
* specs/[name]/tasks.md → Work items (maps to ROADMAP.md)
|
|
13
|
+
* constitution.md → Project rules (maps to AGENTS.md)
|
|
14
|
+
* memory/ → Learned context (maps to DRIFT-LOG.md)
|
|
15
|
+
*
|
|
16
|
+
* Credit: Integration inspired by GitHub's Spec Kit framework
|
|
17
|
+
* (github.com/github/spec-kit)
|
|
18
|
+
*
|
|
19
|
+
* Zero dependencies — pure Node.js built-ins only.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync, statSync } from 'node:fs';
|
|
23
|
+
import { resolve, join } from 'node:path';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Detect if a project uses Spec Kit.
|
|
27
|
+
* Returns details about detected Spec Kit artifacts.
|
|
28
|
+
*/
|
|
29
|
+
export function detectSpecKit(projectDir) {
|
|
30
|
+
const result = {
|
|
31
|
+
detected: false,
|
|
32
|
+
specifyDir: false,
|
|
33
|
+
specs: [],
|
|
34
|
+
constitution: false,
|
|
35
|
+
memory: false,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Check for .specify/ directory
|
|
39
|
+
const specifyDir = resolve(projectDir, '.specify');
|
|
40
|
+
if (existsSync(specifyDir)) {
|
|
41
|
+
result.detected = true;
|
|
42
|
+
result.specifyDir = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check for specs/ directory with spec.md files
|
|
46
|
+
const specsDir = resolve(projectDir, 'specs');
|
|
47
|
+
if (existsSync(specsDir)) {
|
|
48
|
+
try {
|
|
49
|
+
const features = readdirSync(specsDir);
|
|
50
|
+
for (const feature of features) {
|
|
51
|
+
const featureDir = join(specsDir, feature);
|
|
52
|
+
try {
|
|
53
|
+
const fstat = statSync(featureDir);
|
|
54
|
+
if (!fstat.isDirectory()) continue;
|
|
55
|
+
} catch { continue; }
|
|
56
|
+
|
|
57
|
+
const specFile = join(featureDir, 'spec.md');
|
|
58
|
+
const planFile = join(featureDir, 'plan.md');
|
|
59
|
+
const tasksFile = join(featureDir, 'tasks.md');
|
|
60
|
+
|
|
61
|
+
if (existsSync(specFile) || existsSync(planFile) || existsSync(tasksFile)) {
|
|
62
|
+
result.detected = true;
|
|
63
|
+
result.specs.push({
|
|
64
|
+
name: feature,
|
|
65
|
+
hasSpec: existsSync(specFile),
|
|
66
|
+
hasPlan: existsSync(planFile),
|
|
67
|
+
hasTasks: existsSync(tasksFile),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch { /* ignore */ }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check for constitution.md
|
|
75
|
+
const constitutionPath = resolve(projectDir, 'constitution.md');
|
|
76
|
+
if (existsSync(constitutionPath)) {
|
|
77
|
+
result.detected = true;
|
|
78
|
+
result.constitution = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check for memory/ directory
|
|
82
|
+
const memoryDir = resolve(projectDir, 'memory');
|
|
83
|
+
if (existsSync(memoryDir)) {
|
|
84
|
+
result.detected = true;
|
|
85
|
+
result.memory = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Map Spec Kit artifact to CDD equivalent.
|
|
93
|
+
* Returns the canonical doc name and section.
|
|
94
|
+
*/
|
|
95
|
+
const SPECKIT_CDD_MAP = {
|
|
96
|
+
'spec.md': { cddDoc: 'REQUIREMENTS.md', section: 'Requirements', type: 'requirement' },
|
|
97
|
+
'plan.md': { cddDoc: 'ARCHITECTURE.md', section: 'Design Decisions', type: 'design' },
|
|
98
|
+
'tasks.md': { cddDoc: 'ROADMAP.md', section: 'Task Backlog', type: 'roadmap' },
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate CDD canonical docs from Spec Kit artifacts.
|
|
103
|
+
* Used by `docguard generate --from-speckit`.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} projectDir - Project root
|
|
106
|
+
* @param {object} config - DocGuard config
|
|
107
|
+
* @param {object} flags - CLI flags
|
|
108
|
+
* @returns {object} - { generated: string[], skipped: string[], errors: string[] }
|
|
109
|
+
*/
|
|
110
|
+
export function generateFromSpecKit(projectDir, config, flags) {
|
|
111
|
+
const results = { generated: [], skipped: [], errors: [] };
|
|
112
|
+
|
|
113
|
+
const speckit = detectSpecKit(projectDir);
|
|
114
|
+
if (!speckit.detected) {
|
|
115
|
+
results.errors.push('No Spec Kit artifacts detected. This project does not use Spec Kit.');
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Generate REQUIREMENTS.md from spec.md files ──
|
|
120
|
+
if (speckit.specs.some(s => s.hasSpec)) {
|
|
121
|
+
const reqPath = resolve(projectDir, 'REQUIREMENTS.md');
|
|
122
|
+
if (existsSync(reqPath) && !flags.force) {
|
|
123
|
+
results.skipped.push('REQUIREMENTS.md already exists (use --force to overwrite)');
|
|
124
|
+
} else {
|
|
125
|
+
const lines = [
|
|
126
|
+
'# Requirements',
|
|
127
|
+
'',
|
|
128
|
+
'> Auto-generated from Spec Kit spec.md files by DocGuard',
|
|
129
|
+
'',
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
for (const spec of speckit.specs) {
|
|
133
|
+
if (!spec.hasSpec) continue;
|
|
134
|
+
const specPath = join(projectDir, 'specs', spec.name, 'spec.md');
|
|
135
|
+
const content = readFileSync(specPath, 'utf-8');
|
|
136
|
+
|
|
137
|
+
lines.push(`## ${spec.name}`);
|
|
138
|
+
lines.push('');
|
|
139
|
+
lines.push(content.trim());
|
|
140
|
+
lines.push('');
|
|
141
|
+
lines.push('---');
|
|
142
|
+
lines.push('');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
lines.push(`<!-- Generated by DocGuard from Spec Kit artifacts on ${new Date().toISOString().split('T')[0]} -->`);
|
|
146
|
+
|
|
147
|
+
writeFileSync(reqPath, lines.join('\n'), 'utf-8');
|
|
148
|
+
results.generated.push('REQUIREMENTS.md');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Map constitution.md to AGENTS.md context ──
|
|
153
|
+
if (speckit.constitution) {
|
|
154
|
+
const constitutionPath = resolve(projectDir, 'constitution.md');
|
|
155
|
+
const agentsPath = resolve(projectDir, 'AGENTS.md');
|
|
156
|
+
|
|
157
|
+
if (existsSync(agentsPath)) {
|
|
158
|
+
const agentsContent = readFileSync(agentsPath, 'utf-8');
|
|
159
|
+
|
|
160
|
+
// Check if AGENTS.md already references constitution
|
|
161
|
+
if (!agentsContent.includes('constitution.md') && !agentsContent.includes('Constitution')) {
|
|
162
|
+
results.skipped.push('AGENTS.md exists but does not reference constitution.md — consider adding a reference');
|
|
163
|
+
} else {
|
|
164
|
+
results.skipped.push('AGENTS.md already references constitution.md');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Map memory/ to DRIFT-LOG.md context ──
|
|
170
|
+
if (speckit.memory) {
|
|
171
|
+
results.skipped.push('memory/ directory detected — maps conceptually to DRIFT-LOG.md (no auto-generation needed)');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return results;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Validate Spec Kit artifact consistency.
|
|
179
|
+
* Used by guard to give CDD credit for Spec Kit artifacts.
|
|
180
|
+
*
|
|
181
|
+
* @returns {{ errors: string[], warnings: string[], passed: number, total: number }}
|
|
182
|
+
*/
|
|
183
|
+
export function validateSpecKitIntegration(projectDir, config) {
|
|
184
|
+
const results = { errors: [], warnings: [], passed: 0, total: 0 };
|
|
185
|
+
|
|
186
|
+
const speckit = detectSpecKit(projectDir);
|
|
187
|
+
|
|
188
|
+
// If no Spec Kit detected, silently pass
|
|
189
|
+
if (!speckit.detected) {
|
|
190
|
+
return results;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check 1: .specify/ directory exists → Spec Kit initialized
|
|
194
|
+
results.total++;
|
|
195
|
+
if (speckit.specifyDir) {
|
|
196
|
+
results.passed++;
|
|
197
|
+
} else {
|
|
198
|
+
results.warnings.push('Spec Kit artifacts detected but .specify/ directory missing. Run `specify init` to initialize');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check 2: Each spec has corresponding canonical doc coverage
|
|
202
|
+
for (const spec of speckit.specs) {
|
|
203
|
+
if (spec.hasSpec) {
|
|
204
|
+
results.total++;
|
|
205
|
+
// Check if spec.md content appears in any canonical doc
|
|
206
|
+
const specPath = join(projectDir, 'specs', spec.name, 'spec.md');
|
|
207
|
+
const content = readFileSync(specPath, 'utf-8');
|
|
208
|
+
|
|
209
|
+
// Look for requirement IDs in the spec
|
|
210
|
+
const hasReqIds = /\b(REQ|FR|NFR|US|STORY|AC)-\d{2,4}\b/.test(content);
|
|
211
|
+
|
|
212
|
+
if (hasReqIds) {
|
|
213
|
+
results.passed++;
|
|
214
|
+
} else {
|
|
215
|
+
results.warnings.push(
|
|
216
|
+
`specs/${spec.name}/spec.md has no requirement IDs. Add IDs (e.g., REQ-001) for traceability`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check 3: Constitution mapped to AGENTS.md
|
|
223
|
+
if (speckit.constitution) {
|
|
224
|
+
results.total++;
|
|
225
|
+
const agentsPath = resolve(projectDir, 'AGENTS.md');
|
|
226
|
+
if (existsSync(agentsPath)) {
|
|
227
|
+
results.passed++;
|
|
228
|
+
} else {
|
|
229
|
+
results.warnings.push('constitution.md exists but no AGENTS.md found. Create one for AI agent rules');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return results;
|
|
234
|
+
}
|
package/cli/shared.mjs
CHANGED
|
@@ -62,6 +62,41 @@ export const PROFILES = {
|
|
|
62
62
|
freshness: true,
|
|
63
63
|
},
|
|
64
64
|
},
|
|
65
|
+
'enterprise-ai': {
|
|
66
|
+
description: 'EU AI Act compliance — Annex IV documentation requirements, ALCOA+ alignment, strict freshness. For AI/ML projects under regulatory scrutiny.',
|
|
67
|
+
requiredFiles: {
|
|
68
|
+
canonical: [
|
|
69
|
+
'docs-canonical/ARCHITECTURE.md',
|
|
70
|
+
'docs-canonical/DATA-MODEL.md',
|
|
71
|
+
'docs-canonical/SECURITY.md',
|
|
72
|
+
'docs-canonical/TEST-SPEC.md',
|
|
73
|
+
'docs-canonical/ENVIRONMENT.md',
|
|
74
|
+
],
|
|
75
|
+
agentFile: ['AGENTS.md', 'CLAUDE.md'],
|
|
76
|
+
changelog: 'CHANGELOG.md',
|
|
77
|
+
driftLog: 'DRIFT-LOG.md',
|
|
78
|
+
},
|
|
79
|
+
validators: {
|
|
80
|
+
structure: true,
|
|
81
|
+
docsSync: true,
|
|
82
|
+
drift: true,
|
|
83
|
+
changelog: true,
|
|
84
|
+
architecture: true,
|
|
85
|
+
testSpec: true,
|
|
86
|
+
security: true,
|
|
87
|
+
environment: true,
|
|
88
|
+
freshness: true,
|
|
89
|
+
docQuality: true,
|
|
90
|
+
todoTracking: true,
|
|
91
|
+
schemaSync: true,
|
|
92
|
+
},
|
|
93
|
+
// Stricter freshness threshold — 14 days instead of 30
|
|
94
|
+
freshness: { maxDaysStale: 14 },
|
|
95
|
+
// SECURITY.md must have Risk Assessment section
|
|
96
|
+
requiredSections: {
|
|
97
|
+
'SECURITY.md': ['Risk Assessment', 'Threat Model'],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
65
100
|
};
|
|
66
101
|
|
|
67
102
|
// ── .docguardignore Support ───────────────────────────────────────────────
|