docguard-cli 0.8.2 → 0.9.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 +13 -6
- package/cli/commands/guard.mjs +8 -0
- package/cli/commands/llms.mjs +159 -0
- package/cli/commands/score.mjs +162 -0
- 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/todo-tracking.mjs +295 -0
- package/cli/validators/traceability.mjs +194 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -223,13 +223,13 @@ $ npx docguard-cli guard
|
|
|
223
223
|
|
|
224
224
|
---
|
|
225
225
|
|
|
226
|
-
##
|
|
226
|
+
## 19 Validators
|
|
227
227
|
|
|
228
228
|
| # | Validator | What It Checks | Default |
|
|
229
229
|
|---|-----------|---------------|---------|
|
|
230
230
|
| 1 | **Structure** | Required CDD files exist | ✅ On |
|
|
231
231
|
| 2 | **Doc Sections** | Canonical docs have required sections | ✅ On |
|
|
232
|
-
| 3 | **Docs-Sync** | Routes/services referenced in docs | ✅ On |
|
|
232
|
+
| 3 | **Docs-Sync** | Routes/services referenced in docs + OpenAPI cross-check | ✅ On |
|
|
233
233
|
| 4 | **Drift** | `// DRIFT:` comments logged in DRIFT-LOG.md | ✅ On |
|
|
234
234
|
| 5 | **Changelog** | CHANGELOG.md has [Unreleased] section | ✅ On |
|
|
235
235
|
| 6 | **Test-Spec** | Tests exist per TEST-SPEC.md rules | ✅ On |
|
|
@@ -237,16 +237,19 @@ $ npx docguard-cli guard
|
|
|
237
237
|
| 8 | **Security** | No hardcoded secrets in source code | ✅ On |
|
|
238
238
|
| 9 | **Architecture** | Imports follow layer boundaries | ✅ On |
|
|
239
239
|
| 10 | **Freshness** | Docs not stale relative to code changes | ✅ On |
|
|
240
|
-
| 11 | **Traceability** | Canonical docs linked to source
|
|
240
|
+
| 11 | **Traceability** | Canonical docs linked to source + V-Model requirement IDs | ✅ On |
|
|
241
241
|
| 12 | **Docs-Diff** | Code artifacts match documented entities | ✅ On |
|
|
242
242
|
| 13 | **Metadata-Sync** | Version refs consistent across docs | ✅ On |
|
|
243
243
|
| 14 | **Docs-Coverage** | Code features referenced in documentation | ✅ On |
|
|
244
244
|
| 15 | **Metrics-Consistency** | Hardcoded numbers match actual counts | ✅ On |
|
|
245
|
-
| 16 | **
|
|
245
|
+
| 16 | **Doc-Quality** | Writing quality (readability, passive voice, atomicity) | ✅ On |
|
|
246
|
+
| 17 | **TODO-Tracking** | Untracked TODOs/FIXMEs and skipped tests | ✅ On |
|
|
247
|
+
| 18 | **Schema-Sync** | Database models documented in DATA-MODEL.md | ✅ On |
|
|
248
|
+
| 19 | **Spec-Kit** | GitHub Spec Kit artifact detection and CDD mapping | ✅ On |
|
|
246
249
|
|
|
247
250
|
---
|
|
248
251
|
|
|
249
|
-
##
|
|
252
|
+
## 18 Templates
|
|
250
253
|
|
|
251
254
|
Every template includes professional metadata: `docguard:version`, `docguard:status`, badges, and revision history.
|
|
252
255
|
|
|
@@ -260,6 +263,7 @@ Every template includes professional metadata: `docguard:version`, `docguard:sta
|
|
|
260
263
|
| DEPLOYMENT.md | Canonical | Infrastructure, CI/CD, DNS |
|
|
261
264
|
| ADR.md | Canonical | Architecture Decision Records |
|
|
262
265
|
| ROADMAP.md | Canonical | Project phases, feature tracking |
|
|
266
|
+
| REQUIREMENTS.md | Canonical | Requirement IDs, V-Model traceability |
|
|
263
267
|
| KNOWN-GOTCHAS.md | Implementation | Symptom/gotcha/fix entries |
|
|
264
268
|
| TROUBLESHOOTING.md | Implementation | Error diagnosis guides |
|
|
265
269
|
| RUNBOOKS.md | Implementation | Operational procedures |
|
|
@@ -268,6 +272,7 @@ Every template includes professional metadata: `docguard:version`, `docguard:sta
|
|
|
268
272
|
| AGENTS.md | Agent | AI agent behavior rules |
|
|
269
273
|
| CHANGELOG.md | Tracking | Change log |
|
|
270
274
|
| DRIFT-LOG.md | Tracking | Deviation tracking |
|
|
275
|
+
| llms.txt | Generated | AI-friendly project summary (llmstxt.org) |
|
|
271
276
|
|
|
272
277
|
---
|
|
273
278
|
|
|
@@ -280,7 +285,8 @@ your-project/
|
|
|
280
285
|
│ ├── DATA-MODEL.md # Database schemas, entity relationships
|
|
281
286
|
│ ├── SECURITY.md # Auth, permissions, secrets
|
|
282
287
|
│ ├── TEST-SPEC.md # Required tests, coverage rules
|
|
283
|
-
│
|
|
288
|
+
│ ├── ENVIRONMENT.md # Environment variables, setup
|
|
289
|
+
│ └── REQUIREMENTS.md # Requirement IDs, V-Model traceability
|
|
284
290
|
│
|
|
285
291
|
├── docs-implementation/ # Current state (optional)
|
|
286
292
|
│ ├── KNOWN-GOTCHAS.md # Lessons learned
|
|
@@ -291,6 +297,7 @@ your-project/
|
|
|
291
297
|
├── AGENTS.md # AI agent behavior rules
|
|
292
298
|
├── CHANGELOG.md # Change tracking
|
|
293
299
|
├── DRIFT-LOG.md # Documented deviations
|
|
300
|
+
├── llms.txt # AI-friendly project summary
|
|
294
301
|
└── .docguard.json # DocGuard configuration
|
|
295
302
|
```
|
|
296
303
|
|
package/cli/commands/guard.mjs
CHANGED
|
@@ -22,6 +22,10 @@ import { validateDocsDiff } from '../validators/docs-diff.mjs';
|
|
|
22
22
|
import { validateMetadataSync } from '../validators/metadata-sync.mjs';
|
|
23
23
|
import { validateMetricsConsistency } from '../validators/metrics-consistency.mjs';
|
|
24
24
|
import { validateDocsCoverage } from '../validators/docs-coverage.mjs';
|
|
25
|
+
import { validateDocQuality } from '../validators/doc-quality.mjs';
|
|
26
|
+
import { validateTodoTracking } from '../validators/todo-tracking.mjs';
|
|
27
|
+
import { validateSchemaSync } from '../validators/schema-sync.mjs';
|
|
28
|
+
import { validateSpecKitIntegration } from '../scanners/speckit.mjs';
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* Internal guard — returns structured data, no console output, no process.exit.
|
|
@@ -57,6 +61,10 @@ export function runGuardInternal(projectDir, config) {
|
|
|
57
61
|
{ key: 'docsDiff', name: 'Docs-Diff', fn: () => validateDocsDiff(projectDir, config) },
|
|
58
62
|
{ key: 'metadataSync', name: 'Metadata-Sync', fn: () => validateMetadataSync(projectDir, config) },
|
|
59
63
|
{ key: 'docsCoverage', name: 'Docs-Coverage', fn: () => validateDocsCoverage(projectDir, config) },
|
|
64
|
+
{ key: 'docQuality', name: 'Doc-Quality', fn: () => validateDocQuality(projectDir, config) },
|
|
65
|
+
{ key: 'todoTracking', name: 'TODO-Tracking', fn: () => validateTodoTracking(projectDir, config) },
|
|
66
|
+
{ key: 'schemaSync', name: 'Schema-Sync', fn: () => validateSchemaSync(projectDir, config) },
|
|
67
|
+
{ key: 'specKit', name: 'Spec-Kit', fn: () => validateSpecKitIntegration(projectDir, config) },
|
|
60
68
|
// Metrics-Consistency runs post-loop (needs guard results)
|
|
61
69
|
];
|
|
62
70
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* llms Command — Generate llms.txt from canonical documentation
|
|
3
|
+
*
|
|
4
|
+
* llms.txt is a proposed standard (Jeremy Howard, Answer.AI, 2024) for providing
|
|
5
|
+
* AI-friendly content summaries at a project root. This command generates it
|
|
6
|
+
* from existing canonical docs.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* docguard llms — Generate/regenerate llms.txt
|
|
10
|
+
* docguard llms --stdout — Print to stdout instead of file
|
|
11
|
+
*
|
|
12
|
+
* Integration:
|
|
13
|
+
* - `docguard init` includes llms.txt in standard template
|
|
14
|
+
* - `docguard generate` auto-regenerates llms.txt
|
|
15
|
+
* - `docguard guard` validates llms.txt exists and is current
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
19
|
+
import { resolve, join, basename } from 'node:path';
|
|
20
|
+
import { c } from '../shared.mjs';
|
|
21
|
+
|
|
22
|
+
// ──── Doc descriptions for llms.txt ────
|
|
23
|
+
const DOC_DESCRIPTIONS = {
|
|
24
|
+
'ARCHITECTURE.md': 'System architecture, component boundaries, and tech stack',
|
|
25
|
+
'DATA-MODEL.md': 'Database schemas, entity relationships, and data flow',
|
|
26
|
+
'SECURITY.md': 'Authentication, authorization, secrets management, and security policies',
|
|
27
|
+
'TEST-SPEC.md': 'Test coverage requirements, testing strategy, and quality rules',
|
|
28
|
+
'ENVIRONMENT.md': 'Setup instructions, environment variables, and prerequisites',
|
|
29
|
+
'API-REFERENCE.md': 'API endpoints, request/response formats, and integration docs',
|
|
30
|
+
'DEPLOYMENT.md': 'Deployment procedures, CI/CD pipelines, and infrastructure',
|
|
31
|
+
'MONITORING.md': 'Observability, logging, alerting, and health checks',
|
|
32
|
+
'PERFORMANCE.md': 'Performance requirements, benchmarks, and optimization',
|
|
33
|
+
'ACCESSIBILITY.md': 'WCAG compliance, accessibility standards, and testing',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const OPTIONAL_DOCS = {
|
|
37
|
+
'DRIFT-LOG.md': 'Known deviations from canonical documentation',
|
|
38
|
+
'CHANGELOG.md': 'Version history and release notes',
|
|
39
|
+
'ROADMAP.md': 'Planned features and development roadmap',
|
|
40
|
+
'REQUIREMENTS.md': 'Tracked requirements with traceability IDs',
|
|
41
|
+
'AGENTS.md': 'AI agent behavior rules and workflow instructions',
|
|
42
|
+
'CURRENT-STATE.md': 'Current implementation status and known issues',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Generate llms.txt content from project docs.
|
|
47
|
+
*/
|
|
48
|
+
export function generateLlmsTxt(projectDir, config) {
|
|
49
|
+
const lines = [];
|
|
50
|
+
|
|
51
|
+
// ── Header ──
|
|
52
|
+
const projectName = config.projectName || basename(projectDir);
|
|
53
|
+
const description = getProjectDescription(projectDir);
|
|
54
|
+
|
|
55
|
+
lines.push(`# ${projectName}`);
|
|
56
|
+
if (description) {
|
|
57
|
+
lines.push(`> ${description}`);
|
|
58
|
+
}
|
|
59
|
+
lines.push('');
|
|
60
|
+
|
|
61
|
+
// ── Canonical Docs ──
|
|
62
|
+
const docsDir = resolve(projectDir, 'docs-canonical');
|
|
63
|
+
const existingDocs = [];
|
|
64
|
+
|
|
65
|
+
if (existsSync(docsDir)) {
|
|
66
|
+
try {
|
|
67
|
+
const entries = readdirSync(docsDir).filter(f => f.endsWith('.md')).sort();
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const desc = DOC_DESCRIPTIONS[entry] || `${entry.replace('.md', '')} documentation`;
|
|
70
|
+
existingDocs.push({ path: `docs-canonical/${entry}`, name: entry, desc });
|
|
71
|
+
}
|
|
72
|
+
} catch { /* ignore */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (existingDocs.length > 0) {
|
|
76
|
+
lines.push('## Docs');
|
|
77
|
+
lines.push('');
|
|
78
|
+
for (const doc of existingDocs) {
|
|
79
|
+
lines.push(`- [${doc.name.replace('.md', '')}](${doc.path}): ${doc.desc}`);
|
|
80
|
+
}
|
|
81
|
+
lines.push('');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Optional Docs ──
|
|
85
|
+
const optionalFound = [];
|
|
86
|
+
for (const [file, desc] of Object.entries(OPTIONAL_DOCS)) {
|
|
87
|
+
if (existsSync(resolve(projectDir, file))) {
|
|
88
|
+
optionalFound.push({ path: file, name: file, desc });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (optionalFound.length > 0) {
|
|
93
|
+
lines.push('## Optional');
|
|
94
|
+
lines.push('');
|
|
95
|
+
for (const doc of optionalFound) {
|
|
96
|
+
lines.push(`- [${doc.name.replace('.md', '')}](${doc.path}): ${doc.desc}`);
|
|
97
|
+
}
|
|
98
|
+
lines.push('');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Footer ──
|
|
102
|
+
lines.push('---');
|
|
103
|
+
lines.push(`Generated by DocGuard v${config.version || 'unknown'} | [docguard-cli](https://www.npmjs.com/package/docguard-cli)`);
|
|
104
|
+
lines.push('');
|
|
105
|
+
|
|
106
|
+
return lines.join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get project description from package.json or ARCHITECTURE.md.
|
|
111
|
+
*/
|
|
112
|
+
function getProjectDescription(projectDir) {
|
|
113
|
+
// Try package.json first
|
|
114
|
+
const pkgPath = resolve(projectDir, 'package.json');
|
|
115
|
+
if (existsSync(pkgPath)) {
|
|
116
|
+
try {
|
|
117
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
118
|
+
if (pkg.description) return pkg.description;
|
|
119
|
+
} catch { /* ignore */ }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Try ARCHITECTURE.md first line after the header
|
|
123
|
+
const archPath = resolve(projectDir, 'docs-canonical', 'ARCHITECTURE.md');
|
|
124
|
+
if (existsSync(archPath)) {
|
|
125
|
+
try {
|
|
126
|
+
const content = readFileSync(archPath, 'utf-8');
|
|
127
|
+
const lines = content.split('\n');
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
const trimmed = line.trim();
|
|
130
|
+
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('|') && !trimmed.startsWith('-')) {
|
|
131
|
+
return trimmed.substring(0, 200);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch { /* ignore */ }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Public command — generate llms.txt file.
|
|
142
|
+
*/
|
|
143
|
+
export function runLlms(projectDir, config, flags) {
|
|
144
|
+
const content = generateLlmsTxt(projectDir, config);
|
|
145
|
+
|
|
146
|
+
if (flags.stdout) {
|
|
147
|
+
console.log(content);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const outputPath = resolve(projectDir, 'llms.txt');
|
|
152
|
+
writeFileSync(outputPath, content, 'utf-8');
|
|
153
|
+
|
|
154
|
+
console.log(`${c.bold}📄 DocGuard llms.txt Generator${c.reset}`);
|
|
155
|
+
console.log(`${c.green}✅ Generated ${outputPath}${c.reset}`);
|
|
156
|
+
console.log(`${c.dim} Standard: llms.txt (Jeremy Howard, Answer.AI, 2024)${c.reset}`);
|
|
157
|
+
console.log(`${c.dim} DocGuard keeps this in sync with your canonical docs.${c.reset}`);
|
|
158
|
+
console.log('');
|
|
159
|
+
}
|
package/cli/commands/score.mjs
CHANGED
|
@@ -131,6 +131,30 @@ export function runScore(projectDir, config, flags) {
|
|
|
131
131
|
console.log(` ${c.dim}Methodology: CJE multi-signal composite (Lopez et al., TRACE, IEEE TMLCN 2026)${c.reset}\n`);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// ── ALCOA+ Compliance Scoring ──
|
|
135
|
+
// Maps existing validators to the 9 ALCOA+ attributes (FDA data integrity framework)
|
|
136
|
+
// Always shown — gives enterprise positioning value
|
|
137
|
+
const alcoa = computeAlcoaCompliance(projectDir, config, scores);
|
|
138
|
+
|
|
139
|
+
console.log(` ${c.bold}🏛️ ALCOA+ Compliance${c.reset} ${c.dim}(FDA Data Integrity Framework)${c.reset}`);
|
|
140
|
+
console.log(` ${c.dim}─────────────────────────────────${c.reset}`);
|
|
141
|
+
|
|
142
|
+
for (const attr of alcoa.attributes) {
|
|
143
|
+
const icon = attr.met ? `${c.green}✅` : `${c.yellow}⚠️`;
|
|
144
|
+
const status = attr.met ? `${c.green}${attr.evidence}` : `${c.yellow}${attr.gap}`;
|
|
145
|
+
console.log(` ${icon} ${attr.name.padEnd(16)}${c.reset} — ${status}${c.reset}`);
|
|
146
|
+
if (!attr.met && attr.fix) {
|
|
147
|
+
console.log(` ${c.dim} Fix: ${attr.fix}${c.reset}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const alcoaColor = alcoa.score >= 78 ? c.green : alcoa.score >= 56 ? c.yellow : c.red;
|
|
152
|
+
console.log(`\n ${alcoaColor}${c.bold}ALCOA+ Score: ${alcoa.score}% (${alcoa.met}/${alcoa.total} attributes)${c.reset}`);
|
|
153
|
+
if (alcoa.met < alcoa.total) {
|
|
154
|
+
console.log(` ${c.dim}${alcoa.total - alcoa.met} action(s) needed for full compliance${c.reset}`);
|
|
155
|
+
}
|
|
156
|
+
console.log('');
|
|
157
|
+
|
|
134
158
|
// Badge snippet
|
|
135
159
|
const bColor = totalScore >= 90 ? 'brightgreen' : totalScore >= 80 ? 'green' : totalScore >= 70 ? 'yellowgreen' : totalScore >= 60 ? 'yellow' : totalScore >= 50 ? 'orange' : 'red';
|
|
136
160
|
const badgeUrl = `https://img.shields.io/badge/CDD_Score-${totalScore}%2F100_(${grade})-${bColor}`;
|
|
@@ -146,6 +170,144 @@ export function runScoreInternal(projectDir, config) {
|
|
|
146
170
|
return { score: totalScore, grade, categories: scores };
|
|
147
171
|
}
|
|
148
172
|
|
|
173
|
+
/**
|
|
174
|
+
* ALCOA+ Compliance Scoring
|
|
175
|
+
*
|
|
176
|
+
* Maps DocGuard's existing validators to the 9 ALCOA+ attributes
|
|
177
|
+
* (FDA 21 CFR Part 11 / EMA Annex 11 data integrity framework).
|
|
178
|
+
*
|
|
179
|
+
* ALCOA+ = Attributable, Legible, Contemporaneous, Original, Accurate
|
|
180
|
+
* + Complete, Consistent, Enduring, Available
|
|
181
|
+
*
|
|
182
|
+
* Reference: WHO Technical Report Series, No. 996, 2016, Annex 5
|
|
183
|
+
*/
|
|
184
|
+
function computeAlcoaCompliance(projectDir, config, scores) {
|
|
185
|
+
const attributes = [];
|
|
186
|
+
|
|
187
|
+
// 1. Attributable — Can we trace who wrote/reviewed docs?
|
|
188
|
+
const hasGit = existsSync(resolve(projectDir, '.git'));
|
|
189
|
+
const docsDir = resolve(projectDir, 'docs-canonical');
|
|
190
|
+
let hasReviewedMeta = false;
|
|
191
|
+
if (existsSync(docsDir)) {
|
|
192
|
+
try {
|
|
193
|
+
const docs = readdirSync(docsDir).filter(f => f.endsWith('.md'));
|
|
194
|
+
for (const doc of docs) {
|
|
195
|
+
const content = readFileSync(join(docsDir, doc), 'utf-8');
|
|
196
|
+
if (content.includes('docguard:last-reviewed') || content.includes('last-reviewed')) {
|
|
197
|
+
hasReviewedMeta = true;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch { /* ignore */ }
|
|
202
|
+
}
|
|
203
|
+
attributes.push({
|
|
204
|
+
name: 'Attributable',
|
|
205
|
+
met: hasGit,
|
|
206
|
+
evidence: hasGit ? `Git authorship found${hasReviewedMeta ? ', review metadata present' : ''}` : null,
|
|
207
|
+
gap: !hasGit ? 'No version control found' : null,
|
|
208
|
+
fix: !hasGit ? 'Initialize git repository: git init' : null,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// 2. Legible — Are docs readable and well-written?
|
|
212
|
+
const legible = scores.docQuality >= 60;
|
|
213
|
+
attributes.push({
|
|
214
|
+
name: 'Legible',
|
|
215
|
+
met: legible,
|
|
216
|
+
evidence: legible ? `Doc quality score: ${scores.docQuality}% (readable)` : null,
|
|
217
|
+
gap: !legible ? `Doc quality score: ${scores.docQuality}% (needs improvement)` : null,
|
|
218
|
+
fix: !legible ? 'Run docguard diagnose for specific readability improvements' : null,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// 3. Contemporaneous — Are docs kept current?
|
|
222
|
+
let freshnessMet = true;
|
|
223
|
+
if (existsSync(docsDir)) {
|
|
224
|
+
try {
|
|
225
|
+
const docs = readdirSync(docsDir).filter(f => f.endsWith('.md'));
|
|
226
|
+
for (const doc of docs) {
|
|
227
|
+
const stat_ = statSync(join(docsDir, doc));
|
|
228
|
+
const daysSinceModified = (Date.now() - stat_.mtimeMs) / (1000 * 60 * 60 * 24);
|
|
229
|
+
if (daysSinceModified > 30) {
|
|
230
|
+
freshnessMet = false;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch { /* ignore */ }
|
|
235
|
+
}
|
|
236
|
+
attributes.push({
|
|
237
|
+
name: 'Contemporaneous',
|
|
238
|
+
met: freshnessMet,
|
|
239
|
+
evidence: freshnessMet ? 'All docs updated within 30 days' : null,
|
|
240
|
+
gap: !freshnessMet ? 'Some docs not updated in 30+ days' : null,
|
|
241
|
+
fix: !freshnessMet ? 'Review and update stale docs, add <!-- docguard:last-reviewed YYYY-MM-DD -->' : null,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// 4. Original — Are docs stored as originals (not copies)?
|
|
245
|
+
const hasCanonicalDir = existsSync(docsDir);
|
|
246
|
+
attributes.push({
|
|
247
|
+
name: 'Original',
|
|
248
|
+
met: hasCanonicalDir,
|
|
249
|
+
evidence: hasCanonicalDir ? 'Canonical docs present as markdown originals' : null,
|
|
250
|
+
gap: !hasCanonicalDir ? 'No docs-canonical/ directory found' : null,
|
|
251
|
+
fix: !hasCanonicalDir ? 'Run docguard init to create canonical documentation' : null,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// 5. Accurate — Do docs match the code?
|
|
255
|
+
const accurate = scores.drift >= 80 && scores.docQuality >= 50;
|
|
256
|
+
attributes.push({
|
|
257
|
+
name: 'Accurate',
|
|
258
|
+
met: accurate,
|
|
259
|
+
evidence: accurate ? `Drift: ${scores.drift}%, doc quality: ${scores.docQuality}%` : null,
|
|
260
|
+
gap: !accurate ? `Drift: ${scores.drift}%, doc quality: ${scores.docQuality}% — docs may be inaccurate` : null,
|
|
261
|
+
fix: !accurate ? 'Run docguard diagnose to find doc/code mismatches' : null,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// 6. Complete — Are all required docs present?
|
|
265
|
+
const complete = scores.structure >= 80;
|
|
266
|
+
attributes.push({
|
|
267
|
+
name: 'Complete',
|
|
268
|
+
met: complete,
|
|
269
|
+
evidence: complete ? `Structure score: ${scores.structure}% — required docs present` : null,
|
|
270
|
+
gap: !complete ? `Structure score: ${scores.structure}% — missing required docs` : null,
|
|
271
|
+
fix: !complete ? 'Run docguard init to create missing documentation' : null,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// 7. Consistent — Are versions, metadata, and references in sync?
|
|
275
|
+
const consistent = scores.changelog >= 50;
|
|
276
|
+
attributes.push({
|
|
277
|
+
name: 'Consistent',
|
|
278
|
+
met: consistent,
|
|
279
|
+
evidence: consistent ? `Changelog: ${scores.changelog}% — versions tracked` : null,
|
|
280
|
+
gap: !consistent ? `Changelog: ${scores.changelog}% — version inconsistencies` : null,
|
|
281
|
+
fix: !consistent ? 'Update CHANGELOG.md with [Unreleased] section and version headers' : null,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// 8. Enduring — Will docs survive infrastructure changes?
|
|
285
|
+
const enduring = hasGit;
|
|
286
|
+
attributes.push({
|
|
287
|
+
name: 'Enduring',
|
|
288
|
+
met: enduring,
|
|
289
|
+
evidence: enduring ? 'Git-backed repository with version history' : null,
|
|
290
|
+
gap: !enduring ? 'No version control — docs could be lost' : null,
|
|
291
|
+
fix: !enduring ? 'Initialize git repository: git init' : null,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// 9. Available — Can anyone access the docs?
|
|
295
|
+
const available = hasCanonicalDir;
|
|
296
|
+
attributes.push({
|
|
297
|
+
name: 'Available',
|
|
298
|
+
met: available,
|
|
299
|
+
evidence: available ? 'Docs in plain markdown — no vendor lock-in, universally accessible' : null,
|
|
300
|
+
gap: !available ? 'No docs directory found' : null,
|
|
301
|
+
fix: !available ? 'Run docguard init to create accessible documentation' : null,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const met = attributes.filter(a => a.met).length;
|
|
305
|
+
const total = attributes.length;
|
|
306
|
+
const score = Math.round((met / total) * 100);
|
|
307
|
+
|
|
308
|
+
return { attributes, met, total, score };
|
|
309
|
+
}
|
|
310
|
+
|
|
149
311
|
function calcAllScores(projectDir, config) {
|
|
150
312
|
const scores = {};
|
|
151
313
|
const details = {}; // Per-category failure details for actionable suggestions
|
package/cli/docguard.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import { runWatch } from './commands/watch.mjs';
|
|
|
37
37
|
import { runDiagnose } from './commands/diagnose.mjs';
|
|
38
38
|
import { runPublish } from './commands/publish.mjs';
|
|
39
39
|
import { runTrace } from './commands/trace.mjs';
|
|
40
|
+
import { runLlms } from './commands/llms.mjs';
|
|
40
41
|
|
|
41
42
|
// ── Shared constants (imported to break circular dependencies) ──────────
|
|
42
43
|
import { c, PROFILES } from './shared.mjs';
|
|
@@ -354,6 +355,8 @@ async function main() {
|
|
|
354
355
|
flags.signals = true;
|
|
355
356
|
} else if (args[i] === '--debate') {
|
|
356
357
|
flags.debate = true;
|
|
358
|
+
} else if (args[i] === '--stdout') {
|
|
359
|
+
flags.stdout = true;
|
|
357
360
|
}
|
|
358
361
|
}
|
|
359
362
|
|
|
@@ -427,6 +430,9 @@ async function main() {
|
|
|
427
430
|
case 'traceability':
|
|
428
431
|
runTrace(projectDir, config, flags);
|
|
429
432
|
break;
|
|
433
|
+
case 'llms':
|
|
434
|
+
runLlms(projectDir, config, flags);
|
|
435
|
+
break;
|
|
430
436
|
default:
|
|
431
437
|
console.error(`${c.red}Unknown command: ${command}${c.reset}`);
|
|
432
438
|
console.log(`Run ${c.cyan}docguard --help${c.reset} for usage.`);
|
|
@@ -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
|
+
}
|