docguard-cli 0.8.0 → 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 CHANGED
@@ -81,6 +81,27 @@ diagnose → AI reads prompts → AI fixes docs → guard verifies
81
81
 
82
82
  ---
83
83
 
84
+ ## Usage
85
+
86
+ ```bash
87
+ # Initialize CDD docs for your project
88
+ npx docguard-cli init
89
+
90
+ # Reverse-engineer docs from existing code
91
+ npx docguard-cli generate
92
+
93
+ # Validate project — use as CI gate
94
+ npx docguard-cli guard
95
+
96
+ # Get AI-actionable fix prompts
97
+ npx docguard-cli diagnose
98
+
99
+ # Check CDD maturity score
100
+ npx docguard-cli score
101
+ ```
102
+
103
+ ---
104
+
84
105
  ## 13 Commands
85
106
 
86
107
  ### 🔮 Generate — Reverse-engineer docs from code
@@ -202,23 +223,33 @@ $ npx docguard-cli guard
202
223
 
203
224
  ---
204
225
 
205
- ## 9 Validators
226
+ ## 19 Validators
206
227
 
207
228
  | # | Validator | What It Checks | Default |
208
229
  |---|-----------|---------------|---------|
209
230
  | 1 | **Structure** | Required CDD files exist | ✅ On |
210
231
  | 2 | **Doc Sections** | Canonical docs have required sections | ✅ On |
211
- | 3 | **Docs-Sync** | Routes/services referenced in docs | ✅ On |
232
+ | 3 | **Docs-Sync** | Routes/services referenced in docs + OpenAPI cross-check | ✅ On |
212
233
  | 4 | **Drift** | `// DRIFT:` comments logged in DRIFT-LOG.md | ✅ On |
213
234
  | 5 | **Changelog** | CHANGELOG.md has [Unreleased] section | ✅ On |
214
235
  | 6 | **Test-Spec** | Tests exist per TEST-SPEC.md rules | ✅ On |
215
236
  | 7 | **Environment** | Env vars documented, .env.example exists | ✅ On |
216
- | 8 | **Security** | No hardcoded secrets in source code | Off |
217
- | 9 | **Architecture** | Imports follow layer boundaries | Off |
237
+ | 8 | **Security** | No hardcoded secrets in source code | On |
238
+ | 9 | **Architecture** | Imports follow layer boundaries | On |
239
+ | 10 | **Freshness** | Docs not stale relative to code changes | ✅ On |
240
+ | 11 | **Traceability** | Canonical docs linked to source + V-Model requirement IDs | ✅ On |
241
+ | 12 | **Docs-Diff** | Code artifacts match documented entities | ✅ On |
242
+ | 13 | **Metadata-Sync** | Version refs consistent across docs | ✅ On |
243
+ | 14 | **Docs-Coverage** | Code features referenced in documentation | ✅ On |
244
+ | 15 | **Metrics-Consistency** | Hardcoded numbers match actual counts | ✅ On |
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 |
218
249
 
219
250
  ---
220
251
 
221
- ## 16 Templates
252
+ ## 18 Templates
222
253
 
223
254
  Every template includes professional metadata: `docguard:version`, `docguard:status`, badges, and revision history.
224
255
 
@@ -232,6 +263,7 @@ Every template includes professional metadata: `docguard:version`, `docguard:sta
232
263
  | DEPLOYMENT.md | Canonical | Infrastructure, CI/CD, DNS |
233
264
  | ADR.md | Canonical | Architecture Decision Records |
234
265
  | ROADMAP.md | Canonical | Project phases, feature tracking |
266
+ | REQUIREMENTS.md | Canonical | Requirement IDs, V-Model traceability |
235
267
  | KNOWN-GOTCHAS.md | Implementation | Symptom/gotcha/fix entries |
236
268
  | TROUBLESHOOTING.md | Implementation | Error diagnosis guides |
237
269
  | RUNBOOKS.md | Implementation | Operational procedures |
@@ -240,6 +272,7 @@ Every template includes professional metadata: `docguard:version`, `docguard:sta
240
272
  | AGENTS.md | Agent | AI agent behavior rules |
241
273
  | CHANGELOG.md | Tracking | Change log |
242
274
  | DRIFT-LOG.md | Tracking | Deviation tracking |
275
+ | llms.txt | Generated | AI-friendly project summary (llmstxt.org) |
243
276
 
