specweave 0.30.11 → 0.30.13

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.
Files changed (134) hide show
  1. package/dist/src/cli/commands/init.d.ts.map +1 -1
  2. package/dist/src/cli/commands/init.js +25 -2
  3. package/dist/src/cli/commands/init.js.map +1 -1
  4. package/dist/src/cli/helpers/ado-area-selector.d.ts.map +1 -1
  5. package/dist/src/cli/helpers/ado-area-selector.js +13 -0
  6. package/dist/src/cli/helpers/ado-area-selector.js.map +1 -1
  7. package/dist/src/cli/helpers/init/living-docs-preflight.d.ts +5 -1
  8. package/dist/src/cli/helpers/init/living-docs-preflight.d.ts.map +1 -1
  9. package/dist/src/cli/helpers/init/living-docs-preflight.js +80 -28
  10. package/dist/src/cli/helpers/init/living-docs-preflight.js.map +1 -1
  11. package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
  12. package/dist/src/cli/helpers/issue-tracker/index.js +7 -2
  13. package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
  14. package/dist/src/cli/helpers/issue-tracker/sync-config-writer.d.ts +7 -0
  15. package/dist/src/cli/helpers/issue-tracker/sync-config-writer.d.ts.map +1 -1
  16. package/dist/src/cli/helpers/issue-tracker/sync-config-writer.js +33 -2
  17. package/dist/src/cli/helpers/issue-tracker/sync-config-writer.js.map +1 -1
  18. package/dist/src/cli/workers/brownfield-worker.d.ts +13 -0
  19. package/dist/src/cli/workers/brownfield-worker.d.ts.map +1 -1
  20. package/dist/src/cli/workers/brownfield-worker.js +154 -0
  21. package/dist/src/cli/workers/brownfield-worker.js.map +1 -1
  22. package/dist/src/cli/workers/clone-worker.js +19 -3
  23. package/dist/src/cli/workers/clone-worker.js.map +1 -1
  24. package/dist/src/cli/workers/living-docs-worker.js +272 -11
  25. package/dist/src/cli/workers/living-docs-worker.js.map +1 -1
  26. package/dist/src/core/background/brownfield-launcher.d.ts +2 -1
  27. package/dist/src/core/background/brownfield-launcher.d.ts.map +1 -1
  28. package/dist/src/core/background/brownfield-launcher.js.map +1 -1
  29. package/dist/src/core/background/types.d.ts +10 -2
  30. package/dist/src/core/background/types.d.ts.map +1 -1
  31. package/dist/src/core/discrepancy/brownfield-types.d.ts +3 -1
  32. package/dist/src/core/discrepancy/brownfield-types.d.ts.map +1 -1
  33. package/dist/src/core/living-docs/board-matcher.d.ts +120 -0
  34. package/dist/src/core/living-docs/board-matcher.d.ts.map +1 -0
  35. package/dist/src/core/living-docs/board-matcher.js +466 -0
  36. package/dist/src/core/living-docs/board-matcher.js.map +1 -0
  37. package/dist/src/core/living-docs/feature-archiver.d.ts +39 -0
  38. package/dist/src/core/living-docs/feature-archiver.d.ts.map +1 -1
  39. package/dist/src/core/living-docs/feature-archiver.js +197 -0
  40. package/dist/src/core/living-docs/feature-archiver.js.map +1 -1
  41. package/dist/src/core/living-docs/foundation-builder.js +1 -1
  42. package/dist/src/core/living-docs/foundation-builder.js.map +1 -1
  43. package/dist/src/core/living-docs/living-docs-sync.d.ts +19 -8
  44. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
  45. package/dist/src/core/living-docs/living-docs-sync.js +148 -52
  46. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
  47. package/dist/src/core/living-docs/suggestions-generator.js +1 -1
  48. package/dist/src/core/living-docs/suggestions-generator.js.map +1 -1
  49. package/dist/src/core/living-docs/umbrella-detector.d.ts +4 -0
  50. package/dist/src/core/living-docs/umbrella-detector.d.ts.map +1 -1
  51. package/dist/src/core/living-docs/umbrella-detector.js +20 -1
  52. package/dist/src/core/living-docs/umbrella-detector.js.map +1 -1
  53. package/dist/src/core/living-docs/workitem-matcher.js +5 -5
  54. package/dist/src/core/living-docs/workitem-matcher.js.map +1 -1
  55. package/dist/src/core/llm/availability-messages.d.ts +33 -0
  56. package/dist/src/core/llm/availability-messages.d.ts.map +1 -0
  57. package/dist/src/core/llm/availability-messages.js +170 -0
  58. package/dist/src/core/llm/availability-messages.js.map +1 -0
  59. package/dist/src/core/llm/index.d.ts +34 -0
  60. package/dist/src/core/llm/index.d.ts.map +1 -0
  61. package/dist/src/core/llm/index.js +35 -0
  62. package/dist/src/core/llm/index.js.map +1 -0
  63. package/dist/src/core/llm/provider-factory.d.ts +48 -0
  64. package/dist/src/core/llm/provider-factory.d.ts.map +1 -0
  65. package/dist/src/core/llm/provider-factory.js +274 -0
  66. package/dist/src/core/llm/provider-factory.js.map +1 -0
  67. package/dist/src/core/llm/providers/anthropic-provider.d.ts +66 -0
  68. package/dist/src/core/llm/providers/anthropic-provider.d.ts.map +1 -0
  69. package/dist/src/core/llm/providers/anthropic-provider.js +195 -0
  70. package/dist/src/core/llm/providers/anthropic-provider.js.map +1 -0
  71. package/dist/src/core/llm/providers/azure-openai-provider.d.ts +47 -0
  72. package/dist/src/core/llm/providers/azure-openai-provider.d.ts.map +1 -0
  73. package/dist/src/core/llm/providers/azure-openai-provider.js +116 -0
  74. package/dist/src/core/llm/providers/azure-openai-provider.js.map +1 -0
  75. package/dist/src/core/llm/providers/bedrock-provider.d.ts +44 -0
  76. package/dist/src/core/llm/providers/bedrock-provider.d.ts.map +1 -0
  77. package/dist/src/core/llm/providers/bedrock-provider.js +149 -0
  78. package/dist/src/core/llm/providers/bedrock-provider.js.map +1 -0
  79. package/dist/src/core/llm/providers/claude-code-provider.d.ts +115 -0
  80. package/dist/src/core/llm/providers/claude-code-provider.d.ts.map +1 -0
  81. package/dist/src/core/llm/providers/claude-code-provider.js +379 -0
  82. package/dist/src/core/llm/providers/claude-code-provider.js.map +1 -0
  83. package/dist/src/core/llm/providers/ollama-provider.d.ts +40 -0
  84. package/dist/src/core/llm/providers/ollama-provider.d.ts.map +1 -0
  85. package/dist/src/core/llm/providers/ollama-provider.js +116 -0
  86. package/dist/src/core/llm/providers/ollama-provider.js.map +1 -0
  87. package/dist/src/core/llm/providers/openai-provider.d.ts +44 -0
  88. package/dist/src/core/llm/providers/openai-provider.d.ts.map +1 -0
  89. package/dist/src/core/llm/providers/openai-provider.js +119 -0
  90. package/dist/src/core/llm/providers/openai-provider.js.map +1 -0
  91. package/dist/src/core/llm/providers/vertex-ai-provider.d.ts +46 -0
  92. package/dist/src/core/llm/providers/vertex-ai-provider.d.ts.map +1 -0
  93. package/dist/src/core/llm/providers/vertex-ai-provider.js +123 -0
  94. package/dist/src/core/llm/providers/vertex-ai-provider.js.map +1 -0
  95. package/dist/src/core/llm/types.d.ts +181 -0
  96. package/dist/src/core/llm/types.d.ts.map +1 -0
  97. package/dist/src/core/llm/types.js +56 -0
  98. package/dist/src/core/llm/types.js.map +1 -0
  99. package/dist/src/importers/item-converter.d.ts +4 -0
  100. package/dist/src/importers/item-converter.d.ts.map +1 -1
  101. package/dist/src/importers/item-converter.js +73 -12
  102. package/dist/src/importers/item-converter.js.map +1 -1
  103. package/dist/src/init/repo/types.d.ts +1 -1
  104. package/dist/src/living-docs/enterprise-analyzer.d.ts +160 -0
  105. package/dist/src/living-docs/enterprise-analyzer.d.ts.map +1 -0
  106. package/dist/src/living-docs/enterprise-analyzer.js +887 -0
  107. package/dist/src/living-docs/enterprise-analyzer.js.map +1 -0
  108. package/dist/src/living-docs/epic-id-allocator.d.ts +4 -0
  109. package/dist/src/living-docs/epic-id-allocator.d.ts.map +1 -1
  110. package/dist/src/living-docs/epic-id-allocator.js +4 -0
  111. package/dist/src/living-docs/epic-id-allocator.js.map +1 -1
  112. package/dist/src/living-docs/fs-id-allocator.d.ts +9 -0
  113. package/dist/src/living-docs/fs-id-allocator.d.ts.map +1 -1
  114. package/dist/src/living-docs/fs-id-allocator.js +16 -5
  115. package/dist/src/living-docs/fs-id-allocator.js.map +1 -1
  116. package/dist/src/living-docs/smart-doc-organizer.d.ts +114 -0
  117. package/dist/src/living-docs/smart-doc-organizer.d.ts.map +1 -0
  118. package/dist/src/living-docs/smart-doc-organizer.js +535 -0
  119. package/dist/src/living-docs/smart-doc-organizer.js.map +1 -0
  120. package/package.json +1 -1
  121. package/plugins/specweave/commands/specweave-archive.md +69 -2
  122. package/plugins/specweave/commands/specweave-judge.md +265 -0
  123. package/plugins/specweave/commands/specweave-organize-docs.md +185 -0
  124. package/plugins/specweave/hooks/hooks.json +3 -3
  125. package/plugins/specweave/hooks/universal/hook-wrapper.cmd +26 -0
  126. package/plugins/specweave/hooks/universal/hook-wrapper.sh +67 -0
  127. package/plugins/specweave-docs/commands/build.md +158 -0
  128. package/plugins/specweave-docs/commands/{docs-generate.md → generate.md} +7 -2
  129. package/plugins/specweave-docs/commands/health.md +268 -0
  130. package/plugins/specweave-docs/commands/{docs-init.md → init.md} +7 -2
  131. package/plugins/specweave-docs/commands/organize.md +184 -0
  132. package/plugins/specweave-docs/commands/preview.md +138 -0
  133. package/plugins/specweave-docs/skills/preview/SKILL.md +105 -0
  134. package/plugins/specweave-release/commands/specweave-release-npm.md +22 -235
