docguard-cli 0.5.2 → 0.7.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.
@@ -52,7 +52,7 @@ export function runGuardInternal(projectDir, config) {
52
52
 
53
53
  for (const { key, name, fn } of validatorMap) {
54
54
  if (validators[key] === false) {
55
- results.push({ name, key, status: 'skipped', errors: [], warnings: [], passed: 0, total: 0 });
55
+ results.push({ name, key, status: 'skipped', quality: null, errors: [], warnings: [], passed: 0, total: 0 });
56
56
  continue;
57
57
  }
58
58
 
@@ -61,9 +61,22 @@ export function runGuardInternal(projectDir, config) {
61
61
  const hasErrors = result.errors.length > 0;
62
62
  const hasWarnings = result.warnings.length > 0;
63
63
  const status = hasErrors ? 'fail' : hasWarnings ? 'warn' : 'pass';
64
- results.push({ ...result, name, key, status });
64
+
65
+ // Quality label: HIGH/MEDIUM/LOW (inspired by CJE quality stratification, Lopez et al. TRACE 2026)
66
+ let quality;
67
+ if (hasErrors) {
68
+ quality = 'LOW';
69
+ } else if (hasWarnings) {
70
+ quality = 'MEDIUM';
71
+ } else {
72
+ // Pass — check coverage ratio for HIGH vs MEDIUM
73
+ const ratio = result.total > 0 ? result.passed / result.total : 1;
74
+ quality = ratio >= 0.9 ? 'HIGH' : 'MEDIUM';
75
+ }
76
+
77
+ results.push({ ...result, name, key, status, quality });
65
78
  } catch (err) {
66
- results.push({ name, key, status: 'fail', errors: [err.message], warnings: [], passed: 0, total: 1 });
79
+ results.push({ name, key, status: 'fail', quality: 'LOW', errors: [err.message], warnings: [], passed: 0, total: 1 });
67
80
  }
68
81
  }
69
82
 
@@ -114,12 +127,16 @@ export function runGuard(projectDir, config, flags) {
114
127
  continue;
115
128
  }
116
129
 
130
+ // Quality label badge
131
+ const qColor = v.quality === 'HIGH' ? c.green : v.quality === 'MEDIUM' ? c.yellow : c.red;
132
+ const qBadge = `${qColor}[${v.quality}]${c.reset}`;
133
+
117
134
  if (v.status === 'pass') {
118
- console.log(` ${c.green}✅ ${v.name}${c.reset}${c.dim} ${v.passed}/${v.total} checks passed${c.reset}`);
135
+ console.log(` ${c.green}✅ ${v.name}${c.reset} ${qBadge}${c.dim} ${v.passed}/${v.total} checks passed${c.reset}`);
119
136
  } else if (v.status === 'fail') {
120
- console.log(` ${c.red}❌ ${v.name}${c.reset}${c.dim} ${v.passed}/${v.total} checks passed${c.reset}`);
137
+ console.log(` ${c.red}❌ ${v.name}${c.reset} ${qBadge}${c.dim} ${v.passed}/${v.total} checks passed${c.reset}`);
121
138
  } else {
122
- console.log(` ${c.yellow}⚠️ ${v.name}${c.reset}${c.dim} ${v.passed}/${v.total} checks passed${c.reset}`);
139
+ console.log(` ${c.yellow}⚠️ ${v.name}${c.reset} ${qBadge}${c.dim} ${v.passed}/${v.total} checks passed${c.reset}`);
123
140
  }
124
141
 
125
142
  if (flags.verbose || v.status === 'fail') {
@@ -0,0 +1,246 @@
1
+ /**
2
+ * DocGuard Publish Command
3
+ * Scaffolds documentation publishing config for external doc platforms.
4
+ * Currently supports: Mintlify
5
+ *
6
+ * Usage: docguard publish --platform mintlify [--dir .]
7
+ */
8
+
9
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
10
+ import { resolve, basename } from 'node:path';
11
+ import { c } from '../docguard.mjs';
12
+
13
+ const SUPPORTED_PLATFORMS = ['mintlify'];
14
+
15
+ export function runPublish(projectDir, config, flags) {
16
+ const platform = flags.platform || 'mintlify';
17
+
18
+ if (!SUPPORTED_PLATFORMS.includes(platform)) {
19
+ console.error(`${c.red}✗ Unsupported platform: ${platform}${c.reset}`);
20
+ console.log(` Supported: ${SUPPORTED_PLATFORMS.join(', ')}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ console.log(`${c.bold}📚 DocGuard Publish — ${platform}${c.reset}`);
25
+ console.log(`${c.dim} Scaffolding ${platform} docs from canonical documentation...${c.reset}\n`);
26
+
27
+ switch (platform) {
28
+ case 'mintlify':
29
+ scaffoldMintlify(projectDir, config, flags);
30
+ break;
31
+ }
32
+ }
33
+
34
+ function scaffoldMintlify(dir, config, flags) {
35
+ const docsDir = resolve(dir, 'docs');
36
+
37
+ // Check for existing Mintlify setup
38
+ if (existsSync(resolve(dir, 'docs.json')) && !flags.force) {
39
+ console.log(` ${c.yellow}⚠️ docs.json already exists.${c.reset} Use --force to overwrite.`);
40
+ return;
41
+ }
42
+
43
+ // Create docs directory
44
+ if (!existsSync(docsDir)) {
45
+ mkdirSync(docsDir, { recursive: true });
46
+ }
47
+
48
+ let created = 0;
49
+
50
+ // ── 1. Generate docs.json (Mintlify v2 config) ──
51
+ const docsJson = {
52
+ "$schema": "https://mintlify.com/docs.json",
53
+ name: config.projectName || basename(dir),
54
+ logo: {
55
+ dark: "/logo/dark.svg",
56
+ light: "/logo/light.svg",
57
+ },
58
+ favicon: "/favicon.svg",
59
+ colors: {
60
+ primary: "#0D9373",
61
+ light: "#07C983",
62
+ dark: "#0D9373",
63
+ },
64
+ topbarLinks: [
65
+ {
66
+ name: "GitHub",
67
+ url: `https://github.com/${config.repository || 'your-org/your-repo'}`,
68
+ },
69
+ ],
70
+ topbarCtaButton: {
71
+ name: "Get Started",
72
+ url: "/quickstart",
73
+ },
74
+ tabs: [
75
+ {
76
+ name: "Architecture",
77
+ url: "architecture",
78
+ },
79
+ {
80
+ name: "API Reference",
81
+ url: "api-reference",
82
+ },
83
+ ],
84
+ navigation: buildMintlifyNavigation(dir),
85
+ footerSocials: {
86
+ github: `https://github.com/${config.repository || 'your-org/your-repo'}`,
87
+ },
88
+ };
89
+
90
+ writeFileSync(resolve(dir, 'docs.json'), JSON.stringify(docsJson, null, 2), 'utf-8');
91
+ console.log(` ${c.green}✅ docs.json${c.reset} (Mintlify v2 config)`);
92
+ created++;
93
+
94
+ // ── 2. Generate introduction.mdx ──
95
+ const readmePath = resolve(dir, 'README.md');
96
+ let readmeContent = '';
97
+ if (existsSync(readmePath)) {
98
+ readmeContent = readFileSync(readmePath, 'utf-8');
99
+ // Extract first paragraph for description
100
+ const firstPara = readmeContent.split('\n\n').slice(1, 3).join('\n\n');
101
+ readmeContent = firstPara;
102
+ }
103
+
104
+ const introContent = `---
105
+ title: Introduction
106
+ description: "${config.projectName} documentation"
107
+ ---
108
+
109
+ # ${config.projectName}
110
+
111
+ ${readmeContent || `Welcome to the ${config.projectName} documentation.`}
112
+
113
+ ## Quick Links
114
+
115
+ <CardGroup cols={2}>
116
+ <Card title="Quick Start" icon="rocket" href="/quickstart">
117
+ Get up and running in 5 minutes
118
+ </Card>
119
+ <Card title="Architecture" icon="building" href="/architecture">
120
+ Understand the system design
121
+ </Card>
122
+ <Card title="API Reference" icon="code" href="/api-reference">
123
+ Explore the API endpoints
124
+ </Card>
125
+ <Card title="Data Model" icon="database" href="/data-model">
126
+ Learn about the data structure
127
+ </Card>
128
+ </CardGroup>
129
+ `;
130
+
131
+ writeFileSync(resolve(docsDir, 'introduction.mdx'), introContent, 'utf-8');
132
+ console.log(` ${c.green}✅ docs/introduction.mdx${c.reset}`);
133
+ created++;
134
+
135
+ // ── 3. Generate quickstart.mdx ──
136
+ const quickstartContent = `---
137
+ title: Quick Start
138
+ description: "Get started with ${config.projectName}"
139
+ ---
140
+
141
+ # Quick Start
142
+
143
+ ## Prerequisites
144
+
145
+ - Node.js 18+
146
+ - npm or pnpm
147
+
148
+ ## Installation
149
+
150
+ \`\`\`bash
151
+ npm install
152
+ \`\`\`
153
+
154
+ ## Setup
155
+
156
+ \`\`\`bash
157
+ cp .env.example .env.local
158
+ # Fill in environment variables
159
+ npm run dev
160
+ \`\`\`
161
+
162
+ ## Verify
163
+
164
+ \`\`\`bash
165
+ npx docguard-cli guard # Check documentation compliance
166
+ npx docguard-cli score # View CDD maturity score
167
+ \`\`\`
168
+ `;
169
+
170
+ writeFileSync(resolve(docsDir, 'quickstart.mdx'), quickstartContent, 'utf-8');
171
+ console.log(` ${c.green}✅ docs/quickstart.mdx${c.reset}`);
172
+ created++;
173
+
174
+ // ── 4. Map canonical docs to Mintlify pages ──
175
+ const canonicalDir = resolve(dir, 'docs-canonical');
176
+ const mappings = [
177
+ { source: 'ARCHITECTURE.md', target: 'architecture.mdx', title: 'Architecture' },
178
+ { source: 'API-REFERENCE.md', target: 'api-reference.mdx', title: 'API Reference' },
179
+ { source: 'DATA-MODEL.md', target: 'data-model.mdx', title: 'Data Model' },
180
+ { source: 'SECURITY.md', target: 'security.mdx', title: 'Security' },
181
+ { source: 'ENVIRONMENT.md', target: 'environment.mdx', title: 'Environment' },
182
+ { source: 'TEST-SPEC.md', target: 'test-spec.mdx', title: 'Test Specification' },
183
+ ];
184
+
185
+ for (const mapping of mappings) {
186
+ const sourcePath = resolve(canonicalDir, mapping.source);
187
+ if (existsSync(sourcePath)) {
188
+ let content = readFileSync(sourcePath, 'utf-8');
189
+
190
+ // Add Mintlify frontmatter
191
+ const frontmatter = `---\ntitle: "${mapping.title}"\ndescription: "${config.projectName} ${mapping.title.toLowerCase()}"\n---\n\n`;
192
+
193
+ // Remove docguard metadata comments
194
+ content = content.replace(/<!--\s*docguard:\w+\s+[^>]+\s*-->\n?/g, '');
195
+ content = content.replace(/> \*\*Auto-generated by DocGuard\.\*\*[^\n]*\n?/g, '');
196
+
197
+ writeFileSync(resolve(docsDir, mapping.target), frontmatter + content, 'utf-8');
198
+ console.log(` ${c.green}✅ docs/${mapping.target}${c.reset} ← ${mapping.source}`);
199
+ created++;
200
+ }
201
+ }
202
+
203
+ // ── Summary ──
204
+ console.log(`\n${c.bold} ─────────────────────────────────────────${c.reset}`);
205
+ console.log(` ${c.green}Created: ${created} files${c.reset}`);
206
+ console.log(`\n ${c.bold}Next steps:${c.reset}`);
207
+ console.log(` ${c.cyan}1.${c.reset} Install Mintlify: ${c.dim}npm install -g mintlify${c.reset}`);
208
+ console.log(` ${c.cyan}2.${c.reset} Preview locally: ${c.dim}mintlify dev${c.reset}`);
209
+ console.log(` ${c.cyan}3.${c.reset} Push to GitHub → auto-deploys on Mintlify${c.reset}`);
210
+ console.log(` ${c.dim}\n Mintlify is free for open-source projects.${c.reset}`);
211
+ console.log(` ${c.dim} Docs: https://mintlify.com/docs${c.reset}\n`);
212
+ }
213
+
214
+ function buildMintlifyNavigation(dir) {
215
+ const nav = [
216
+ {
217
+ group: "Getting Started",
218
+ pages: ["introduction", "quickstart"],
219
+ },
220
+ ];
221
+
222
+ // Add architecture group if docs exist
223
+ const canonicalDir = resolve(dir, 'docs-canonical');
224
+ const architecturePages = [];
225
+ if (existsSync(resolve(canonicalDir, 'ARCHITECTURE.md'))) architecturePages.push("architecture");
226
+ if (existsSync(resolve(canonicalDir, 'DATA-MODEL.md'))) architecturePages.push("data-model");
227
+ if (existsSync(resolve(canonicalDir, 'SECURITY.md'))) architecturePages.push("security");
228
+ if (architecturePages.length > 0) {
229
+ nav.push({ group: "Architecture", pages: architecturePages });
230
+ }
231
+
232
+ // Add operations group
233
+ const opsPages = [];
234
+ if (existsSync(resolve(canonicalDir, 'ENVIRONMENT.md'))) opsPages.push("environment");
235
+ if (existsSync(resolve(canonicalDir, 'TEST-SPEC.md'))) opsPages.push("test-spec");
236
+ if (opsPages.length > 0) {
237
+ nav.push({ group: "Operations", pages: opsPages });
238
+ }
239
+
240
+ // Add API reference
241
+ if (existsSync(resolve(canonicalDir, 'API-REFERENCE.md'))) {
242
+ nav.push({ group: "API Reference", pages: ["api-reference"] });
243
+ }
244
+
245
+ return nav;
246
+ }
@@ -99,6 +99,37 @@ export function runScore(projectDir, config, flags) {
99
99
  console.log(` Tax-to-value ratio: ${taxColor}${c.bold}${tax.level}${c.reset}`);
100
100
  console.log(` ${c.dim}${tax.recommendation}${c.reset}\n`);
101
101
  }
102
+
103
+ // ── Multi-Signal Breakdown (--signals flag) ──
104
+ // Inspired by CJE multi-signal composite scoring (Lopez et al., TRACE, IEEE TMLCN 2026)
105
+ if (flags.signals) {
106
+ console.log(` ${c.bold}📡 Multi-Signal Quality Breakdown${c.reset}`);
107
+ console.log(` ${c.dim}─────────────────────────────────${c.reset}`);
108
+
109
+ const signals = [
110
+ { name: 'Structure', score: scores.structure, weight: WEIGHTS.structure, description: 'Required files exist' },
111
+ { name: 'Doc Quality', score: scores.docQuality, weight: WEIGHTS.docQuality, description: 'Docs have required sections + content' },
112
+ { name: 'Testing', score: scores.testing, weight: WEIGHTS.testing, description: 'Test spec alignment' },
113
+ { name: 'Security', score: scores.security, weight: WEIGHTS.security, description: 'No hardcoded secrets, .gitignore' },
114
+ { name: 'Environment', score: scores.environment, weight: WEIGHTS.environment, description: 'Env docs, .env.example' },
115
+ { name: 'Drift', score: scores.drift, weight: WEIGHTS.drift, description: 'Drift tracking discipline' },
116
+ { name: 'Changelog', score: scores.changelog, weight: WEIGHTS.changelog, description: 'Changelog maintenance' },
117
+ { name: 'Architecture', score: scores.architecture, weight: WEIGHTS.architecture, description: 'Layer boundary compliance' },
118
+ ];
119
+
120
+ for (const sig of signals) {
121
+ const weighted = Math.round((sig.score / 100) * sig.weight);
122
+ const quality = sig.score >= 90 ? 'HIGH' : sig.score >= 50 ? 'MEDIUM' : 'LOW';
123
+ const qColor = quality === 'HIGH' ? c.green : quality === 'MEDIUM' ? c.yellow : c.red;
124
+ const bar = renderBar(sig.score);
125
+
126
+ console.log(` ${bar} ${qColor}[${quality}]${c.reset} ${sig.name.padEnd(14)} ${sig.score}% → ${c.bold}${weighted}/${sig.weight}${c.reset} pts ${c.dim}${sig.description}${c.reset}`);
127
+ }
128
+
129
+ console.log(`\n ${c.dim}Composite: Σ(signal_score × weight) = ${totalScore}/100${c.reset}`);
130
+ console.log(` ${c.dim}Quality labels: HIGH (≥90%), MEDIUM (50-89%), LOW (<50%)${c.reset}`);
131
+ console.log(` ${c.dim}Methodology: CJE multi-signal composite (Lopez et al., TRACE, IEEE TMLCN 2026)${c.reset}\n`);
132
+ }
102
133
  }