244
277
  ---
245
278
 
@@ -252,7 +285,8 @@ your-project/
252
285
  │ ├── DATA-MODEL.md # Database schemas, entity relationships
253
286
  │ ├── SECURITY.md # Auth, permissions, secrets
254
287
  │ ├── TEST-SPEC.md # Required tests, coverage rules
255
- └── ENVIRONMENT.md # Environment variables, setup
288
+ ├── ENVIRONMENT.md # Environment variables, setup
289
+ │ └── REQUIREMENTS.md # Requirement IDs, V-Model traceability
256
290
 
257
291
  ├── docs-implementation/ # Current state (optional)
258
292
  │ ├── KNOWN-GOTCHAS.md # Lessons learned
@@ -263,6 +297,7 @@ your-project/
263
297
  ├── AGENTS.md # AI agent behavior rules
264
298
  ├── CHANGELOG.md # Change tracking
265
299
  ├── DRIFT-LOG.md # Documented deviations
300
+ ├── llms.txt # AI-friendly project summary
266
301
  └── .docguard.json # DocGuard configuration
267
302
  ```
268
303
 
@@ -17,7 +17,8 @@ import { c } from '../shared.mjs';
17
17
  import { runGuardInternal } from './guard.mjs';
18
18
  import { runScoreInternal } from './score.mjs';
19
19
  import { existsSync, readFileSync, mkdirSync } from 'node:fs';
20
- import { resolve } from 'node:path';
20
+ import { resolve, dirname } from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
21
22
  import { execSync } from 'node:child_process';
22
23
 
23
24
  // Map validator failures to the right fix --doc target
@@ -103,16 +104,16 @@ export function runDiagnose(projectDir, config, flags) {
103
104
  // ── Step 2: Collect issues ──
104
105
  let issues = collectIssues(guardData);
105
106
 
106
- // ── Step 3: Auto-fix what we can (unless --no-fix) ──
107
- const shouldAutoFix = !flags.noFix && flags.format !== 'json';
108
- if (shouldAutoFix && issues.length > 0) {
107
+ // ── Step 3: Auto-fix (only with --auto flag) or suggest fixes ──
108
+ const shouldAutoFix = flags.auto && flags.format !== 'json';
109
+ if (issues.length > 0) {
109
110
  const autoFixable = issues.filter(i => i.autoFixable);
110
111
  const hasStructural = issues.some(i => i.validator === 'Structure');
111
112
 
112
- if (hasStructural || autoFixable.length > 0) {
113
+ if (shouldAutoFix && (hasStructural || autoFixable.length > 0)) {
113
114
  // Run init to create missing files
114
115
  try {
115
- const cliPath = resolve(import.meta.dirname, '..', 'docguard.mjs');
116
+ const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'docguard.mjs');
116
117
  execSync(`node "${cliPath}" init --dir "${projectDir}"`, {
117
118
  encoding: 'utf-8',
118
119
  stdio: 'pipe',
@@ -121,7 +122,7 @@ export function runDiagnose(projectDir, config, flags) {
121
122
 
122
123
  // Run generate to fill in content
123
124
  try {
124
- const cliPath = resolve(import.meta.dirname, '..', 'docguard.mjs');
125
+ const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'docguard.mjs');
125
126
  execSync(`node "${cliPath}" generate --dir "${projectDir}" --force`, {
126
127
  encoding: 'utf-8',
127
128
  stdio: 'pipe',
@@ -138,6 +139,12 @@ export function runDiagnose(projectDir, config, flags) {
138
139
  console.log(` ${c.green}⚡ Auto-fixed ${fixedCount} issue(s)${c.reset} (created/regenerated docs)\n`);
139
140
  }
140
141
  }
142
+ } else if (!shouldAutoFix && (hasStructural || autoFixable.length > 0) && (!flags.format || flags.format === 'text')) {
143
+ // Suggest-only mode: tell user what they can do
144
+ console.log(` ${c.yellow}💡 ${autoFixable.length + (hasStructural ? 1 : 0)} issue(s) can be auto-fixed.${c.reset} Run with ${c.cyan}--auto${c.reset} to create/regenerate docs, or manually:`);
145
+ if (hasStructural) console.log(` ${c.dim}docguard init --dir .${c.reset}`);
146
+ if (autoFixable.length > 0) console.log(` ${c.dim}docguard generate --dir . --force${c.reset}`);
147
+ console.log('');
141
148
  }
142
149
  }
143
150
 
@@ -340,7 +347,7 @@ function outputPrompt(projectDir, guardData, scoreData, issues, flags) {
340
347
  lines.push('VALIDATION:');
341
348
  lines.push('After making all fixes, run: docguard guard');
342
349
  lines.push('Expected result: All checks pass (0 errors, 0 warnings)');
343
- lines.push(`Target score: ≥${scoreData.score + 5}/100`);
350
+ lines.push(`Target score: ≥${Math.min(scoreData.score + 5, 100)}/100`);
344
351
 
345
352
  // Agent-aware: add explicit checklist for basic-tier agents
346
353
  if (agentTier === 'basic') {
@@ -14,8 +14,9 @@
14
14
  */
15
15
 
16
16
  import { existsSync, readFileSync, mkdirSync } from 'node:fs';
17
- import { resolve, basename } from 'node:path';
17
+ import { resolve, basename, dirname } from 'node:path';
18
18
  import { execSync } from 'node:child_process';
19
+ import { fileURLToPath } from 'node:url';
19
20
  import { c } from '../shared.mjs';
20
21
 
21
22
  // ── Document Quality Definitions ───────────────────────────────────────────
@@ -471,7 +472,7 @@ function autoFixIssues(projectDir, config, issues) {
471
472
  }
472
473
 
473
474
  try {
474
- const cliPath = resolve(import.meta.dirname, '..', 'docguard.mjs');
475
+ const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'docguard.mjs');
475
476
  execSync(`node ${cliPath} init --dir "${projectDir}"`, {
476
477
  encoding: 'utf-8',
477
478
  stdio: 'pipe',
@@ -318,7 +318,7 @@ function scanProject(dir) {
318
318
  }
319
319
  });
320
320
 
321
- // Find tests
321
+ // Find tests — top-level test dirs
322
322
  ['tests', 'test', '__tests__', 'spec', 'e2e'].forEach(testDir => {
323
323
  const fullDir = resolve(dir, testDir);
324
324
  if (existsSync(fullDir)) {
@@ -329,6 +329,55 @@ function scanProject(dir) {
329
329
  }
330
330
  });
331
331
 
332
+ // Find co-located tests: src/**/__tests__/ and src/**/*.test.* / src/**/*.spec.*
333
+ const srcDir = resolve(dir, 'src');
334
+ if (existsSync(srcDir)) {
335
+ walkDir(srcDir, (filePath) => {
336
+ const rel = relative(dir, filePath);
337
+ const isTestDir = rel.includes('__tests__') || rel.includes('__test__');
338
+ const isTestFile = /\.(test|spec)\.[^.]+$/.test(rel);
339
+ if ((isTestDir || isTestFile) && !scan.tests.includes(rel)) {
340
+ scan.tests.push(rel);
341
+ }
342
+ });
343
+ }
344
+
345
+ // Read vitest/jest config for custom test patterns
346
+ const testConfigs = ['vitest.config.ts', 'vitest.config.js', 'vitest.config.mts', 'jest.config.ts', 'jest.config.js'];
347
+ for (const cfgFile of testConfigs) {
348
+ const cfgPath = resolve(dir, cfgFile);
349
+ if (existsSync(cfgPath)) {
350
+ try {
351
+ const cfgContent = readFileSync(cfgPath, 'utf-8');
352
+ // Extract include patterns like: include: ['src/**/*.test.ts']
353
+ const includeMatch = cfgContent.match(/include\s*:\s*\[([^\]]+)\]/);
354
+ if (includeMatch) {
355
+ // Parse the test root from the pattern (e.g., 'src/**/*.test.ts' → 'src')
356
+ const patterns = includeMatch[1].match(/['"]([^'"]+)['"]/g);
357
+ if (patterns) {
358
+ for (const p of patterns) {
359
+ const pattern = p.replace(/['"]|\s/g, '');
360
+ // Extract root dir from glob (e.g., 'src/**/*.test.ts' → 'src')
361
+ const rootDir = pattern.split('/')[0];
362
+ if (rootDir && rootDir !== '**' && rootDir !== '*') {
363
+ const fullDir = resolve(dir, rootDir);
364
+ if (existsSync(fullDir)) {
365
+ walkDir(fullDir, (filePath) => {
366
+ const rel = relative(dir, filePath);
367
+ if (/\.(test|spec)\.[^.]+$/.test(rel) && !scan.tests.includes(rel)) {
368
+ scan.tests.push(rel);
369
+ }
370
+ });
371
+ }
372
+ }
373
+ }
374
+ }
375
+ }
376
+ } catch { /* config parse may fail */ }
377
+ break; // Use first found config
378
+ }
379
+ }
380
+
332
381
  // Find components
333
382
  ['src/components', 'components', 'src/ui'].forEach(compDir => {
334
383
  const fullDir = resolve(dir, compDir);
@@ -367,6 +416,15 @@ function scanProject(dir) {
367
416
  // Count files and lines
368
417
  countFilesAndLines(dir, scan);
369
418
 
419
+ // ── Filter test files out of source lists ──
420
+ // Test files (*.test.*, *.spec.*, __tests__/) should NOT appear as source files
421
+ const isTestFile = (f) => f.includes('__tests__') || f.includes('__test__') || /\.(test|spec)\.[^.]+$/.test(f);
422
+ scan.routes = scan.routes.filter(f => !isTestFile(f));
423
+ scan.models = scan.models.filter(f => !isTestFile(f));
424
+ scan.services = scan.services.filter(f => !isTestFile(f));
425
+ scan.components = scan.components.filter(f => !isTestFile(f));
426
+ scan.middlewares = scan.middlewares.filter(f => !isTestFile(f));
427
+
370
428
  return scan;
371
429
  }
372
430
 
@@ -19,6 +19,13 @@ import { validateArchitecture } from '../validators/architecture.mjs';
19
19
  import { validateFreshness } from '../validators/freshness.mjs';
20
20
  import { validateTraceability } from '../validators/traceability.mjs';
21
21
  import { validateDocsDiff } from '../validators/docs-diff.mjs';
22
+ import { validateMetadataSync } from '../validators/metadata-sync.mjs';
23
+ import { validateMetricsConsistency } from '../validators/metrics-consistency.mjs';
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';
22
29
 
23
30
  /**
24
31
  * Internal guard — returns structured data, no console output, no process.exit.
@@ -52,6 +59,13 @@ export function runGuardInternal(projectDir, config) {
52
59
  }},
53
60
  { key: 'traceability', name: 'Traceability', fn: () => validateTraceability(projectDir, config) },
54
61
  { key: 'docsDiff', name: 'Docs-Diff', fn: () => validateDocsDiff(projectDir, config) },
62
+ { key: 'metadataSync', name: 'Metadata-Sync', fn: () => validateMetadataSync(projectDir, config) },
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) },
68
+ // Metrics-Consistency runs post-loop (needs guard results)
55
69
  ];
56
70
 
57
71
  for (const { key, name, fn } of validatorMap) {
@@ -84,6 +98,21 @@ export function runGuardInternal(projectDir, config) {
84
98
  }
85
99
  }
86
100
 
101
+ // ── Metrics-Consistency runs AFTER all other validators (needs their results) ──
102
+ if (validators.metricsConsistency !== false) {
103
+ try {
104
+ const result = validateMetricsConsistency(projectDir, config, results);
105
+ const hasErrors = result.errors.length > 0;
106
+ const hasWarnings = result.warnings.length > 0;
107
+ const status = hasErrors ? 'fail' : hasWarnings ? 'warn' : 'pass';
108
+ const ratio = result.total > 0 ? result.passed / result.total : 1;
109
+ const quality = hasErrors ? 'LOW' : hasWarnings ? 'MEDIUM' : ratio >= 0.9 ? 'HIGH' : 'MEDIUM';
110
+ results.push({ ...result, name: 'Metrics-Consistency', key: 'metricsConsistency', status, quality });
111
+ } catch (err) {
112
+ results.push({ name: 'Metrics-Consistency', key: 'metricsConsistency', status: 'fail', quality: 'LOW', errors: [err.message], warnings: [], passed: 0, total: 1 });
113
+ }
114
+ }
115
+
87
116
  const activeResults = results.filter(r => r.status !== 'skipped');
88
117
  const totalErrors = activeResults.reduce((sum, r) => sum + r.errors.length, 0);
89
118
  const totalWarnings = activeResults.reduce((sum, r) => sum + r.warnings.length, 0);
@@ -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
+ }
@@ -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.`);