@@ -0,0 +1,887 @@
1
+ /**
2
+ * Enterprise Documentation Analyzer
3
+ *
4
+ * Provides comprehensive documentation analysis covering:
5
+ * - All docs/internal folders (specs, architecture, ADRs, governance)
6
+ * - Spec-code mismatch detection
7
+ * - Documentation health scoring
8
+ * - Enterprise-grade reporting
9
+ */
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import { glob } from 'glob';
13
+ import { consoleLogger } from '../utils/logger.js';
14
+ /**
15
+ * Enterprise Documentation Analyzer
16
+ */
17
+ export class EnterpriseDocAnalyzer {
18
+ constructor(options) {
19
+ this.projectPath = options.projectPath;
20
+ this.logger = options.logger ?? consoleLogger;
21
+ this.includeArchived = options.includeArchived ?? false;
22
+ }
23
+ /**
24
+ * Run full enterprise documentation analysis
25
+ */
26
+ async analyze() {
27
+ this.logger.info('Starting enterprise documentation analysis...');
28
+ // Phase 1: Scan all documentation categories
29
+ const categories = await this.scanAllCategories();
30
+ // Phase 2: Extract acceptance criteria from specs
31
+ const allACs = this.extractAllAcceptanceCriteria(categories);
32
+ // Phase 3: Detect spec-code mismatches
33
+ const mismatches = await this.detectMismatches(allACs);
34
+ // Phase 4: Detect naming convention violations
35
+ const namingViolations = this.detectNamingViolations(categories);
36
+ this.logger.info(`Detected ${namingViolations.length} naming convention violations`);
37
+ // Phase 5: Detect duplicate documents
38
+ const duplicates = this.detectDuplicates(categories);
39
+ this.logger.info(`Detected ${duplicates.length} potential duplicates`);
40
+ // Phase 6: Detect discrepancies
41
+ const discrepancies = await this.detectDiscrepancies(categories);
42
+ this.logger.info(`Detected ${discrepancies.length} discrepancies`);
43
+ // Phase 7: Calculate health scores
44
+ const healthScore = this.calculateHealthScore(categories, mismatches, namingViolations);
45
+ // Phase 8: Generate recommendations
46
+ const recommendations = this.generateRecommendations(categories, mismatches, healthScore, namingViolations, duplicates);
47
+ const totalDocuments = categories.reduce((sum, cat) => sum + cat.fileCount, 0);
48
+ return {
49
+ generatedAt: new Date(),
50
+ projectPath: this.projectPath,
51
+ categories,
52
+ totalDocuments,
53
+ healthScore,
54
+ mismatches,
55
+ namingViolations,
56
+ duplicates,
57
+ discrepancies,
58
+ recommendations,
59
+ };
60
+ }
61
+ /**
62
+ * Scan all documentation categories
63
+ */
64
+ async scanAllCategories() {
65
+ const categories = [];
66
+ const internalDocsPath = path.join(this.projectPath, '.specweave/docs/internal');
67
+ if (!fs.existsSync(internalDocsPath)) {
68
+ this.logger.warn('No .specweave/docs/internal directory found');
69
+ return categories;
70
+ }
71
+ // Define category mappings
72
+ const categoryDefs = [
73
+ { name: 'Feature Specs', subpath: 'specs' },
74
+ { name: 'Architecture', subpath: 'architecture' },
75
+ { name: 'ADRs', subpath: 'architecture/adr' },
76
+ { name: 'Governance', subpath: 'governance' },
77
+ { name: 'Modules', subpath: 'modules' },
78
+ { name: 'Emergency Procedures', subpath: 'emergency-procedures' },
79
+ ];
80
+ for (const def of categoryDefs) {
81
+ const categoryPath = path.join(internalDocsPath, def.subpath);
82
+ if (fs.existsSync(categoryPath)) {
83
+ const category = await this.scanCategory(def.name, categoryPath);
84
+ if (category.fileCount > 0) {
85
+ categories.push(category);
86
+ }
87
+ }
88
+ }
89
+ // Also scan increment specs
90
+ const incrementsPath = path.join(this.projectPath, '.specweave/increments');
91
+ if (fs.existsSync(incrementsPath)) {
92
+ const incrementCategory = await this.scanIncrementSpecs(incrementsPath);
93
+ if (incrementCategory.fileCount > 0) {
94
+ categories.push(incrementCategory);
95
+ }
96
+ }
97
+ this.logger.info(`Scanned ${categories.length} documentation categories`);
98
+ return categories;
99
+ }
100
+ /**
101
+ * Scan a single documentation category
102
+ */
103
+ async scanCategory(name, categoryPath) {
104
+ // Simple pattern - rely on ignore for filtering
105
+ const files = await glob('**/*.md', {
106
+ cwd: categoryPath,
107
+ nodir: true,
108
+ ignore: this.includeArchived ? [] : ['**/_archive/**', '**/node_modules/**'],
109
+ });
110
+ const documents = [];
111
+ let latestUpdate = null;
112
+ for (const file of files) {
113
+ const fullPath = path.join(categoryPath, file);
114
+ const stats = fs.statSync(fullPath);
115
+ const content = fs.readFileSync(fullPath, 'utf-8');
116
+ const acs = this.parseAcceptanceCriteria(content, fullPath);
117
+ documents.push({
118
+ path: fullPath,
119
+ name: path.basename(file, '.md'),
120
+ category: name,
121
+ lastModified: stats.mtime,
122
+ size: stats.size,
123
+ hasAcceptanceCriteria: acs.length > 0,
124
+ acceptanceCriteria: acs,
125
+ });
126
+ if (!latestUpdate || stats.mtime > latestUpdate) {
127
+ latestUpdate = stats.mtime;
128
+ }
129
+ }
130
+ return {
131
+ name,
132
+ path: categoryPath,
133
+ fileCount: documents.length,
134
+ files: documents,
135
+ lastUpdated: latestUpdate,
136
+ };
137
+ }
138
+ /**
139
+ * Scan increment spec files
140
+ */
141
+ async scanIncrementSpecs(incrementsPath) {
142
+ // Simple pattern - rely on ignore for filtering
143
+ const files = await glob('*/spec.md', {
144
+ cwd: incrementsPath,
145
+ nodir: true,
146
+ ignore: this.includeArchived ? [] : ['_archive/**'],
147
+ });
148
+ const documents = [];
149
+ let latestUpdate = null;
150
+ for (const file of files) {
151
+ const fullPath = path.join(incrementsPath, file);
152
+ const stats = fs.statSync(fullPath);
153
+ const content = fs.readFileSync(fullPath, 'utf-8');
154
+ const acs = this.parseAcceptanceCriteria(content, fullPath);
155
+ documents.push({
156
+ path: fullPath,
157
+ name: path.dirname(file),
158
+ category: 'Increment Specs',
159
+ lastModified: stats.mtime,
160
+ size: stats.size,
161
+ hasAcceptanceCriteria: acs.length > 0,
162
+ acceptanceCriteria: acs,
163
+ });
164
+ if (!latestUpdate || stats.mtime > latestUpdate) {
165
+ latestUpdate = stats.mtime;
166
+ }
167
+ }
168
+ return {
169
+ name: 'Increment Specs',
170
+ path: incrementsPath,
171
+ fileCount: documents.length,
172
+ files: documents,
173
+ lastUpdated: latestUpdate,
174
+ };
175
+ }
176
+ /**
177
+ * Parse acceptance criteria from markdown content
178
+ */
179
+ parseAcceptanceCriteria(content, sourceFile) {
180
+ const acs = [];
181
+ // Pattern: - [ ] **AC-US1-01**: Description or - [x] **AC-US1-01**: Description
182
+ const acPattern = /- \[([ x])\] \*\*?(AC-[A-Z0-9]+-\d+)\*\*?:?\s*(.+)/g;
183
+ let match;
184
+ while ((match = acPattern.exec(content)) !== null) {
185
+ acs.push({
186
+ id: match[2],
187
+ description: match[3].trim(),
188
+ isComplete: match[1] === 'x',
189
+ sourceFile,
190
+ });
191
+ }
192
+ return acs;
193
+ }
194
+ /**
195
+ * Extract all acceptance criteria from categories
196
+ */
197
+ extractAllAcceptanceCriteria(categories) {
198
+ const allACs = [];
199
+ for (const category of categories) {
200
+ for (const doc of category.files) {
201
+ allACs.push(...doc.acceptanceCriteria);
202
+ }
203
+ }
204
+ this.logger.info(`Extracted ${allACs.length} acceptance criteria`);
205
+ return allACs;
206
+ }
207
+ /**
208
+ * Detect mismatches between specs and code
209
+ */
210
+ async detectMismatches(acs) {
211
+ const mismatches = [];
212
+ const srcPath = path.join(this.projectPath, 'src');
213
+ if (!fs.existsSync(srcPath)) {
214
+ return mismatches;
215
+ }
216
+ // Get list of source files for evidence search
217
+ const sourceFiles = await glob('**/*.{ts,js,tsx,jsx}', {
218
+ cwd: srcPath,
219
+ nodir: true,
220
+ ignore: ['**/*.test.*', '**/*.spec.*', '**/node_modules/**'],
221
+ });
222
+ // Check completed ACs for code evidence
223
+ const completedACs = acs.filter(ac => ac.isComplete);
224
+ for (const ac of completedACs) {
225
+ // Extract keywords from AC description
226
+ const keywords = this.extractKeywords(ac.description);
227
+ // Search for evidence in code
228
+ const evidence = await this.searchCodeEvidence(srcPath, sourceFiles, keywords);
229
+ if (!evidence || evidence.files.length === 0) {
230
+ // AC marked complete but no code evidence found
231
+ mismatches.push({
232
+ specFile: ac.sourceFile,
233
+ criterionId: ac.id,
234
+ description: ac.description,
235
+ claimedComplete: true,
236
+ codeEvidence: null,
237
+ confidence: 70, // Medium confidence - might be false positive
238
+ mismatchType: 'ghost_completion',
239
+ });
240
+ }
241
+ else if (evidence.lineCount < 10) {
242
+ // Very little code evidence
243
+ mismatches.push({
244
+ specFile: ac.sourceFile,
245
+ criterionId: ac.id,
246
+ description: ac.description,
247
+ claimedComplete: true,
248
+ codeEvidence: evidence,
249
+ confidence: 50,
250
+ mismatchType: 'partial_implementation',
251
+ });
252
+ }
253
+ }
254
+ this.logger.info(`Detected ${mismatches.length} potential mismatches`);
255
+ return mismatches;
256
+ }
257
+ /**
258
+ * Extract keywords from AC description for code search
259
+ */
260
+ extractKeywords(description) {
261
+ // Remove common words and extract meaningful terms
262
+ const stopWords = new Set([
263
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
264
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
265
+ 'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare',
266
+ 'ought', 'used', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by',
267
+ 'from', 'as', 'into', 'through', 'during', 'before', 'after', 'above',
268
+ 'below', 'between', 'under', 'again', 'further', 'then', 'once',
269
+ 'and', 'but', 'or', 'nor', 'so', 'yet', 'both', 'either', 'neither',
270
+ 'not', 'only', 'own', 'same', 'than', 'too', 'very', 'just', 'that',
271
+ 'this', 'these', 'those', 'when', 'where', 'which', 'who', 'whom',
272
+ 'whose', 'why', 'how', 'all', 'each', 'every', 'any', 'some', 'no',
273
+ ]);
274
+ const words = description
275
+ .toLowerCase()
276
+ .replace(/[^a-z0-9\s]/g, ' ')
277
+ .split(/\s+/)
278
+ .filter(word => word.length > 3 && !stopWords.has(word));
279
+ // Deduplicate and limit to 5 keywords
280
+ return [...new Set(words)].slice(0, 5);
281
+ }
282
+ /**
283
+ * Search for code evidence matching keywords
284
+ */
285
+ async searchCodeEvidence(srcPath, sourceFiles, keywords) {
286
+ if (keywords.length === 0)
287
+ return null;
288
+ const matchingFiles = [];
289
+ const functions = [];
290
+ let totalLines = 0;
291
+ for (const file of sourceFiles.slice(0, 100)) { // Limit for performance
292
+ const fullPath = path.join(srcPath, file);
293
+ try {
294
+ const content = fs.readFileSync(fullPath, 'utf-8');
295
+ const contentLower = content.toLowerCase();
296
+ // Check if any keyword appears in file
297
+ const matches = keywords.filter(kw => contentLower.includes(kw));
298
+ if (matches.length >= Math.ceil(keywords.length / 2)) {
299
+ matchingFiles.push(file);
300
+ totalLines += content.split('\n').length;
301
+ // Extract function names that might be related
302
+ const funcPattern = /(?:function|const|let|var)\s+(\w+)\s*[=(]/g;
303
+ let funcMatch;
304
+ while ((funcMatch = funcPattern.exec(content)) !== null) {
305
+ const funcName = funcMatch[1].toLowerCase();
306
+ if (keywords.some(kw => funcName.includes(kw))) {
307
+ functions.push(funcMatch[1]);
308
+ }
309
+ }
310
+ }
311
+ }
312
+ catch {
313
+ // Skip files that can't be read
314
+ }
315
+ }
316
+ if (matchingFiles.length === 0)
317
+ return null;
318
+ return {
319
+ files: matchingFiles,
320
+ functions: [...new Set(functions)],
321
+ lineCount: totalLines,
322
+ };
323
+ }
324
+ /**
325
+ * Detect naming convention violations in documentation files
326
+ */
327
+ detectNamingViolations(categories) {
328
+ const violations = [];
329
+ // Define expected patterns per category
330
+ const categoryPatterns = {
331
+ 'Feature Specs': { pattern: /^(us-\d{3}|FS-\d{3}|[a-z][a-z0-9-]+)\.md$/i, expected: 'us-XXX.md, FS-XXX/*, or lowercase-kebab.md' },
332
+ 'Architecture': { pattern: /^[a-z][a-z0-9-]+\.md$/, expected: 'lowercase-kebab-case.md' },
333
+ 'ADRs': { pattern: /^\d{4}-[a-z][a-z0-9-]+\.md$/, expected: 'XXXX-title-in-kebab-case.md' },
334
+ 'Governance': { pattern: /^[a-z][a-z0-9-]+\.md$/, expected: 'lowercase-kebab-case.md' },
335
+ 'Modules': { pattern: /^[a-z][a-z0-9-]+\.md$/i, expected: 'lowercase-kebab-case.md' },
336
+ 'Emergency Procedures': { pattern: /^[a-z][a-z0-9-]+\.md$/, expected: 'lowercase-kebab-case.md' },
337
+ 'Increment Specs': { pattern: /^spec\.md$/, expected: 'spec.md' },
338
+ };
339
+ // Patterns to detect violations
340
+ const allCapsPattern = /^[A-Z][A-Z0-9-]+\.md$/;
341
+ const mixedCasePattern = /^(?=.*[A-Z])(?=.*[a-z])[A-Za-z0-9-]+\.md$/;
342
+ const dateSuffixPattern = /-\d{4}-\d{2}-\d{2}/;
343
+ const noExtensionPattern = /^[^.]+$/;
344
+ for (const category of categories) {
345
+ const expectedRule = categoryPatterns[category.name];
346
+ for (const doc of category.files) {
347
+ const fileName = path.basename(doc.path);
348
+ const relativePath = path.relative(this.projectPath, doc.path);
349
+ // Check for ALL CAPS files (e.g., CIRCUIT-BREAKER-MONITORING.md)
350
+ // Exclude standard files like README.md, FEATURE.md, API.md, CHANGELOG.md
351
+ const standardAllCapsFiles = ['README.md', 'FEATURE.md', 'API.md', 'CHANGELOG.md', 'LICENSE.md', 'CONTRIBUTING.md'];
352
+ if (allCapsPattern.test(fileName) && !standardAllCapsFiles.includes(fileName)) {
353
+ violations.push({
354
+ file: relativePath,
355
+ category: category.name,
356
+ violationType: 'all_caps',
357
+ expectedPattern: expectedRule?.expected ?? 'lowercase-kebab-case.md',
358
+ actual: fileName,
359
+ severity: 'warning',
360
+ });
361
+ continue;
362
+ }
363
+ // Check for mixed case (e.g., CircuitBreaker.md)
364
+ // Exclude standard files that use conventional naming
365
+ if (mixedCasePattern.test(fileName) && !standardAllCapsFiles.includes(fileName) && !fileName.startsWith('README') && !fileName.startsWith('API')) {
366
+ violations.push({
367
+ file: relativePath,
368
+ category: category.name,
369
+ violationType: 'mixed_case',
370
+ expectedPattern: expectedRule?.expected ?? 'lowercase-kebab-case.md',
371
+ actual: fileName,
372
+ severity: 'warning',
373
+ });
374
+ continue;
375
+ }
376
+ // Check for date suffixes (e.g., feature-fix-2025-11-24.md)
377
+ if (dateSuffixPattern.test(fileName)) {
378
+ violations.push({
379
+ file: relativePath,
380
+ category: category.name,
381
+ violationType: 'date_suffix',
382
+ expectedPattern: 'Document names should not include dates (use git history)',
383
+ actual: fileName,
384
+ severity: 'info',
385
+ });
386
+ }
387
+ // Check for no extension
388
+ if (noExtensionPattern.test(fileName)) {
389
+ violations.push({
390
+ file: relativePath,
391
+ category: category.name,
392
+ violationType: 'no_extension',
393
+ expectedPattern: 'Files should have .md extension',
394
+ actual: fileName,
395
+ severity: 'error',
396
+ });
397
+ }
398
+ // Check against category-specific pattern
399
+ if (expectedRule && !expectedRule.pattern.test(fileName)) {
400
+ // Skip if already caught by other checks
401
+ if (!allCapsPattern.test(fileName) && !mixedCasePattern.test(fileName)) {
402
+ // Skip files starting with _ (navigation/index files)
403
+ if (fileName.startsWith('_')) {
404
+ continue; // Navigation files like _index-*.md, _categories.md
405
+ }
406
+ // Skip ADR numbered files (e.g., 0001-decision-name.md) - this is standard ADR convention
407
+ const adrPattern = /^\d{4}-[a-z0-9-]+\.md$/;
408
+ const isInAdrFolder = relativePath.includes('/adr/') || relativePath.includes('\\adr\\');
409
+ if (isInAdrFolder && adrPattern.test(fileName)) {
410
+ continue; // Valid ADR naming, skip
411
+ }
412
+ violations.push({
413
+ file: relativePath,
414
+ category: category.name,
415
+ violationType: 'inconsistent_prefix',
416
+ expectedPattern: expectedRule.expected,
417
+ actual: fileName,
418
+ severity: 'info',
419
+ });
420
+ }
421
+ }
422
+ }
423
+ }
424
+ return violations;
425
+ }
426
+ /**
427
+ * Detect duplicate documents based on title similarity and content
428
+ */
429
+ detectDuplicates(categories) {
430
+ const duplicates = [];
431
+ const titleMap = new Map(); // Use Set to prevent duplicates
432
+ // Standard files that intentionally have same name across folders
433
+ const standardOrganizationalFiles = [
434
+ 'readme', 'feature', 'api', 'changelog', 'license', 'contributing', 'index',
435
+ ];
436
+ // Group files by normalized title
437
+ for (const category of categories) {
438
+ for (const doc of category.files) {
439
+ // Normalize title: lowercase, remove numbers, dashes, underscores
440
+ const normalizedTitle = doc.name
441
+ .toLowerCase()
442
+ .replace(/[\d_-]+/g, '')
443
+ .replace(/\s+/g, '');
444
+ // Skip standard organizational files from same_title detection
445
+ if (standardOrganizationalFiles.includes(normalizedTitle.replace(/\.md$/, ''))) {
446
+ continue;
447
+ }
448
+ if (!titleMap.has(normalizedTitle)) {
449
+ titleMap.set(normalizedTitle, new Set());
450
+ }
451
+ titleMap.get(normalizedTitle).add(doc.path); // Use add() for Set
452
+ }
453
+ }
454
+ // Find duplicates with same normalized title
455
+ for (const [normalizedTitle, filesSet] of titleMap) {
456
+ const files = Array.from(filesSet); // Convert Set to Array
457
+ if (files.length > 1 && normalizedTitle.length > 3) {
458
+ // Check for exact or near duplicates by comparing content
459
+ const contentHashes = new Map(); // Use Set
460
+ for (const filePath of files) {
461
+ try {
462
+ const content = fs.readFileSync(filePath, 'utf-8');
463
+ // Create simple content hash (first 500 chars normalized)
464
+ const contentSample = content
465
+ .slice(0, 500)
466
+ .toLowerCase()
467
+ .replace(/\s+/g, '')
468
+ .replace(/[^a-z0-9]/g, '');
469
+ if (!contentHashes.has(contentSample)) {
470
+ contentHashes.set(contentSample, new Set());
471
+ }
472
+ contentHashes.get(contentSample).add(filePath);
473
+ }
474
+ catch {
475
+ // Skip files that can't be read
476
+ }
477
+ }
478
+ // Check for exact content duplicates
479
+ let hasExactDupes = false;
480
+ for (const [, sameContentFilesSet] of contentHashes) {
481
+ const sameContentFiles = Array.from(sameContentFilesSet);
482
+ if (sameContentFiles.length > 1) {
483
+ hasExactDupes = true;
484
+ duplicates.push({
485
+ files: sameContentFiles.map(f => path.relative(this.projectPath, f)),
486
+ similarity: 100,
487
+ duplicateType: 'exact',
488
+ });
489
+ }
490
+ }
491
+ // If no exact duplicates but same title, mark as same_title
492
+ // But only if files are in the same documentation type (not adr vs hld vs concepts)
493
+ if (!hasExactDupes && files.length > 1) {
494
+ const relativePaths = files.map(f => path.relative(this.projectPath, f));
495
+ // Group by documentation type to avoid cross-type false positives
496
+ const docTypes = ['adr', 'hld', 'concepts', 'specs', 'guides'];
497
+ const filesByType = new Map();
498
+ for (const file of relativePaths) {
499
+ // Determine doc type from path
500
+ let docType = 'other';
501
+ for (const type of docTypes) {
502
+ if (file.includes(`/${type}/`) || file.includes(`\\${type}\\`)) {
503
+ docType = type;
504
+ break;
505
+ }
506
+ }
507
+ if (!filesByType.has(docType)) {
508
+ filesByType.set(docType, []);
509
+ }
510
+ filesByType.get(docType).push(file);
511
+ }
512
+ // Only report as duplicate if multiple files of same doc type
513
+ for (const [, sameTypeFiles] of filesByType) {
514
+ if (sameTypeFiles.length > 1) {
515
+ // Filter out archived feature specs - different phases of same feature may share user stories
516
+ const archivedFeatureFiles = sameTypeFiles.filter(f => f.includes('/_archive/FS-'));
517
+ if (archivedFeatureFiles.length === sameTypeFiles.length && sameTypeFiles.length > 0) {
518
+ // All files are archived feature specs - this is likely intentional phased features
519
+ continue;
520
+ }
521
+ // Filter out SUPERSEDED ADRs - these are intentional redirects, not duplicates
522
+ const nonSupersededFiles = sameTypeFiles.filter(file => {
523
+ try {
524
+ const fullPath = path.join(this.projectPath, file);
525
+ const content = fs.readFileSync(fullPath, 'utf-8').slice(0, 500);
526
+ // Check if file is marked as SUPERSEDED (ADR convention)
527
+ return !content.includes('SUPERSEDED');
528
+ }
529
+ catch {
530
+ return true; // Include if can't read
531
+ }
532
+ });
533
+ // Only report if multiple non-superseded files remain
534
+ if (nonSupersededFiles.length > 1) {
535
+ duplicates.push({
536
+ files: nonSupersededFiles,
537
+ similarity: 80,
538
+ duplicateType: 'same_title',
539
+ });
540
+ }
541
+ }
542
+ }
543
+ }
544
+ }
545
+ }
546
+ return duplicates;
547
+ }
548
+ /**
549
+ * Detect discrepancies in documentation (broken links, orphaned refs)
550
+ */
551
+ async detectDiscrepancies(categories) {
552
+ const discrepancies = [];
553
+ const allDocPaths = new Set();
554
+ // Build set of all doc paths
555
+ for (const category of categories) {
556
+ for (const doc of category.files) {
557
+ allDocPaths.add(doc.path);
558
+ allDocPaths.add(path.basename(doc.path));
559
+ allDocPaths.add(doc.name);
560
+ }
561
+ }
562
+ // Check each document for broken links and orphaned references
563
+ for (const category of categories) {
564
+ for (const doc of category.files) {
565
+ try {
566
+ const content = fs.readFileSync(doc.path, 'utf-8');
567
+ const relativePath = path.relative(this.projectPath, doc.path);
568
+ // Strip code blocks to avoid false positives from documentation examples
569
+ const contentWithoutCodeBlocks = content.replace(/```[\s\S]*?```/g, '');
570
+ // Check for markdown links to other docs (using content without code blocks)
571
+ const linkPattern = /\[([^\]]+)\]\(([^)]+\.md)\)/g;
572
+ let match;
573
+ while ((match = linkPattern.exec(contentWithoutCodeBlocks)) !== null) {
574
+ const linkedPath = match[2];
575
+ const absoluteLinkedPath = path.resolve(path.dirname(doc.path), linkedPath);
576
+ if (!fs.existsSync(absoluteLinkedPath)) {
577
+ // For links to increments folder, also check _archive
578
+ let isArchivedIncrement = false;
579
+ if (linkedPath.includes('/increments/') && !linkedPath.includes('/_archive/')) {
580
+ // Extract increment ID and check archive
581
+ const incrementMatch = linkedPath.match(/increments\/(\d{4}-[a-z0-9-]+)/);
582
+ if (incrementMatch) {
583
+ const archivedPath = absoluteLinkedPath.replace(`/increments/${incrementMatch[1]}`, `/increments/_archive/${incrementMatch[1]}`);
584
+ isArchivedIncrement = fs.existsSync(archivedPath);
585
+ }
586
+ }
587
+ if (!isArchivedIncrement) {
588
+ discrepancies.push({
589
+ file: relativePath,
590
+ discrepancyType: 'broken_link',
591
+ description: `Broken link to: ${linkedPath}`,
592
+ relatedFiles: [linkedPath],
593
+ });
594
+ }
595
+ }
596
+ }
597
+ // Check for references to increment IDs that don't exist (using content without code blocks)
598
+ const incrementRefPattern = /(?:increment|spec)[:\s]+(\d{4}-[a-z0-9-]+)/gi;
599
+ while ((match = incrementRefPattern.exec(contentWithoutCodeBlocks)) !== null) {
600
+ const incrementId = match[1];
601
+ const incrementPath = path.join(this.projectPath, '.specweave/increments', incrementId);
602
+ const archivedPath = path.join(this.projectPath, '.specweave/increments/_archive', incrementId);
603
+ if (!fs.existsSync(incrementPath) && !fs.existsSync(archivedPath)) {
604
+ discrepancies.push({
605
+ file: relativePath,
606
+ discrepancyType: 'orphaned_reference',
607
+ description: `Reference to non-existent increment: ${incrementId}`,
608
+ });
609
+ }
610
+ }
611
+ // Check for outdated version references (e.g., v0.XX references)
612
+ // Skip historical references like "Before v0.18.3", "NEW in v0.12.0", "Added in v0.13.0"
613
+ const versionPattern = /\bv(0\.\d+\.\d+)\b/g;
614
+ const historicalPatterns = [
615
+ /before\s+v[\d.]+/i,
616
+ /after\s+v[\d.]+/i,
617
+ /\bnew\s+in\s+v[\d.]+/i,
618
+ /added\s+in\s+v[\d.]+/i,
619
+ /since\s+v[\d.]+/i,
620
+ /v[\d.]+\s*\+/i, // v0.13.0+ format
621
+ /v[\d.]+\s*(and\s+earlier|or\s+earlier)/i,
622
+ /introduced\s+in\s+v[\d.]+/i,
623
+ /deprecated\s+(in|since)\s+v[\d.]+/i,
624
+ /removed\s+in\s+v[\d.]+/i,
625
+ /\*\*version\*\*:\s*v[\d.]+/i, // **Version**: vX.X.X format
626
+ /\(planned\)/i, // (planned) indicator
627
+ ];
628
+ const isHistoricalDoc = historicalPatterns.some(p => p.test(content));
629
+ // Only check versions in non-historical documents
630
+ if (!isHistoricalDoc) {
631
+ while ((match = versionPattern.exec(content)) !== null) {
632
+ const referencedVersion = match[1];
633
+ // Flag very old versions (before 0.20)
634
+ const majorMinor = referencedVersion.split('.').slice(0, 2).join('.');
635
+ if (parseFloat(majorMinor) < 0.2) {
636
+ discrepancies.push({
637
+ file: relativePath,
638
+ discrepancyType: 'outdated_version',
639
+ description: `Potentially outdated version reference: v${referencedVersion}`,
640
+ });
641
+ }
642
+ }
643
+ }
644
+ }
645
+ catch {
646
+ // Skip files that can't be read
647
+ }
648
+ }
649
+ }
650
+ return discrepancies;
651
+ }
652
+ /**
653
+ * Calculate documentation health score
654
+ */
655
+ calculateHealthScore(categories, mismatches, namingViolations) {
656
+ // Calculate freshness (based on document age)
657
+ const now = new Date();
658
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
659
+ let freshDocs = 0;
660
+ let totalDocs = 0;
661
+ for (const cat of categories) {
662
+ for (const doc of cat.files) {
663
+ totalDocs++;
664
+ if (doc.lastModified > thirtyDaysAgo) {
665
+ freshDocs++;
666
+ }
667
+ }
668
+ }
669
+ const freshness = totalDocs > 0 ? Math.round((freshDocs / totalDocs) * 100) : 0;
670
+ // Calculate coverage (docs with ACs vs total docs)
671
+ let docsWithACs = 0;
672
+ for (const cat of categories) {
673
+ docsWithACs += cat.files.filter(d => d.hasAcceptanceCriteria).length;
674
+ }
675
+ const coverage = totalDocs > 0 ? Math.round((docsWithACs / totalDocs) * 100) : 0;
676
+ // Calculate accuracy (ACs without mismatches + naming violations penalty)
677
+ let totalACs = 0;
678
+ for (const cat of categories) {
679
+ for (const doc of cat.files) {
680
+ totalACs += doc.acceptanceCriteria.length;
681
+ }
682
+ }
683
+ const mismatchCount = mismatches.length;
684
+ // Apply naming violation penalty (errors: -3%, warnings: -1%, info: -0.5%)
685
+ const namingPenalty = namingViolations.reduce((penalty, v) => {
686
+ if (v.severity === 'error')
687
+ return penalty + 3;
688
+ if (v.severity === 'warning')
689
+ return penalty + 1;
690
+ return penalty + 0.5;
691
+ }, 0);
692
+ const accuracy = totalACs > 0
693
+ ? Math.max(0, Math.round(((totalACs - mismatchCount) / totalACs) * 100) - Math.min(namingPenalty, 20))
694
+ : Math.max(0, 100 - Math.min(namingPenalty, 20));
695
+ // Calculate overall score
696
+ const overall = Math.round((freshness * 0.2) + (coverage * 0.3) + (accuracy * 0.5));
697
+ // Determine grade
698
+ let grade;
699
+ if (overall >= 90)
700
+ grade = 'A';
701
+ else if (overall >= 80)
702
+ grade = 'B';
703
+ else if (overall >= 70)
704
+ grade = 'C';
705
+ else if (overall >= 60)
706
+ grade = 'D';
707
+ else
708
+ grade = 'F';
709
+ return {
710
+ overall,
711
+ grade,
712
+ freshness,
713
+ coverage,
714
+ accuracy,
715
+ trend: 'stable', // Would need historical data for actual trend
716
+ };
717
+ }
718
+ /**
719
+ * Generate recommendations based on analysis
720
+ */
721
+ generateRecommendations(categories, mismatches, healthScore, namingViolations, duplicates) {
722
+ const recommendations = [];
723
+ // Freshness recommendations
724
+ if (healthScore.freshness < 50) {
725
+ recommendations.push('Documentation freshness is low. Consider reviewing and updating docs that are over 30 days old.');
726
+ }
727
+ // Coverage recommendations
728
+ if (healthScore.coverage < 60) {
729
+ recommendations.push('Documentation coverage is limited. Add acceptance criteria to more documents.');
730
+ }
731
+ // Mismatch recommendations
732
+ if (mismatches.length > 0) {
733
+ const ghostCompletions = mismatches.filter(m => m.mismatchType === 'ghost_completion');
734
+ if (ghostCompletions.length > 0) {
735
+ recommendations.push(`${ghostCompletions.length} acceptance criteria are marked complete but lack code evidence. Review: ${ghostCompletions.slice(0, 3).map(m => m.criterionId).join(', ')}${ghostCompletions.length > 3 ? '...' : ''}`);
736
+ }
737
+ }
738
+ // Naming convention recommendations
739
+ if (namingViolations.length > 0) {
740
+ const allCapsViolations = namingViolations.filter(v => v.violationType === 'all_caps');
741
+ const dateSuffixViolations = namingViolations.filter(v => v.violationType === 'date_suffix');
742
+ if (allCapsViolations.length > 0) {
743
+ recommendations.push(`${allCapsViolations.length} files use ALL CAPS naming (e.g., ${allCapsViolations[0].actual}). Rename to lowercase-kebab-case for consistency.`);
744
+ }
745
+ if (dateSuffixViolations.length > 0) {
746
+ recommendations.push(`${dateSuffixViolations.length} files include dates in names. Use git history for versioning instead of date suffixes.`);
747
+ }
748
+ }
749
+ // Duplicate recommendations
750
+ if (duplicates.length > 0) {
751
+ const exactDupes = duplicates.filter(d => d.duplicateType === 'exact');
752
+ if (exactDupes.length > 0) {
753
+ recommendations.push(`${exactDupes.length} sets of duplicate documents detected. Consider consolidating: ${exactDupes[0].files.slice(0, 2).join(', ')}`);
754
+ }
755
+ }
756
+ // Category-specific recommendations
757
+ const hasADRs = categories.some(c => c.name === 'ADRs' && c.fileCount > 0);
758
+ if (!hasADRs) {
759
+ recommendations.push('No Architecture Decision Records found. Consider documenting key architectural decisions in .specweave/docs/internal/architecture/adr/');
760
+ }
761
+ const hasGovernance = categories.some(c => c.name === 'Governance' && c.fileCount > 0);
762
+ if (!hasGovernance) {
763
+ recommendations.push('No governance documentation found. Consider adding coding standards to .specweave/docs/internal/governance/');
764
+ }
765
+ // Large folder organization recommendations
766
+ for (const category of categories) {
767
+ if (category.fileCount > 30) {
768
+ recommendations.push(`📁 "${category.name}" has ${category.fileCount} files. Run /specweave:organize-docs to generate themed navigation indexes for easier browsing.`);
769
+ }
770
+ }
771
+ return recommendations;
772
+ }
773
+ }
774
+ /**
775
+ * Generate markdown report from enterprise analysis
776
+ */
777
+ export function generateEnterpriseReport(report) {
778
+ const lines = [];
779
+ lines.push('# Enterprise Documentation Health Report');
780
+ lines.push('');
781
+ lines.push(`*Generated: ${report.generatedAt.toLocaleString()}*`);
782
+ lines.push('');
783
+ // Health Score Summary
784
+ lines.push('## Documentation Health Score');
785
+ lines.push('');
786
+ lines.push(`| Metric | Score | Grade |`);
787
+ lines.push(`|--------|-------|-------|`);
788
+ lines.push(`| **Overall** | ${report.healthScore.overall}% | **${report.healthScore.grade}** |`);
789
+ lines.push(`| Freshness | ${report.healthScore.freshness}% | - |`);
790
+ lines.push(`| Coverage | ${report.healthScore.coverage}% | - |`);
791
+ lines.push(`| Accuracy | ${report.healthScore.accuracy}% | - |`);
792
+ lines.push('');
793
+ // Document Categories
794
+ lines.push('## Documentation Categories');
795
+ lines.push('');
796
+ lines.push('| Category | Documents | Last Updated |');
797
+ lines.push('|----------|-----------|--------------|');
798
+ for (const cat of report.categories) {
799
+ const lastUpdated = cat.lastUpdated
800
+ ? cat.lastUpdated.toLocaleDateString()
801
+ : 'N/A';
802
+ lines.push(`| ${cat.name} | ${cat.fileCount} | ${lastUpdated} |`);
803
+ }
804
+ lines.push('');
805
+ lines.push(`**Total Documents**: ${report.totalDocuments}`);
806
+ lines.push('');
807
+ // Mismatches
808
+ if (report.mismatches.length > 0) {
809
+ lines.push('## Spec-Code Mismatches');
810
+ lines.push('');
811
+ lines.push('| AC ID | Type | Confidence | File |');
812
+ lines.push('|-------|------|------------|------|');
813
+ for (const mismatch of report.mismatches.slice(0, 20)) {
814
+ const typeEmoji = mismatch.mismatchType === 'ghost_completion' ? '👻' :
815
+ mismatch.mismatchType === 'partial_implementation' ? '⚠️' : '❓';
816
+ const fileName = path.basename(mismatch.specFile);
817
+ lines.push(`| ${mismatch.criterionId} | ${typeEmoji} ${mismatch.mismatchType} | ${mismatch.confidence}% | ${fileName} |`);
818
+ }
819
+ if (report.mismatches.length > 20) {
820
+ lines.push(`| ... | ... | ... | *${report.mismatches.length - 20} more* |`);
821
+ }
822
+ lines.push('');
823
+ }
824
+ // Naming Violations
825
+ if (report.namingViolations.length > 0) {
826
+ lines.push('## Naming Convention Violations');
827
+ lines.push('');
828
+ lines.push('| File | Type | Severity | Expected Pattern |');
829
+ lines.push('|------|------|----------|------------------|');
830
+ const severityEmoji = { error: '🔴', warning: '🟡', info: '🔵' };
831
+ for (const violation of report.namingViolations.slice(0, 25)) {
832
+ const emoji = severityEmoji[violation.severity];
833
+ lines.push(`| ${violation.file} | ${emoji} ${violation.violationType} | ${violation.severity} | ${violation.expectedPattern} |`);
834
+ }
835
+ if (report.namingViolations.length > 25) {
836
+ lines.push(`| ... | ... | ... | *${report.namingViolations.length - 25} more* |`);
837
+ }
838
+ lines.push('');
839
+ }
840
+ // Duplicates
841
+ if (report.duplicates.length > 0) {
842
+ lines.push('## Duplicate Documents');
843
+ lines.push('');
844
+ lines.push('| Files | Similarity | Type |');
845
+ lines.push('|-------|------------|------|');
846
+ for (const dup of report.duplicates.slice(0, 15)) {
847
+ const filesStr = dup.files.slice(0, 3).join(', ') + (dup.files.length > 3 ? '...' : '');
848
+ lines.push(`| ${filesStr} | ${dup.similarity}% | ${dup.duplicateType} |`);
849
+ }
850
+ if (report.duplicates.length > 15) {
851
+ lines.push(`| ... | ... | *${report.duplicates.length - 15} more* |`);
852
+ }
853
+ lines.push('');
854
+ }
855
+ // Discrepancies
856
+ if (report.discrepancies.length > 0) {
857
+ lines.push('## Documentation Discrepancies');
858
+ lines.push('');
859
+ lines.push('| File | Type | Description |');
860
+ lines.push('|------|------|-------------|');
861
+ const discrepancyEmoji = {
862
+ broken_link: '🔗',
863
+ orphaned_reference: '👻',
864
+ outdated_version: '📅',
865
+ conflicting_info: '⚠️',
866
+ };
867
+ for (const disc of report.discrepancies.slice(0, 20)) {
868
+ const emoji = discrepancyEmoji[disc.discrepancyType] ?? '❓';
869
+ lines.push(`| ${disc.file} | ${emoji} ${disc.discrepancyType} | ${disc.description} |`);
870
+ }
871
+ if (report.discrepancies.length > 20) {
872
+ lines.push(`| ... | ... | *${report.discrepancies.length - 20} more* |`);
873
+ }
874
+ lines.push('');
875
+ }
876
+ // Recommendations
877
+ if (report.recommendations.length > 0) {
878
+ lines.push('## Recommendations');
879
+ lines.push('');
880
+ for (const rec of report.recommendations) {
881
+ lines.push(`- ${rec}`);
882
+ }
883
+ lines.push('');
884
+ }
885
+ return lines.join('\n');
886
+ }
887
+ //# sourceMappingURL=enterprise-analyzer.js.map