103
134
 
104
135
  /**
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Trace Command — Generate a requirements traceability matrix
3
+ * Maps canonical docs ↔ source code ↔ tests → produces a traceability report.
4
+ *
5
+ * Inspired by requirements traceability in Lopez et al., AITPG (IEEE TSE 2026)
6
+ * and ISO/IEC/IEEE 29119 traceability requirements.
7
+ */
8
+
9
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
10
+ import { resolve, join, extname, basename, relative } from 'node:path';
11
+ import { c } from '../docguard.mjs';
12
+
13
+ const IGNORE_DIRS = new Set([
14
+ 'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
15
+ '.cache', '__pycache__', '.venv', 'vendor', '.turbo', '.vercel',
16
+ '.amplify-hosting', '.serverless',
17
+ ]);
18
+
19
+ const CODE_EXTENSIONS = new Set([
20
+ '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
21
+ '.py', '.java', '.go', '.rs', '.rb', '.php', '.cs',
22
+ ]);
23
+
24
+ const TEST_PATTERNS = [
25
+ /\.test\.[jt]sx?$/,
26
+ /\.spec\.[jt]sx?$/,
27
+ /test_.*\.py$/,
28
+ /_test\.go$/,
29
+ /Test\.java$/,
30
+ ];
31
+
32
+ /**
33
+ * Mapping of canonical documents to the code/config artifacts they trace to.
34
+ * Each entry defines what source patterns prove coverage of that canonical doc.
35
+ */
36
+ const TRACE_MAP = {
37
+ 'ARCHITECTURE.md': {
38
+ standard: 'arc42 / C4 Model',
39
+ sourcePatterns: [
40
+ { label: 'Entry points', glob: /^(index|main|app|server)\.[jt]sx?$/ },
41
+ { label: 'Config files', glob: /^(package\.json|tsconfig.*|next\.config|vite\.config)/ },
42
+ { label: 'Route handlers', glob: /(routes?|api|pages|app)\// },
43
+ ],
44
+ },
45
+ 'DATA-MODEL.md': {
46
+ standard: 'C4 Component / ER (Chen)',
47
+ sourcePatterns: [
48
+ { label: 'Schema definitions', glob: /(schema|model|entity|migration|prisma)/i },
49
+ { label: 'Type definitions', glob: /types?\.[jt]sx?$/ },
50
+ { label: 'Database configs', glob: /(drizzle|knex|sequelize|typeorm)/i },
51
+ ],
52
+ },
53
+ 'TEST-SPEC.md': {
54
+ standard: 'ISO/IEC/IEEE 29119-3',
55
+ sourcePatterns: [
56
+ { label: 'Test files', glob: /\.(test|spec)\.[jt]sx?$/ },
57
+ { label: 'Test config', glob: /(jest|vitest|playwright|cypress)\.config/ },
58
+ { label: 'E2E tests', glob: /(e2e|integration)\// },
59
+ ],
60
+ },
61
+ 'SECURITY.md': {
62
+ standard: 'OWASP ASVS v4.0',
63
+ sourcePatterns: [
64
+ { label: 'Auth modules', glob: /(auth|login|session|jwt|oauth|middleware)/i },
65
+ { label: 'Secret configs', glob: /\.(env|env\.example|env\.local)$/ },
66
+ { label: 'Gitignore', glob: /^\.gitignore$/ },
67
+ ],
68
+ },
69
+ 'ENVIRONMENT.md': {
70
+ standard: '12-Factor App',
71
+ sourcePatterns: [
72
+ { label: 'Env files', glob: /\.env/ },
73
+ { label: 'Docker configs', glob: /(Dockerfile|docker-compose|\.dockerignore)/ },
74
+ { label: 'CI/CD configs', glob: /\.(github|gitlab-ci|circleci)/ },
75
+ ],
76
+ },
77
+ 'API-REFERENCE.md': {
78
+ standard: 'OpenAPI 3.1',
79
+ sourcePatterns: [
80
+ { label: 'Route handlers', glob: /(routes?|controllers?|handlers?)\// },
81
+ { label: 'OpenAPI spec', glob: /(openapi|swagger)\.(json|ya?ml)/ },
82
+ { label: 'API middleware', glob: /middleware\// },
83
+ ],
84
+ },
85
+ };
86
+
87
+ export function runTrace(projectDir, config, flags) {
88
+ console.log(`${c.bold}🔗 DocGuard Trace — ${config.projectName}${c.reset}`);
89
+ console.log(`${c.dim} Directory: ${projectDir}${c.reset}`);
90
+ console.log(`${c.dim} Generating requirements traceability matrix...${c.reset}\n`);
91
+
92
+ // ── 1. Inventory canonical docs ──
93
+ const docsDir = resolve(projectDir, 'docs-canonical');
94
+ const canonicalDocs = [];
95
+ if (existsSync(docsDir)) {
96
+ for (const f of readdirSync(docsDir)) {
97
+ if (f.endsWith('.md')) canonicalDocs.push(f);
98
+ }
99
+ }
100
+
101
+ // ── 2. Scan project files ──
102
+ const projectFiles = [];
103
+ scanDir(projectDir, projectDir, projectFiles);
104
+
105
+ // ── 3. Build traceability matrix ──
106
+ const matrix = [];
107
+
108
+ for (const [docName, traceInfo] of Object.entries(TRACE_MAP)) {
109
+ const docPath = resolve(docsDir, docName);
110
+ const docExists = existsSync(docPath);
111
+ let lastModified = null;
112
+ let docSize = 0;
113
+
114
+ if (docExists) {
115
+ const stat = statSync(docPath);
116
+ lastModified = stat.mtime.toISOString().split('T')[0];
117
+ docSize = stat.size;
118
+ }
119
+
120
+ // Find matching source files for each pattern
121
+ const traces = [];
122
+ for (const pattern of traceInfo.sourcePatterns) {
123
+ const matches = projectFiles.filter(f => pattern.glob.test(f));
124
+ traces.push({
125
+ label: pattern.label,
126
+ matchCount: matches.length,
127
+ files: matches.slice(0, 5), // Cap at 5 for display
128
+ hasMore: matches.length > 5,
129
+ });
130
+ }
131
+
132
+ // Find test coverage (files that test code related to this doc)
133
+ const relatedTests = findRelatedTests(projectFiles, traceInfo.sourcePatterns);
134
+
135
+ // Calculate coverage signal
136
+ const totalSources = traces.reduce((sum, t) => sum + t.matchCount, 0);
137
+ const coverageSignal = !docExists ? 'MISSING'
138
+ : totalSources === 0 ? 'UNLINKED'
139
+ : relatedTests.length > 0 ? 'TRACED'
140
+ : 'PARTIAL';
141
+
142
+ matrix.push({
143
+ document: docName,
144
+ standard: traceInfo.standard,
145
+ exists: docExists,
146
+ lastModified,
147
+ docSize,
148
+ traces,
149
+ relatedTests,
150
+ totalSources,
151
+ coverageSignal,
152
+ });
153
+ }
154
+
155
+ // ── 4. Output ──
156
+ if (flags.format === 'json') {
157
+ outputJSON(config.projectName, matrix);
158
+ } else {
159
+ outputText(config.projectName, matrix, canonicalDocs);
160
+ }
161
+ }
162
+
163
+ function outputJSON(projectName, matrix) {
164
+ const result = {
165
+ project: projectName,
166
+ traceability: matrix.map(m => ({
167
+ document: m.document,
168
+ standard: m.standard,
169
+ exists: m.exists,
170
+ lastModified: m.lastModified,
171
+ coverageSignal: m.coverageSignal,
172
+ sources: m.totalSources,
173
+ tests: m.relatedTests.length,
174
+ traces: m.traces,
175
+ })),
176
+ summary: {
177
+ total: matrix.length,
178
+ traced: matrix.filter(m => m.coverageSignal === 'TRACED').length,
179
+ partial: matrix.filter(m => m.coverageSignal === 'PARTIAL').length,
180
+ unlinked: matrix.filter(m => m.coverageSignal === 'UNLINKED').length,
181
+ missing: matrix.filter(m => m.coverageSignal === 'MISSING').length,
182
+ },
183
+ timestamp: new Date().toISOString(),
184
+ };
185
+ console.log(JSON.stringify(result, null, 2));
186
+ }
187
+
188
+ function outputText(projectName, matrix, canonicalDocs) {
189
+ // Header table
190
+ console.log(` ${c.bold}Traceability Matrix${c.reset}\n`);
191
+ console.log(` ${c.dim}${'Document'.padEnd(22)} ${'Standard'.padEnd(28)} ${'Status'.padEnd(10)} ${'Sources'.padEnd(9)} ${'Tests'.padEnd(7)} ${'Last Modified'}${c.reset}`);
192
+ console.log(` ${c.dim}${'─'.repeat(22)} ${'─'.repeat(28)} ${'─'.repeat(10)} ${'─'.repeat(9)} ${'─'.repeat(7)} ${'─'.repeat(14)}${c.reset}`);
193
+
194
+ for (const entry of matrix) {
195
+ const statusColor = entry.coverageSignal === 'TRACED' ? c.green
196
+ : entry.coverageSignal === 'PARTIAL' ? c.yellow
197
+ : entry.coverageSignal === 'UNLINKED' ? c.yellow
198
+ : c.red;
199
+ const statusIcon = entry.coverageSignal === 'TRACED' ? '✅'
200
+ : entry.coverageSignal === 'PARTIAL' ? '⚠️ '
201
+ : entry.coverageSignal === 'UNLINKED' ? '🔗'
202
+ : '❌';
203
+
204
+ console.log(` ${statusIcon} ${entry.document.padEnd(19)} ${c.dim}${entry.standard.padEnd(28)}${c.reset} ${statusColor}${entry.coverageSignal.padEnd(10)}${c.reset} ${String(entry.totalSources).padEnd(9)} ${String(entry.relatedTests.length).padEnd(7)} ${entry.lastModified || c.dim + 'n/a' + c.reset}`);
205
+ }
206
+
207
+ // Detailed traces (verbose)
208
+ console.log(`\n ${c.bold}Detailed Traces${c.reset}\n`);
209
+
210
+ for (const entry of matrix) {
211
+ if (!entry.exists) {
212
+ console.log(` ${c.red}❌ ${entry.document}${c.reset} — ${c.dim}Document not found. Run \`docguard generate\` to create.${c.reset}`);
213
+ continue;
214
+ }
215
+
216
+ console.log(` ${c.bold}📄 ${entry.document}${c.reset} ${c.dim}(${entry.standard})${c.reset}`);
217
+
218
+ for (const trace of entry.traces) {
219
+ const icon = trace.matchCount > 0 ? `${c.green}✓${c.reset}` : `${c.dim}○${c.reset}`;
220
+ console.log(` ${icon} ${trace.label}: ${trace.matchCount} file(s)`);
221
+ if (trace.matchCount > 0 && trace.files.length > 0) {
222
+ for (const f of trace.files) {
223
+ console.log(` ${c.dim}→ ${f}${c.reset}`);
224
+ }
225
+ if (trace.hasMore) {
226
+ console.log(` ${c.dim} ... and ${trace.matchCount - 5} more${c.reset}`);
227
+ }
228
+ }
229
+ }
230
+
231
+ if (entry.relatedTests.length > 0) {
232
+ console.log(` ${c.green}✓${c.reset} Test coverage: ${entry.relatedTests.length} test file(s)`);
233
+ for (const t of entry.relatedTests.slice(0, 3)) {
234
+ console.log(` ${c.dim}→ ${t}${c.reset}`);
235
+ }
236
+ if (entry.relatedTests.length > 3) {
237
+ console.log(` ${c.dim} ... and ${entry.relatedTests.length - 3} more${c.reset}`);
238
+ }
239
+ } else {
240
+ console.log(` ${c.yellow}○${c.reset} ${c.dim}No related test files found${c.reset}`);
241
+ }
242
+ console.log('');
243
+ }
244
+
245
+ // Summary
246
+ const traced = matrix.filter(m => m.coverageSignal === 'TRACED').length;
247
+ const partial = matrix.filter(m => m.coverageSignal === 'PARTIAL').length;
248
+ const unlinked = matrix.filter(m => m.coverageSignal === 'UNLINKED').length;
249
+ const missing = matrix.filter(m => m.coverageSignal === 'MISSING').length;
250
+
251
+ console.log(` ${c.bold}─────────────────────────────────────${c.reset}`);
252
+ console.log(` ${c.green}Traced: ${traced}${c.reset} ${c.yellow}Partial: ${partial}${c.reset} ${c.yellow}Unlinked: ${unlinked}${c.reset} ${c.red}Missing: ${missing}${c.reset}`);
253
+ console.log(` ${c.dim}Total: ${matrix.length} canonical documents evaluated${c.reset}`);
254
+
255
+ if (missing > 0 || unlinked > 0) {
256
+ console.log(`\n ${c.dim}Run ${c.cyan}docguard generate${c.dim} to create missing docs.${c.reset}`);
257
+ console.log(` ${c.dim}Run ${c.cyan}docguard diagnose${c.dim} to fix coverage gaps.${c.reset}`);
258
+ }
259
+
260
+ console.log(`\n ${c.dim}Traceability methodology: ISO/IEC/IEEE 29119 (Lopez et al., AITPG, IEEE TSE 2026)${c.reset}\n`);
261
+ }
262
+
263
+ // ── Helpers ────────────────────────────────────────────────────────────────
264
+
265
+ function scanDir(rootDir, dir, files) {
266
+ let entries;
267
+ try {
268
+ entries = readdirSync(dir);
269
+ } catch { return; }
270
+
271
+ for (const entry of entries) {
272
+ if (IGNORE_DIRS.has(entry)) continue;
273
+ if (entry.startsWith('.') && entry !== '.env' && entry !== '.env.example'
274
+ && entry !== '.gitignore' && !entry.startsWith('.github')) continue;
275
+
276
+ const full = join(dir, entry);
277
+ let stat;
278
+ try { stat = statSync(full); } catch { continue; }
279
+
280
+ if (stat.isDirectory()) {
281
+ scanDir(rootDir, full, files);
282
+ } else {
283
+ files.push(relative(rootDir, full));
284
+ }
285
+ }
286
+ }
287
+
288
+ function findRelatedTests(projectFiles, sourcePatterns) {
289
+ // Find test files that might cover the source patterns
290
+ const testFiles = projectFiles.filter(f => TEST_PATTERNS.some(p => p.test(f)));
291
+
292
+ // Match tests to source patterns by directory/name proximity
293
+ const relatedTests = new Set();
294
+
295
+ for (const pattern of sourcePatterns) {
296
+ const sourceFiles = projectFiles.filter(f => pattern.glob.test(f));
297
+ for (const src of sourceFiles) {
298
+ const srcBase = basename(src).replace(/\.[^.]+$/, '');
299
+ const srcDir = src.split('/')[0];
300
+
301
+ for (const test of testFiles) {
302
+ // Match by name similarity or directory proximity
303
+ if (test.includes(srcBase) || test.includes(srcDir)) {
304
+ relatedTests.add(test);
305
+ }
306
+ }
307
+ }
308
+ }
309
+
310
+ return [...relatedTests];
311
+ }