docguard-cli 0.6.0 → 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.
package/PHILOSOPHY.md CHANGED
@@ -143,6 +143,28 @@ CDD is designed for progressive adoption:
143
143
 
144
144
  ---
145
145
 
146
+ ## Academic Foundations
147
+
148
+ CDD is a practitioner methodology, but its core patterns align with peer-reviewed research in AI-driven documentation and quality evaluation:
149
+
150
+ ### The Three-Stage Pipeline
151
+
152
+ DocGuard's architecture follows a **generate → validate → evaluate** pipeline inspired by the AITPG framework (Lopez et al., IEEE TSE 2026), which demonstrated that multi-agent debate over RAG-grounded standards produces 32% more comprehensive documentation while maintaining semantic alignment with expert references.
153
+
154
+ ### Calibrated Quality Evaluation
155
+
156
+ DocGuard's quality scoring (HIGH/MEDIUM/LOW labels, multi-signal composite scores) adapts the Calibrated Judge Evaluation (CJE) framework from TRACE (Lopez et al., IEEE TMLCN 2026), which showed that weighted multi-signal scoring with confidence intervals provides actionable quality gating for enterprise deployment.
157
+
158
+ ### Standards-Grounded Generation
159
+
160
+ Both AITPG and TRACE demonstrate that grounding AI-generated content in vectorized standards (ISO 29119, 3GPP) dramatically improves output quality. DocGuard applies this principle by mapping each canonical document to its relevant industry standard (arc42, C4, OWASP ASVS, ISO 29119, OpenAPI 3.1, 12-Factor App).
161
+
162
+ > **Full citations**: See [CONTRIBUTING.md — Research & Academic Credits](CONTRIBUTING.md#research--academic-credits)
163
+ >
164
+ > **Lead researcher**: [Martin Manuel Lopez](https://github.com/martinmanuel9) · [ORCID 0009-0002-7652-2385](https://orcid.org/0009-0002-7652-2385), University of Arizona
165
+
166
+ ---
167
+
146
168
  ## License
147
169
 
148
170
  This philosophy document and the CDD methodology are released under the [MIT License](https://opensource.org/licenses/MIT).
package/README.md CHANGED
@@ -322,6 +322,19 @@ We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
322
322
 
323
323
  ---
324
324
 
325
+ ## Research Credits
326
+
327
+ DocGuard's quality evaluation and documentation generation patterns are informed by peer-reviewed research from the University of Arizona and the Joint Interoperability Test Command (JITC), U.S. Department of Defense:
328
+
329
+ - **AITPG** — AI-driven Test Plan Generator using Multi-Agent Debate and RAG ([Lopez et al., IEEE TSE 2026](Research/AITPG.pdf))
330
+ - **TRACE** — Telecom Root Cause Analysis through Calibrated Explainability ([Lopez et al., IEEE TMLCN 2026](Research/TRACE.pdf))
331
+
332
+ Lead researcher: **[Martin Manuel Lopez](https://github.com/martinmanuel9)** · [ORCID 0009-0002-7652-2385](https://orcid.org/0009-0002-7652-2385)
333
+
334
+ See [CONTRIBUTING.md](CONTRIBUTING.md#research--academic-credits) for full citations and concept attributions.
335
+
336
+ ---
337
+
325
338
  ## License
326
339
 
327
340
  [MIT](LICENSE) — Free to use, modify, and distribute.
@@ -16,8 +16,9 @@
16
16
  import { c } from '../docguard.mjs';
17
17
  import { runGuardInternal } from './guard.mjs';
18
18
  import { runScoreInternal } from './score.mjs';
19
- import { existsSync, readFileSync } from 'node:fs';
19
+ import { existsSync, readFileSync, mkdirSync } from 'node:fs';
20
20
  import { resolve } from 'node:path';
21
+ import { execSync } from 'node:child_process';
21
22
 
22
23
  // Map validator failures to the right fix --doc target
23
24
  const VALIDATOR_TO_DOC = {
@@ -39,62 +40,141 @@ const FIX_INSTRUCTIONS = {
39
40
  action: 'Create missing files',
40
41
  command: 'docguard init',
41
42
  description: 'Run init to create missing documentation templates.',
43
+ autoFixable: true,
42
44
  },
43
45
  'Doc Sections': {
44
46
  action: 'Fill document sections',
45
47
  command: 'docguard fix --doc',
46
48
  description: 'Documents exist but have missing or placeholder sections. Use fix --doc to generate AI content prompts.',
49
+ autoFixable: false,
47
50
  },
48
51
  'Docs-Sync': {
49
52
  action: 'Sync documentation references',
50
53
  command: 'docguard fix --doc architecture',
51
54
  description: 'Documentation references are out of sync with code. Review and update component maps.',
55
+ autoFixable: false,
52
56
  },
53
57
  'Drift': {
54
58
  action: 'Update DRIFT-LOG.md',
55
59
  description: 'Code deviates from canonical docs without logged reasons. Add DRIFT entries or update the canonical docs.',
60
+ autoFixable: false,
56
61
  },
57
62
  'Changelog': {
58
63
  action: 'Update CHANGELOG.md',
59
64
  description: 'CHANGELOG.md is missing or has no [Unreleased] section. Add recent changes.',
65
+ autoFixable: false,
60
66
  },
61
67
  'Test-Spec': {
62
68
  action: 'Update TEST-SPEC.md',
63
69
  command: 'docguard fix --doc test-spec',
64
70
  description: 'Test documentation needs updating to match actual test structure.',
71
+ autoFixable: false,
65
72
  },
66
73
  'Environment': {
67
74
  action: 'Update ENVIRONMENT.md',
68
75
  command: 'docguard fix --doc environment',
69
76
  description: 'Environment documentation is missing or incomplete.',
77
+ autoFixable: false,
70
78
  },
71
79
  'Security': {
72
80
  action: 'Update SECURITY.md',
73
81
  command: 'docguard fix --doc security',
74
82
  description: 'Security documentation needs updating.',
83
+ autoFixable: false,
75
84
  },
76
85
  'Architecture': {
77
86
  action: 'Update ARCHITECTURE.md',
78
87
  command: 'docguard fix --doc architecture',
79
88
  description: 'Architecture documentation doesn\'t match the codebase.',
89
+ autoFixable: false,
80
90
  },
81
91
  'Freshness': {
82
92
  action: 'Review stale documents',
83
93
  description: 'Documents haven\'t been reviewed since recent code changes. Re-run fix --doc for each stale doc.',
94
+ autoFixable: false,
84
95
  },
85
96
  };
86
97
 
87
98
  export function runDiagnose(projectDir, config, flags) {
88
99
  // ── Step 1: Run guard internally ──
89
- const guardData = runGuardInternal(projectDir, config);
100
+ let guardData = runGuardInternal(projectDir, config);
90
101
  const scoreData = runScoreInternal(projectDir, config);
91
102
 
92
103
  // ── Step 2: Collect issues ──
104
+ let issues = collectIssues(guardData);
105
+
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) {
109
+ const autoFixable = issues.filter(i => i.autoFixable);
110
+ const hasStructural = issues.some(i => i.validator === 'Structure');
111
+
112
+ if (hasStructural || autoFixable.length > 0) {
113
+ // Run init to create missing files
114
+ try {
115
+ const cliPath = resolve(import.meta.dirname, '..', 'docguard.mjs');
116
+ execSync(`node "${cliPath}" init --dir "${projectDir}"`, {
117
+ encoding: 'utf-8',
118
+ stdio: 'pipe',
119
+ });
120
+ } catch { /* init may partially succeed */ }
121
+
122
+ // Run generate to fill in content
123
+ try {
124
+ const cliPath = resolve(import.meta.dirname, '..', 'docguard.mjs');
125
+ execSync(`node "${cliPath}" generate --dir "${projectDir}" --force`, {
126
+ encoding: 'utf-8',
127
+ stdio: 'pipe',
128
+ });
129
+ } catch { /* generate may partially succeed */ }
130
+
131
+ // Re-run guard to see what's still broken
132
+ guardData = runGuardInternal(projectDir, config);
133
+ issues = collectIssues(guardData);
134
+
135
+ if (!flags.format || flags.format === 'text') {
136
+ const fixedCount = autoFixable.length - issues.filter(i => i.autoFixable).length;
137
+ if (fixedCount > 0) {
138
+ console.log(` ${c.green}⚡ Auto-fixed ${fixedCount} issue(s)${c.reset} (created/regenerated docs)\n`);
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ // Detect stale docs from freshness and map to specific fix --doc targets
145
+ for (const issue of issues) {
146
+ if (issue.validator === 'Freshness' && !issue.docTarget) {
147
+ const match = issue.message.match(/([\w-]+\.md)/i);
148
+ if (match) {
149
+ const docName = match[1].toLowerCase().replace('.md', '');
150
+ const docMap = { 'architecture': 'architecture', 'data-model': 'data-model', 'security': 'security', 'test-spec': 'test-spec', 'environment': 'environment' };
151
+ issue.docTarget = docMap[docName] || null;
152
+ if (issue.docTarget) {
153
+ issue.command = `docguard fix --doc ${issue.docTarget}`;
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ // ── Step 4: Output ──
160
+ if (flags.format === 'json') {
161
+ outputJSON(guardData, scoreData, issues);
162
+ } else if (flags.format === 'prompt') {
163
+ outputPrompt(projectDir, guardData, scoreData, issues, flags);
164
+ } else {
165
+ outputText(projectDir, guardData, scoreData, issues, flags);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Collect issues from guard results with fix metadata.
171
+ */
172
+ function collectIssues(guardData) {
93
173
  const issues = [];
94
174
  for (const v of guardData.validators) {
95
175
  if (v.status === 'skipped' || v.status === 'pass') continue;
96
176
 
97
- const fixInfo = FIX_INSTRUCTIONS[v.name] || { action: `Review ${v.name}`, description: 'Manual review needed.' };
177
+ const fixInfo = FIX_INSTRUCTIONS[v.name] || { action: `Review ${v.name}`, description: 'Manual review needed.', autoFixable: false };
98
178
  const docTarget = VALIDATOR_TO_DOC[v.name];
99
179
 
100
180
  for (const err of v.errors) {
@@ -105,6 +185,7 @@ export function runDiagnose(projectDir, config, flags) {
105
185
  action: fixInfo.action,
106
186
  command: fixInfo.command || null,
107
187
  docTarget,
188
+ autoFixable: fixInfo.autoFixable || false,
108
189
  });
109
190
  }
110
191
  for (const warn of v.warnings) {
@@ -115,34 +196,11 @@ export function runDiagnose(projectDir, config, flags) {
115
196
  action: fixInfo.action,
116
197
  command: fixInfo.command || null,
117
198
  docTarget,
199
+ autoFixable: fixInfo.autoFixable || false,
118
200
  });
119
201
  }
120
202
  }
121
-
122
- // Detect stale docs from freshness and map to specific fix --doc targets
123
- for (const issue of issues) {
124
- if (issue.validator === 'Freshness' && !issue.docTarget) {
125
- // Try to extract doc name from warning message
126
- const match = issue.message.match(/([\w-]+\.md)/i);
127
- if (match) {
128
- const docName = match[1].toLowerCase().replace('.md', '');
129
- const docMap = { 'architecture': 'architecture', 'data-model': 'data-model', 'security': 'security', 'test-spec': 'test-spec', 'environment': 'environment' };
130
- issue.docTarget = docMap[docName] || null;
131
- if (issue.docTarget) {
132
- issue.command = `docguard fix --doc ${issue.docTarget}`;
133
- }
134
- }
135
- }
136
- }
137
-
138
- // ── Step 3: Output ──
139
- if (flags.format === 'json') {
140
- outputJSON(guardData, scoreData, issues);
141
- } else if (flags.format === 'prompt') {
142
- outputPrompt(projectDir, guardData, scoreData, issues);
143
- } else {
144
- outputText(projectDir, guardData, scoreData, issues);
145
- }
203
+ return issues;
146
204
  }
147
205
 
148
206
  function outputJSON(guardData, scoreData, issues) {
@@ -168,7 +226,7 @@ function outputJSON(guardData, scoreData, issues) {
168
226
  console.log(JSON.stringify(result, null, 2));
169
227
  }
170
228
 
171
- function outputText(projectDir, guardData, scoreData, issues) {
229
+ function outputText(projectDir, guardData, scoreData, issues, flags) {
172
230
  console.log(`${c.bold}🔍 DocGuard Diagnose — ${guardData.project}${c.reset}`);
173
231
  console.log(`${c.dim} Profile: ${guardData.profile} | Score: ${scoreData.score}/100 (${scoreData.grade})${c.reset}`);
174
232
  console.log(`${c.dim} Guard: ${guardData.passed}/${guardData.total} passed | Status: ${guardData.status}${c.reset}\n`);
@@ -213,17 +271,27 @@ function outputText(projectDir, guardData, scoreData, issues) {
213
271
  }
214
272
 
215
273
  // ── AI Prompt (always shown in text mode for easy copy) ──
216
- console.log(` ${c.bold}🤖 AI-Ready Prompt:${c.reset}`);
217
- console.log(` ${c.dim}Copy everything below and paste to your AI agent:${c.reset}\n`);
218
- outputPrompt(undefined, guardData, scoreData, issues);
274
+ if (flags && flags.debate) {
275
+ // Multi-perspective debate prompts (AITPG/TRACE-inspired)
276
+ console.log(` ${c.bold}🤖 Multi-Perspective AI Debate Prompt:${c.reset}`);
277
+ console.log(` ${c.dim}Copy everything below and paste to your AI agent:${c.reset}\n`);
278
+ outputDebatePrompt(projectDir, guardData, scoreData, issues);
279
+ } else {
280
+ console.log(` ${c.bold}🤖 AI-Ready Prompt:${c.reset}`);
281
+ console.log(` ${c.dim}Copy everything below and paste to your AI agent:${c.reset}\n`);
282
+ outputPrompt(undefined, guardData, scoreData, issues, flags);
283
+ }
219
284
  }
220
285
 
221
- function outputPrompt(projectDir, guardData, scoreData, issues) {
286
+ function outputPrompt(projectDir, guardData, scoreData, issues, flags) {
222
287
  if (issues.length === 0) {
223
288
  console.log('No issues to fix. Documentation is healthy.');
224
289
  return;
225
290
  }
226
291
 
292
+ // Detect agent capability for prompt complexity (inspired by CJE equalizer effect, TRACE 2026)
293
+ const agentTier = detectAgentTier(projectDir || '.');
294
+
227
295
  const lines = [];
228
296
  lines.push(`TASK: Fix ${issues.length} documentation issue(s) in project "${guardData.project}"`);
229
297
  lines.push(`Profile: ${guardData.profile} | Score: ${scoreData.score}/100 | Guard: ${guardData.status}`);
@@ -257,6 +325,11 @@ function outputPrompt(projectDir, guardData, scoreData, issues) {
257
325
  if (group.docTarget) {
258
326
  lines.push(` Then research the codebase and write real content for this document.`);
259
327
  }
328
+ // Agent-aware: add extra detail for smaller models
329
+ if (agentTier === 'basic') {
330
+ lines.push(` NOTE: Review the codebase file by file. Look for patterns matching this issue.`);
331
+ lines.push(` Check docs-canonical/ for the expected format. Compare against existing docs.`);
332
+ }
260
333
  for (const msg of group.issues) {
261
334
  lines.push(` - ${msg}`);
262
335
  }
@@ -269,5 +342,123 @@ function outputPrompt(projectDir, guardData, scoreData, issues) {
269
342
  lines.push('Expected result: All checks pass (0 errors, 0 warnings)');
270
343
  lines.push(`Target score: ≥${scoreData.score + 5}/100`);
271
344
 
345
+ // Agent-aware: add explicit checklist for basic-tier agents
346
+ if (agentTier === 'basic') {
347
+ lines.push('');
348
+ lines.push('VERIFICATION CHECKLIST (complete each step):');
349
+ lines.push('□ Read each file in docs-canonical/ before editing');
350
+ lines.push('□ Run `docguard guard` after each file change');
351
+ lines.push('□ Confirm 0 errors before moving to next issue');
352
+ lines.push('□ Run `docguard score` to confirm improvement');
353
+ }
354
+
355
+ console.log(lines.join('\n'));
356
+ }
357
+
358
+ /**
359
+ * Generate multi-perspective debate prompts.
360
+ * Inspired by AITPG multi-agent role specialization (Positive/Negative/Edge + Critic)
361
+ * and TRACE adversarial debate (Advocate/Challenger/Mediator/Explainer).
362
+ * Lopez et al., IEEE TSE/TMLCN 2026.
363
+ */
364
+ function outputDebatePrompt(projectDir, guardData, scoreData, issues) {
365
+ const lines = [];
366
+
367
+ lines.push('═══════════════════════════════════════════════════════');
368
+ lines.push('MULTI-PERSPECTIVE DOCUMENTATION ANALYSIS');
369
+ lines.push(`Project: "${guardData.project}" | Score: ${scoreData.score}/100 | Issues: ${issues.length}`);
370
+ lines.push('Methodology: Multi-agent debate (Lopez et al., AITPG/TRACE, IEEE 2026)');
371
+ lines.push('═══════════════════════════════════════════════════════');
372
+ lines.push('');
373
+
374
+ // Issue context
375
+ lines.push('CONTEXT — Current Issues:');
376
+ for (let i = 0; i < issues.length; i++) {
377
+ lines.push(` ${i + 1}. [${issues[i].severity.toUpperCase()}] [${issues[i].validator}] ${issues[i].message}`);
378
+ }
379
+ lines.push('');
380
+
381
+ // ── Agent 1: Advocate ──
382
+ lines.push('───────────────────────────────────────────────────────');
383
+ lines.push('PERSPECTIVE 1: ADVOCATE (What is working well)');
384
+ lines.push('───────────────────────────────────────────────────────');
385
+ lines.push('Role: You are the Advocate agent. Your job is to identify what the project');
386
+ lines.push('documentation is doing RIGHT and which patterns should be PRESERVED.');
387
+ lines.push('');
388
+ lines.push('Instructions:');
389
+ lines.push('1. Read all files in docs-canonical/');
390
+ lines.push('2. Identify which documents are well-structured and complete');
391
+ lines.push('3. Note which naming conventions, section formats, and patterns are consistent');
392
+ lines.push('4. List 3-5 strengths that must be preserved during remediation');
393
+ lines.push('');
394
+
395
+ // ── Agent 2: Challenger ──
396
+ lines.push('───────────────────────────────────────────────────────');
397
+ lines.push('PERSPECTIVE 2: CHALLENGER (What is broken or risky)');
398
+ lines.push('───────────────────────────────────────────────────────');
399
+ lines.push('Role: You are the Challenger agent. Your job is to STRESS-TEST the documentation');
400
+ lines.push('and identify gaps, inconsistencies, and risks that the issues list might miss.');
401
+ lines.push('');
402
+ lines.push('Instructions:');
403
+ lines.push('1. For each issue listed above, explain WHY it matters and what the root cause is');
404
+ lines.push('2. Identify any ADDITIONAL issues not caught by docguard guard');
405
+ lines.push('3. Check: Are there undocumented API routes? Missing env vars? Stale references?');
406
+ lines.push('4. Rank all issues by business impact (P0 = blocks deployment, P1 = degrades quality, P2 = cosmetic)');
407
+ lines.push('');
408
+
409
+ // ── Agent 3: Synthesizer ──
410
+ lines.push('───────────────────────────────────────────────────────');
411
+ lines.push('PERSPECTIVE 3: SYNTHESIZER (Prioritized remediation plan)');
412
+ lines.push('───────────────────────────────────────────────────────');
413
+ lines.push('Role: You are the Synthesizer agent. Given the Advocate\'s strengths and the');
414
+ lines.push('Challenger\'s gaps, produce a PRIORITIZED and ACTIONABLE remediation plan.');
415
+ lines.push('');
416
+ lines.push('Instructions:');
417
+ lines.push('1. Preserve the patterns the Advocate identified as strengths');
418
+ lines.push('2. Address the Challenger\'s issues in priority order (P0 → P1 → P2)');
419
+ lines.push('3. For each fix, specify:');
420
+ lines.push(' a. Which file to edit');
421
+ lines.push(' b. What section to add or modify');
422
+ lines.push(' c. What content to write (be specific, not vague)');
423
+ lines.push('4. After all fixes, verify with: docguard guard');
424
+ lines.push(`5. Target score: ≥${Math.min(scoreData.score + 10, 100)}/100`);
425
+ lines.push('');
426
+ lines.push('═══════════════════════════════════════════════════════');
427
+ lines.push('Execute all three perspectives in sequence, then implement the Synthesizer\'s plan.');
428
+ lines.push('═══════════════════════════════════════════════════════');
429
+
272
430
  console.log(lines.join('\n'));
273
431
  }
432
+
433
+ /**
434
+ * Detect the AI agent tier from AGENTS.md or .docguard.json.
435
+ * Returns 'advanced' (concise prompts) or 'basic' (verbose step-by-step).
436
+ * Inspired by CJE equalizer effect (Lopez et al., TRACE, IEEE TMLCN 2026).
437
+ */
438
+ function detectAgentTier(projectDir) {
439
+ // Check .docguard.json for explicit agent config
440
+ const configPath = resolve(projectDir, '.docguard.json');
441
+ if (existsSync(configPath)) {
442
+ try {
443
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
444
+ if (config.agentTier) return config.agentTier;
445
+ } catch { /* ignore */ }
446
+ }
447
+
448
+ // Check AGENTS.md for known agent names
449
+ const agentFiles = ['AGENTS.md', 'CLAUDE.md', '.github/copilot-instructions.md'];
450
+ for (const file of agentFiles) {
451
+ const agentPath = resolve(projectDir, file);
452
+ if (existsSync(agentPath)) {
453
+ const content = readFileSync(agentPath, 'utf-8').toLowerCase();
454
+ // Advanced agents: Claude, GPT-4, Gemini Pro
455
+ const advancedMarkers = ['claude', 'gpt-4', 'gemini pro', 'gemini 2', 'opus', 'sonnet'];
456
+ if (advancedMarkers.some(m => content.includes(m))) {
457
+ return 'advanced';
458
+ }
459
+ }
460
+ }
461
+
462
+ // Default to advanced (most users run modern models)
463
+ return 'advanced';
464
+ }
@@ -23,6 +23,70 @@ const CODE_EXTENSIONS = new Set([
23
23
  '.py', '.java', '.go', '.rs', '.rb', '.php', '.cs',
24
24
  ]);
25
25
 
26
+ /**
27
+ * Standards citation map — each doc type maps to its governing industry standard.
28
+ * Inspired by RAG-grounded standards alignment (Lopez et al., AITPG, IEEE TSE 2026).
29
+ */
30
+ const STANDARDS_CITATIONS = {
31
+ 'ARCHITECTURE.md': {
32
+ standard: 'arc42 Template + C4 Model',
33
+ reference: 'Starke, G. & Brown, S. "arc42 — Architecture communication template." https://arc42.org | Brown, S. "The C4 Model for visualising software architecture." https://c4model.com',
34
+ sections: '§1 Introduction, §2 Constraints, §3 Context, §4 Solution Strategy, §5 Building Blocks, §6 Runtime, §7 Deployment, §8 Crosscutting, §9 ADRs, §10 Quality, §11 Risks, §12 Glossary',
35
+ },
36
+ 'DATA-MODEL.md': {
37
+ standard: 'C4 Component Diagram + Entity-Relationship (Chen notation)',
38
+ reference: 'Brown, S. "C4 Model — Component diagrams." https://c4model.com | Chen, P. "The Entity-Relationship Model." ACM TODS 1(1), 1976',
39
+ sections: 'Entities, Relationships, ER Diagrams (Mermaid), Field-level definitions',
40
+ },
41
+ 'TEST-SPEC.md': {
42
+ standard: 'ISO/IEC/IEEE 29119-3:2022 — Test Documentation',
43
+ reference: 'ISO/IEC/IEEE, "Software and systems engineering — Software testing — Part 3: Test documentation." International Standard, 2022',
44
+ sections: 'Test Categories, Coverage Rules, Test Matrix, Tool Configuration',
45
+ },
46
+ 'SECURITY.md': {
47
+ standard: 'OWASP ASVS v4.0 + CWE Top 25',
48
+ reference: 'OWASP Foundation, "Application Security Verification Standard v4.0." https://owasp.org/asvs | MITRE, "CWE Top 25." https://cwe.mitre.org/top25',
49
+ sections: 'Authentication, Secrets Management, Access Control, Input Validation',
50
+ },
51
+ 'ENVIRONMENT.md': {
52
+ standard: '12-Factor App Methodology',
53
+ reference: 'Wiggins, A. "The Twelve-Factor App." https://12factor.net',
54
+ sections: 'Environment Variables, Config Separation, Setup Steps, Provider Configuration',
55
+ },
56
+ 'API-REFERENCE.md': {
57
+ standard: 'OpenAPI Specification 3.1',
58
+ reference: 'OpenAPI Initiative, "OpenAPI Specification v3.1.0." https://spec.openapis.org/oas/v3.1.0',
59
+ sections: 'Endpoints, Request/Response schemas, Authentication, Error codes',
60
+ },
61
+ };
62
+
63
+ /**
64
+ * Append a standards citation footer to generated doc content.
65
+ * @param {string} content - The generated markdown content
66
+ * @param {string} docName - The filename (e.g., 'ARCHITECTURE.md')
67
+ * @returns {string} Content with citation footer appended
68
+ */
69
+ function appendStandardsCitation(content, docName) {
70
+ const citation = STANDARDS_CITATIONS[docName];
71
+ if (!citation) return content;
72
+
73
+ const footer = `
74
+ ---
75
+
76
+ ## Standards Reference
77
+
78
+ > **Aligned with**: ${citation.standard}
79
+ >
80
+ > **Sections covered**: ${citation.sections}
81
+ >
82
+ > **Reference**: ${citation.reference}
83
+ >
84
+ > *Standards alignment inspired by RAG-grounded generation (Lopez et al., AITPG, IEEE TSE 2026).*
85
+ `;
86
+
87
+ return content.trimEnd() + '\n' + footer;
88
+ }
89
+
26
90
  export function runGenerate(projectDir, config, flags) {
27
91
  console.log(`${c.bold}🔮 DocGuard Generate — ${config.projectName}${c.reset}`);
28
92
  console.log(`${c.dim} Directory: ${projectDir}${c.reset}`);
@@ -511,7 +575,7 @@ See \\\`docs-canonical/KNOWN-GOTCHAS.md\\\` for known issues.
511
575
  | 0.1.0 | ${new Date().toISOString().split('T')[0]} | DocGuard Generate | Auto-generated (arc42 + C4 aligned) |
512
576
  `;
513
577
 
514
- writeFileSync(path, content, 'utf-8');
578
+ writeFileSync(path, appendStandardsCitation(content, 'ARCHITECTURE.md'), 'utf-8');
515
579
  console.log(` ${c.green}✅ ARCHITECTURE.md${c.reset} (arc42 §1-§12, ${componentRows.length} components, ${Object.values(stack).filter(Boolean).length} tech)`);
516
580
  return true;
517
581
  }
@@ -608,7 +672,7 @@ ${resourceSections}
608
672
  | 0.1.0 | ${new Date().toISOString().split('T')[0]} | DocGuard Generate | Auto-generated (${deepRoutes.length} endpoints from ${deepRoutes[0]?.source || 'code'}) |
609
673
  `;
610
674
 
611
- writeFileSync(path, content, 'utf-8');
675
+ writeFileSync(path, appendStandardsCitation(content, 'API-REFERENCE.md'), 'utf-8');
612
676
  console.log(` ${c.green}✅ API-REFERENCE.md${c.reset} (${deepRoutes.length} endpoints, ${Object.keys(groups).length} resources)`);
613
677
  return true;
614
678
  }
@@ -763,7 +827,7 @@ ${erDiagram}
763
827
  | 0.1.0 | ${new Date().toISOString().split('T')[0]} | DocGuard Generate | Auto-generated (${entities.length} entities, ${relationships.length} relationships from ${schemaSource}) |
764
828
  `;
765
829
 
766
- writeFileSync(path, content, 'utf-8');
830
+ writeFileSync(path, appendStandardsCitation(content, 'DATA-MODEL.md'), 'utf-8');
767
831
  console.log(` ${c.green}✅ DATA-MODEL.md${c.reset} (${entities.length} entities, ${relationships.length} relationships from ${schemaSource})`);
768
832
  return true;
769
833
  }
@@ -826,7 +890,7 @@ ${envVarRows || '| <!-- No .env.example found --> | | | | |'}
826
890
  | 0.1.0 | ${new Date().toISOString().split('T')[0]} | DocGuard Generate | Auto-generated (${scan.envVars.length} env vars found) |
827
891
  `;
828
892
 
829
- writeFileSync(path, content, 'utf-8');
893
+ writeFileSync(path, appendStandardsCitation(content, 'ENVIRONMENT.md'), 'utf-8');
830
894
  console.log(` ${c.green}✅ ENVIRONMENT.md${c.reset} (${scan.envVars.length} env vars detected)`);
831
895
  return true;
832
896
  }
@@ -911,7 +975,7 @@ ${serviceRows || '| <!-- No services found --> | | | |'}
911
975
  | 0.1.0 | ${new Date().toISOString().split('T')[0]} | DocGuard Generate | Auto-generated (${scan.tests.length} test files, ${serviceMap.filter(s => s.status === '✅').length}/${serviceMap.length} mapped) |
912
976
  `;
913
977
 
914
- writeFileSync(path, content, 'utf-8');
978
+ writeFileSync(path, appendStandardsCitation(content, 'TEST-SPEC.md'), 'utf-8');
915
979
  console.log(` ${c.green}✅ TEST-SPEC.md${c.reset} (${scan.tests.length} tests, ${serviceMap.filter(s => s.status === '✅').length}/${serviceMap.length} services mapped)`);
916
980
  return true;
917
981
  }
@@ -977,7 +1041,7 @@ ${scan.envVars.filter(v => isSecretVar(v.name)).map(v =>
977
1041
  | 0.1.0 | ${new Date().toISOString().split('T')[0]} | DocGuard Generate | Auto-generated |
978
1042
  `;
979
1043
 
980
- writeFileSync(path, content, 'utf-8');
1044
+ writeFileSync(path, appendStandardsCitation(content, 'SECURITY.md'), 'utf-8');
981
1045
  console.log(` ${c.green}✅ SECURITY.md${c.reset} (auth: ${stack.auth || 'not detected'})`);
982
1046
  return true;
983
1047
  }
@@ -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') {
@@ -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
+ }
package/cli/docguard.mjs CHANGED
@@ -36,6 +36,7 @@ import { runFix } from './commands/fix.mjs';
36
36
  import { runWatch } from './commands/watch.mjs';
37
37
  import { runDiagnose } from './commands/diagnose.mjs';
38
38
  import { runPublish } from './commands/publish.mjs';
39
+ import { runTrace } from './commands/trace.mjs';
39
40
 
40
41
  // ── Colors (ANSI escape codes, zero deps) ──────────────────────────────────
41
42
  export const c = {
@@ -287,6 +288,7 @@ ${c.bold}Commands:${c.reset}
287
288
  ${c.green}fix${c.reset} Find issues and generate AI fix instructions
288
289
  ${c.green}watch${c.reset} Watch for file changes and re-run guard automatically
289
290
  ${c.green}publish${c.reset} Scaffold external docs (Mintlify, Docusaurus)
291
+ ${c.green}trace${c.reset} Generate requirements traceability matrix
290
292
 
291
293
  ${c.bold}Options:${c.reset}
292
294
  --dir <path> Project directory (default: current directory)
@@ -396,6 +398,12 @@ function main() {
396
398
  } else if (args[i] === '--platform' && args[i + 1]) {
397
399
  flags.platform = args[i + 1];
398
400
  i++;
401
+ } else if (args[i] === '--no-fix') {
402
+ flags.noFix = true;
403
+ } else if (args[i] === '--signals') {
404
+ flags.signals = true;
405
+ } else if (args[i] === '--debate') {
406
+ flags.debate = true;
399
407
  }
400
408
  }
401
409
 
@@ -464,6 +472,10 @@ function main() {
464
472
  case 'pub':
465
473
  runPublish(projectDir, config, flags);
466
474
  break;
475
+ case 'trace':
476
+ case 'traceability':
477
+ runTrace(projectDir, config, flags);
478
+ break;
467
479
  default:
468
480
  console.error(`${c.red}Unknown command: ${command}${c.reset}`);
469
481
  console.log(`Run ${c.cyan}docguard --help${c.reset} for usage.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "The enforcement tool for Canonical-Driven Development (CDD). Audit, generate, and guard your project documentation.",
5
5
  "type": "module",
6
6
  "bin": {