create-universal-ai-context 2.0.0 → 2.1.2

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.
@@ -0,0 +1,920 @@
1
+ /**
2
+ * AI Context Engineering - Drift Checker Module
3
+ *
4
+ * Validates documentation references against the actual codebase.
5
+ * Detects stale file paths, outdated line numbers, missing functions, etc.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { glob } = require('glob');
11
+
12
+ /**
13
+ * Drift severity levels
14
+ */
15
+ const DRIFT_LEVEL = {
16
+ NONE: 'none',
17
+ LOW: 'low', // Minor issues, easily fixable
18
+ MEDIUM: 'medium', // Requires review
19
+ HIGH: 'high', // Significant issues
20
+ CRITICAL: 'critical' // Major issues, documentation may be unusable
21
+ };
22
+
23
+ /**
24
+ * Health status thresholds
25
+ */
26
+ const HEALTH_THRESHOLDS = {
27
+ HEALTHY: 90, // 90-100%
28
+ NEEDS_UPDATE: 70, // 70-89%
29
+ STALE: 50, // 50-69%
30
+ CRITICAL: 0 // < 50%
31
+ };
32
+
33
+ /**
34
+ * Patterns to extract references from markdown content
35
+ */
36
+ const REFERENCE_PATTERNS = {
37
+ // File paths in backticks: `src/auth.py`
38
+ backtickPath: /`([a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+)`/g,
39
+
40
+ // Line references: file.js:123 or file.py:45-67
41
+ lineReference: /([a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+):(\d+)(?:-(\d+))?/g,
42
+
43
+ // Anchor references: file.py::function_name() or file.js::ClassName
44
+ anchorReference: /([a-zA-Z0-9_\-./\\]+\.[a-zA-Z0-9]+)::(\w+)(?:\(\))?/g,
45
+
46
+ // Directory references: src/components/
47
+ directoryReference: /`?([a-zA-Z0-9_\-./\\]+\/)`?/g,
48
+
49
+ // Markdown links: [text](./path/to/file.md)
50
+ markdownLink: /\[([^\]]+)\]\(([^)]+)\)/g,
51
+
52
+ // Tech stack mentions
53
+ techStackMention: /(?:built\s+with|using|technologies?|stack)[:\s]+([^\n]+)/gi
54
+ };
55
+
56
+ /**
57
+ * Common source file extensions (for filtering path matches)
58
+ */
59
+ const SOURCE_EXTENSIONS = new Set([
60
+ 'js', 'ts', 'jsx', 'tsx', 'mjs', 'cjs',
61
+ 'py', 'pyw', 'pyi',
62
+ 'go', 'rs', 'rb', 'java', 'kt', 'scala',
63
+ 'c', 'cpp', 'cc', 'h', 'hpp',
64
+ 'cs', 'fs', 'vb',
65
+ 'php', 'swift', 'dart',
66
+ 'md', 'json', 'yaml', 'yml', 'toml',
67
+ 'sh', 'bash', 'zsh', 'ps1',
68
+ 'sql', 'graphql'
69
+ ]);
70
+
71
+ /**
72
+ * Extract all reference types from markdown content
73
+ * @param {string} content - Markdown content
74
+ * @returns {object} Categorized references
75
+ */
76
+ function extractAllReferences(content) {
77
+ const references = {
78
+ filePaths: [],
79
+ lineReferences: [],
80
+ anchorReferences: [],
81
+ directoryReferences: [],
82
+ markdownLinks: [],
83
+ techStackClaims: []
84
+ };
85
+
86
+ // Reset regex lastIndex
87
+ const resetRegex = (regex) => { regex.lastIndex = 0; return regex; };
88
+
89
+ // Extract file paths from backticks
90
+ let match;
91
+ const backtickRegex = resetRegex(REFERENCE_PATTERNS.backtickPath);
92
+ while ((match = backtickRegex.exec(content)) !== null) {
93
+ const filePath = match[1].replace(/\\/g, '/');
94
+ const ext = path.extname(filePath).slice(1).toLowerCase();
95
+ if (SOURCE_EXTENSIONS.has(ext) || ext === '') {
96
+ references.filePaths.push({
97
+ type: 'file_path',
98
+ file: filePath,
99
+ original: match[0],
100
+ position: match.index
101
+ });
102
+ }
103
+ }
104
+
105
+ // Extract line references
106
+ const lineRegex = resetRegex(REFERENCE_PATTERNS.lineReference);
107
+ while ((match = lineRegex.exec(content)) !== null) {
108
+ references.lineReferences.push({
109
+ type: 'line_reference',
110
+ file: match[1].replace(/\\/g, '/'),
111
+ line: parseInt(match[2], 10),
112
+ endLine: match[3] ? parseInt(match[3], 10) : null,
113
+ original: match[0],
114
+ position: match.index
115
+ });
116
+ }
117
+
118
+ // Extract anchor references
119
+ const anchorRegex = resetRegex(REFERENCE_PATTERNS.anchorReference);
120
+ while ((match = anchorRegex.exec(content)) !== null) {
121
+ references.anchorReferences.push({
122
+ type: 'anchor_reference',
123
+ file: match[1].replace(/\\/g, '/'),
124
+ anchor: match[2],
125
+ original: match[0],
126
+ position: match.index
127
+ });
128
+ }
129
+
130
+ // Extract directory references
131
+ const dirRegex = resetRegex(REFERENCE_PATTERNS.directoryReference);
132
+ while ((match = dirRegex.exec(content)) !== null) {
133
+ const dir = match[1].replace(/\\/g, '/');
134
+ // Filter out URLs and common false positives
135
+ if (!dir.includes('://') && !dir.startsWith('http') && dir.length > 2) {
136
+ references.directoryReferences.push({
137
+ type: 'directory_reference',
138
+ directory: dir,
139
+ original: match[0],
140
+ position: match.index
141
+ });
142
+ }
143
+ }
144
+
145
+ // Extract markdown links to local files
146
+ const linkRegex = resetRegex(REFERENCE_PATTERNS.markdownLink);
147
+ while ((match = linkRegex.exec(content)) !== null) {
148
+ const href = match[2];
149
+ // Filter out external URLs, anchors-only, and mailto
150
+ if (!href.startsWith('http') &&
151
+ !href.startsWith('#') &&
152
+ !href.startsWith('mailto:') &&
153
+ !href.startsWith('tel:')) {
154
+ references.markdownLinks.push({
155
+ type: 'markdown_link',
156
+ text: match[1],
157
+ href: href.replace(/\\/g, '/'),
158
+ original: match[0],
159
+ position: match.index
160
+ });
161
+ }
162
+ }
163
+
164
+ // Extract tech stack claims
165
+ const techRegex = resetRegex(REFERENCE_PATTERNS.techStackMention);
166
+ while ((match = techRegex.exec(content)) !== null) {
167
+ const technologies = match[1]
168
+ .split(/[,;]+/)
169
+ .map(t => t.trim())
170
+ .filter(t => t.length > 0 && t.length < 50);
171
+ if (technologies.length > 0) {
172
+ references.techStackClaims.push({
173
+ type: 'tech_stack_claim',
174
+ technologies,
175
+ original: match[0],
176
+ position: match.index
177
+ });
178
+ }
179
+ }
180
+
181
+ // Deduplicate references
182
+ references.filePaths = dedupeByFile(references.filePaths);
183
+ references.lineReferences = dedupeByOriginal(references.lineReferences);
184
+ references.anchorReferences = dedupeByOriginal(references.anchorReferences);
185
+ references.directoryReferences = dedupeByOriginal(references.directoryReferences);
186
+ references.markdownLinks = dedupeByOriginal(references.markdownLinks);
187
+
188
+ return references;
189
+ }
190
+
191
+ /**
192
+ * Deduplicate by file path
193
+ */
194
+ function dedupeByFile(refs) {
195
+ const seen = new Set();
196
+ return refs.filter(r => {
197
+ if (seen.has(r.file)) return false;
198
+ seen.add(r.file);
199
+ return true;
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Deduplicate by original text
205
+ */
206
+ function dedupeByOriginal(refs) {
207
+ const seen = new Set();
208
+ return refs.filter(r => {
209
+ if (seen.has(r.original)) return false;
210
+ seen.add(r.original);
211
+ return true;
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Validate a file path reference
217
+ * @param {object} ref - Reference object
218
+ * @param {string} projectRoot - Project root directory
219
+ * @returns {object} Validation result
220
+ */
221
+ function validateFilePath(ref, projectRoot) {
222
+ const fullPath = path.join(projectRoot, ref.file);
223
+
224
+ if (fs.existsSync(fullPath)) {
225
+ return { valid: true };
226
+ }
227
+
228
+ // Try to find similar file
229
+ const suggestion = findSimilarFile(ref.file, projectRoot);
230
+
231
+ return {
232
+ valid: false,
233
+ level: DRIFT_LEVEL.CRITICAL,
234
+ issue: 'File not found',
235
+ suggestion: suggestion ? `Did you mean: ${suggestion}` : null
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Validate a line reference
241
+ * @param {object} ref - Reference object
242
+ * @param {string} projectRoot - Project root directory
243
+ * @returns {object} Validation result
244
+ */
245
+ function validateLineReference(ref, projectRoot) {
246
+ const fullPath = path.join(projectRoot, ref.file);
247
+
248
+ if (!fs.existsSync(fullPath)) {
249
+ return {
250
+ valid: false,
251
+ level: DRIFT_LEVEL.CRITICAL,
252
+ issue: 'Referenced file not found'
253
+ };
254
+ }
255
+
256
+ try {
257
+ const content = fs.readFileSync(fullPath, 'utf-8');
258
+ const lineCount = content.split('\n').length;
259
+
260
+ if (ref.line > lineCount) {
261
+ return {
262
+ valid: false,
263
+ level: DRIFT_LEVEL.HIGH,
264
+ issue: `Line ${ref.line} exceeds file length (${lineCount} lines)`,
265
+ suggestion: `File now has ${lineCount} lines`,
266
+ actualLineCount: lineCount
267
+ };
268
+ }
269
+
270
+ if (ref.endLine && ref.endLine > lineCount) {
271
+ return {
272
+ valid: false,
273
+ level: DRIFT_LEVEL.HIGH,
274
+ issue: `End line ${ref.endLine} exceeds file length (${lineCount} lines)`,
275
+ actualLineCount: lineCount
276
+ };
277
+ }
278
+
279
+ return { valid: true, lineCount };
280
+ } catch (error) {
281
+ return {
282
+ valid: false,
283
+ level: DRIFT_LEVEL.HIGH,
284
+ issue: `Cannot read file: ${error.message}`
285
+ };
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Validate an anchor reference (function/class)
291
+ * @param {object} ref - Reference object
292
+ * @param {string} projectRoot - Project root directory
293
+ * @returns {object} Validation result
294
+ */
295
+ function validateAnchorReference(ref, projectRoot) {
296
+ const fullPath = path.join(projectRoot, ref.file);
297
+
298
+ if (!fs.existsSync(fullPath)) {
299
+ return {
300
+ valid: false,
301
+ level: DRIFT_LEVEL.CRITICAL,
302
+ issue: 'Referenced file not found'
303
+ };
304
+ }
305
+
306
+ try {
307
+ const content = fs.readFileSync(fullPath, 'utf-8');
308
+ const ext = path.extname(ref.file).slice(1).toLowerCase();
309
+
310
+ // Get language-specific patterns
311
+ const patterns = getSymbolPatterns(ext);
312
+ const symbols = extractSymbols(content, patterns);
313
+
314
+ // Check if anchor exists
315
+ const foundSymbol = symbols.find(s =>
316
+ s.name === ref.anchor ||
317
+ s.name === ref.anchor.replace(/\(\)$/, '')
318
+ );
319
+
320
+ if (foundSymbol) {
321
+ return {
322
+ valid: true,
323
+ currentLine: foundSymbol.line,
324
+ symbolType: foundSymbol.type
325
+ };
326
+ }
327
+
328
+ // Suggest similar symbols
329
+ const similarSymbols = symbols
330
+ .filter(s => s.name.toLowerCase().includes(ref.anchor.toLowerCase().substring(0, 3)))
331
+ .slice(0, 5)
332
+ .map(s => s.name);
333
+
334
+ return {
335
+ valid: false,
336
+ level: DRIFT_LEVEL.HIGH,
337
+ issue: `Symbol '${ref.anchor}' not found in ${ref.file}`,
338
+ suggestion: similarSymbols.length > 0
339
+ ? `Available symbols: ${similarSymbols.join(', ')}`
340
+ : null,
341
+ availableSymbols: symbols.slice(0, 10).map(s => s.name)
342
+ };
343
+ } catch (error) {
344
+ return {
345
+ valid: false,
346
+ level: DRIFT_LEVEL.HIGH,
347
+ issue: `Cannot analyze file: ${error.message}`
348
+ };
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Validate a directory reference
354
+ * @param {object} ref - Reference object
355
+ * @param {string} projectRoot - Project root directory
356
+ * @returns {object} Validation result
357
+ */
358
+ function validateDirectory(ref, projectRoot) {
359
+ const fullPath = path.join(projectRoot, ref.directory);
360
+
361
+ if (fs.existsSync(fullPath)) {
362
+ const stat = fs.statSync(fullPath);
363
+ if (stat.isDirectory()) {
364
+ return { valid: true };
365
+ }
366
+ return {
367
+ valid: false,
368
+ level: DRIFT_LEVEL.MEDIUM,
369
+ issue: 'Path exists but is not a directory'
370
+ };
371
+ }
372
+
373
+ // Try to find similar directory
374
+ const suggestion = findSimilarDirectory(ref.directory, projectRoot);
375
+
376
+ return {
377
+ valid: false,
378
+ level: DRIFT_LEVEL.MEDIUM,
379
+ issue: 'Directory not found',
380
+ suggestion: suggestion ? `Did you mean: ${suggestion}` : null
381
+ };
382
+ }
383
+
384
+ /**
385
+ * Validate a markdown link
386
+ * @param {object} ref - Reference object
387
+ * @param {string} docDir - Directory containing the document
388
+ * @param {string} projectRoot - Project root directory
389
+ * @returns {object} Validation result
390
+ */
391
+ function validateMarkdownLink(ref, docDir, projectRoot) {
392
+ // Handle relative paths
393
+ let targetPath;
394
+ if (ref.href.startsWith('./') || ref.href.startsWith('../')) {
395
+ targetPath = path.resolve(docDir, ref.href);
396
+ } else if (ref.href.startsWith('/')) {
397
+ targetPath = path.join(projectRoot, ref.href);
398
+ } else {
399
+ targetPath = path.resolve(docDir, ref.href);
400
+ }
401
+
402
+ // Remove anchor from path
403
+ const pathWithoutAnchor = targetPath.split('#')[0];
404
+
405
+ if (fs.existsSync(pathWithoutAnchor)) {
406
+ return { valid: true };
407
+ }
408
+
409
+ return {
410
+ valid: false,
411
+ level: DRIFT_LEVEL.MEDIUM,
412
+ issue: `Link target not found: ${ref.href}`
413
+ };
414
+ }
415
+
416
+ /**
417
+ * Check drift for a single documentation file
418
+ * @param {string} docPath - Path to markdown file
419
+ * @param {string} projectRoot - Project root directory
420
+ * @returns {object} Drift report
421
+ */
422
+ function checkDocumentDrift(docPath, projectRoot = process.cwd()) {
423
+ const fullPath = path.isAbsolute(docPath) ? docPath : path.join(projectRoot, docPath);
424
+ const relativePath = path.isAbsolute(docPath) ? path.relative(projectRoot, docPath) : docPath;
425
+
426
+ if (!fs.existsSync(fullPath)) {
427
+ return {
428
+ document: relativePath,
429
+ status: 'error',
430
+ error: 'Document file not found',
431
+ healthScore: 0
432
+ };
433
+ }
434
+
435
+ try {
436
+ const content = fs.readFileSync(fullPath, 'utf-8');
437
+ const docDir = path.dirname(fullPath);
438
+ const references = extractAllReferences(content);
439
+ const issues = [];
440
+ const validRefs = [];
441
+
442
+ // Validate file paths
443
+ for (const ref of references.filePaths) {
444
+ const result = validateFilePath(ref, projectRoot);
445
+ if (result.valid) {
446
+ validRefs.push({ ...ref, ...result });
447
+ } else {
448
+ issues.push({ ...ref, ...result });
449
+ }
450
+ }
451
+
452
+ // Validate line references
453
+ for (const ref of references.lineReferences) {
454
+ const result = validateLineReference(ref, projectRoot);
455
+ if (result.valid) {
456
+ validRefs.push({ ...ref, ...result });
457
+ } else {
458
+ issues.push({ ...ref, ...result });
459
+ }
460
+ }
461
+
462
+ // Validate anchor references
463
+ for (const ref of references.anchorReferences) {
464
+ const result = validateAnchorReference(ref, projectRoot);
465
+ if (result.valid) {
466
+ validRefs.push({ ...ref, ...result });
467
+ } else {
468
+ issues.push({ ...ref, ...result });
469
+ }
470
+ }
471
+
472
+ // Validate directory references
473
+ for (const ref of references.directoryReferences) {
474
+ const result = validateDirectory(ref, projectRoot);
475
+ if (result.valid) {
476
+ validRefs.push({ ...ref, ...result });
477
+ } else {
478
+ issues.push({ ...ref, ...result });
479
+ }
480
+ }
481
+
482
+ // Validate markdown links
483
+ for (const ref of references.markdownLinks) {
484
+ const result = validateMarkdownLink(ref, docDir, projectRoot);
485
+ if (result.valid) {
486
+ validRefs.push({ ...ref, ...result });
487
+ } else {
488
+ issues.push({ ...ref, ...result });
489
+ }
490
+ }
491
+
492
+ // Calculate health score
493
+ const totalRefs = validRefs.length + issues.length;
494
+ const healthScore = totalRefs > 0
495
+ ? Math.round((validRefs.length / totalRefs) * 100)
496
+ : 100;
497
+
498
+ // Determine status
499
+ let status;
500
+ if (healthScore >= HEALTH_THRESHOLDS.HEALTHY) {
501
+ status = 'healthy';
502
+ } else if (healthScore >= HEALTH_THRESHOLDS.NEEDS_UPDATE) {
503
+ status = 'needs_update';
504
+ } else if (healthScore >= HEALTH_THRESHOLDS.STALE) {
505
+ status = 'stale';
506
+ } else {
507
+ status = 'critical';
508
+ }
509
+
510
+ // Determine overall drift level
511
+ const level = calculateDriftLevel(issues);
512
+
513
+ return {
514
+ document: relativePath,
515
+ status,
516
+ level,
517
+ healthScore,
518
+ summary: {
519
+ total: totalRefs,
520
+ valid: validRefs.length,
521
+ issues: issues.length
522
+ },
523
+ references: {
524
+ valid: validRefs,
525
+ invalid: issues
526
+ },
527
+ byType: {
528
+ filePaths: references.filePaths.length,
529
+ lineReferences: references.lineReferences.length,
530
+ anchorReferences: references.anchorReferences.length,
531
+ directories: references.directoryReferences.length,
532
+ markdownLinks: references.markdownLinks.length,
533
+ techStackClaims: references.techStackClaims.length
534
+ },
535
+ checkedAt: new Date().toISOString()
536
+ };
537
+ } catch (error) {
538
+ return {
539
+ document: relativePath,
540
+ status: 'error',
541
+ error: `Failed to check document: ${error.message}`,
542
+ healthScore: 0
543
+ };
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Generate comprehensive drift report for multiple documents
549
+ * @param {string[]} docPaths - Array of document paths to analyze
550
+ * @param {string} projectRoot - Project root directory
551
+ * @returns {object} Complete drift report
552
+ */
553
+ function generateDriftReport(docPaths, projectRoot = process.cwd()) {
554
+ const report = {
555
+ version: '2.0.0',
556
+ generatedAt: new Date().toISOString(),
557
+ projectRoot,
558
+ summary: {
559
+ totalDocuments: 0,
560
+ healthyDocuments: 0,
561
+ documentsWithIssues: 0,
562
+ overallHealthScore: 0,
563
+ totalReferences: 0,
564
+ validReferences: 0,
565
+ invalidReferences: 0
566
+ },
567
+ healthBreakdown: {
568
+ filePaths: { valid: 0, invalid: 0, score: 100 },
569
+ lineReferences: { valid: 0, invalid: 0, score: 100 },
570
+ anchorReferences: { valid: 0, invalid: 0, score: 100 },
571
+ directories: { valid: 0, invalid: 0, score: 100 },
572
+ markdownLinks: { valid: 0, invalid: 0, score: 100 }
573
+ },
574
+ documents: [],
575
+ suggestedFixes: []
576
+ };
577
+
578
+ for (const docPath of docPaths) {
579
+ const result = checkDocumentDrift(docPath, projectRoot);
580
+ report.documents.push(result);
581
+
582
+ if (result.status === 'error') continue;
583
+
584
+ report.summary.totalDocuments++;
585
+ report.summary.totalReferences += result.summary.total;
586
+ report.summary.validReferences += result.summary.valid;
587
+ report.summary.invalidReferences += result.summary.issues;
588
+
589
+ if (result.status === 'healthy') {
590
+ report.summary.healthyDocuments++;
591
+ } else {
592
+ report.summary.documentsWithIssues++;
593
+ }
594
+
595
+ // Aggregate type stats
596
+ for (const ref of result.references.valid) {
597
+ const typeKey = getTypeKey(ref.type);
598
+ if (typeKey && report.healthBreakdown[typeKey]) {
599
+ report.healthBreakdown[typeKey].valid++;
600
+ }
601
+ }
602
+ for (const ref of result.references.invalid) {
603
+ const typeKey = getTypeKey(ref.type);
604
+ if (typeKey && report.healthBreakdown[typeKey]) {
605
+ report.healthBreakdown[typeKey].invalid++;
606
+ }
607
+
608
+ // Generate suggested fixes
609
+ if (ref.suggestion) {
610
+ report.suggestedFixes.push({
611
+ document: result.document,
612
+ original: ref.original,
613
+ issue: ref.issue,
614
+ suggestion: ref.suggestion,
615
+ level: ref.level
616
+ });
617
+ }
618
+ }
619
+ }
620
+
621
+ // Calculate overall health score
622
+ report.summary.overallHealthScore = report.summary.totalReferences > 0
623
+ ? Math.round((report.summary.validReferences / report.summary.totalReferences) * 100)
624
+ : 100;
625
+
626
+ // Calculate per-type scores
627
+ for (const [type, stats] of Object.entries(report.healthBreakdown)) {
628
+ const total = stats.valid + stats.invalid;
629
+ stats.score = total > 0 ? Math.round((stats.valid / total) * 100) : 100;
630
+ }
631
+
632
+ return report;
633
+ }
634
+
635
+ /**
636
+ * Find documentation files in project
637
+ * @param {string} projectRoot - Project root
638
+ * @returns {Promise<string[]>} Array of doc paths
639
+ */
640
+ async function findDocumentationFiles(projectRoot) {
641
+ const patterns = [
642
+ 'CLAUDE.md',
643
+ 'AI_CONTEXT.md',
644
+ 'README.md',
645
+ '.github/copilot-instructions.md',
646
+ '.clinerules',
647
+ 'docs/**/*.md',
648
+ '.claude/**/*.md',
649
+ '.ai-context/**/*.md'
650
+ ];
651
+
652
+ const files = [];
653
+ for (const pattern of patterns) {
654
+ try {
655
+ const matches = await glob(pattern, {
656
+ cwd: projectRoot,
657
+ ignore: ['node_modules/**', '**/node_modules/**'],
658
+ nodir: true
659
+ });
660
+ files.push(...matches);
661
+ } catch {
662
+ // Ignore glob errors
663
+ }
664
+ }
665
+
666
+ return [...new Set(files)];
667
+ }
668
+
669
+ /**
670
+ * Calculate drift level from issues
671
+ */
672
+ function calculateDriftLevel(issues) {
673
+ if (issues.length === 0) return DRIFT_LEVEL.NONE;
674
+
675
+ const hasCritical = issues.some(i => i.level === DRIFT_LEVEL.CRITICAL);
676
+ const hasHigh = issues.some(i => i.level === DRIFT_LEVEL.HIGH);
677
+ const hasMedium = issues.some(i => i.level === DRIFT_LEVEL.MEDIUM);
678
+
679
+ if (hasCritical) return DRIFT_LEVEL.CRITICAL;
680
+ if (hasHigh) return DRIFT_LEVEL.HIGH;
681
+ if (hasMedium) return DRIFT_LEVEL.MEDIUM;
682
+ return DRIFT_LEVEL.LOW;
683
+ }
684
+
685
+ /**
686
+ * Get type key for health breakdown
687
+ */
688
+ function getTypeKey(type) {
689
+ const mapping = {
690
+ 'file_path': 'filePaths',
691
+ 'line_reference': 'lineReferences',
692
+ 'anchor_reference': 'anchorReferences',
693
+ 'directory_reference': 'directories',
694
+ 'markdown_link': 'markdownLinks'
695
+ };
696
+ return mapping[type];
697
+ }
698
+
699
+ /**
700
+ * Get symbol extraction patterns for a language
701
+ */
702
+ function getSymbolPatterns(ext) {
703
+ const patterns = {
704
+ // Python
705
+ py: [
706
+ /^(?:async\s+)?def\s+(\w+)\s*\(/gm,
707
+ /^class\s+(\w+)/gm
708
+ ],
709
+ pyw: [
710
+ /^(?:async\s+)?def\s+(\w+)\s*\(/gm,
711
+ /^class\s+(\w+)/gm
712
+ ],
713
+ // JavaScript/TypeScript
714
+ js: [
715
+ /^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/gm,
716
+ /^(?:export\s+)?class\s+(\w+)/gm,
717
+ /^(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(/gm,
718
+ /^(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/gm
719
+ ],
720
+ ts: [
721
+ /^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*[<(]/gm,
722
+ /^(?:export\s+)?class\s+(\w+)/gm,
723
+ /^(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(/gm,
724
+ /^(?:export\s+)?interface\s+(\w+)/gm,
725
+ /^(?:export\s+)?type\s+(\w+)/gm
726
+ ],
727
+ // Go
728
+ go: [
729
+ /^func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(/gm,
730
+ /^type\s+(\w+)\s+struct/gm,
731
+ /^type\s+(\w+)\s+interface/gm
732
+ ],
733
+ // Rust
734
+ rs: [
735
+ /^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/gm,
736
+ /^(?:pub\s+)?struct\s+(\w+)/gm,
737
+ /^(?:pub\s+)?enum\s+(\w+)/gm,
738
+ /^(?:pub\s+)?trait\s+(\w+)/gm,
739
+ /^impl(?:<[^>]+>)?\s+(\w+)/gm
740
+ ],
741
+ // Ruby
742
+ rb: [
743
+ /^(?:\s*)def\s+(\w+)/gm,
744
+ /^(?:\s*)class\s+(\w+)/gm,
745
+ /^(?:\s*)module\s+(\w+)/gm
746
+ ]
747
+ };
748
+
749
+ // Handle jsx, tsx, mjs, cjs as their base type
750
+ const normalized = ext.replace(/[jt]sx$/, ext[0] + 's').replace(/[cm]js$/, 'js');
751
+ return patterns[normalized] || patterns.js; // Default to JS patterns
752
+ }
753
+
754
+ /**
755
+ * Extract symbols from content using patterns
756
+ */
757
+ function extractSymbols(content, patterns) {
758
+ const symbols = [];
759
+ const lines = content.split('\n');
760
+
761
+ for (const pattern of patterns) {
762
+ pattern.lastIndex = 0;
763
+ let match;
764
+ while ((match = pattern.exec(content)) !== null) {
765
+ const name = match[1];
766
+ // Calculate line number
767
+ const beforeMatch = content.substring(0, match.index);
768
+ const line = beforeMatch.split('\n').length;
769
+
770
+ symbols.push({
771
+ name,
772
+ line,
773
+ type: pattern.source.includes('class') ? 'class' :
774
+ pattern.source.includes('struct') ? 'struct' :
775
+ pattern.source.includes('interface') ? 'interface' :
776
+ pattern.source.includes('trait') ? 'trait' :
777
+ pattern.source.includes('type') ? 'type' :
778
+ pattern.source.includes('module') ? 'module' : 'function'
779
+ });
780
+ }
781
+ }
782
+
783
+ return symbols;
784
+ }
785
+
786
+ /**
787
+ * Find similar file in project
788
+ */
789
+ function findSimilarFile(targetFile, projectRoot) {
790
+ const basename = path.basename(targetFile);
791
+ const dirname = path.dirname(targetFile);
792
+
793
+ // Try common variations
794
+ const variations = [
795
+ targetFile,
796
+ path.join(dirname, basename.toLowerCase()),
797
+ path.join('src', targetFile),
798
+ path.join('lib', targetFile),
799
+ basename
800
+ ];
801
+
802
+ for (const variation of variations) {
803
+ const fullPath = path.join(projectRoot, variation);
804
+ if (fs.existsSync(fullPath)) {
805
+ return variation;
806
+ }
807
+ }
808
+
809
+ return null;
810
+ }
811
+
812
+ /**
813
+ * Find similar directory in project
814
+ */
815
+ function findSimilarDirectory(targetDir, projectRoot) {
816
+ const basename = path.basename(targetDir.replace(/\/$/, ''));
817
+
818
+ // Try common variations
819
+ const variations = [
820
+ targetDir,
821
+ path.join('src', basename),
822
+ path.join('lib', basename),
823
+ basename
824
+ ];
825
+
826
+ for (const variation of variations) {
827
+ const fullPath = path.join(projectRoot, variation);
828
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
829
+ return variation + '/';
830
+ }
831
+ }
832
+
833
+ return null;
834
+ }
835
+
836
+ /**
837
+ * Format drift report for console output
838
+ */
839
+ function formatDriftReportConsole(report) {
840
+ const lines = [];
841
+
842
+ lines.push('');
843
+ lines.push('Documentation Drift Report');
844
+ lines.push('=' .repeat(50));
845
+ lines.push('');
846
+
847
+ // Overall summary
848
+ const healthEmoji = report.summary.overallHealthScore >= 90 ? '\u2713' :
849
+ report.summary.overallHealthScore >= 70 ? '\u26A0' : '\u2717';
850
+ lines.push(`Overall Health: ${report.summary.overallHealthScore}% ${healthEmoji}`);
851
+ lines.push('');
852
+ lines.push(`Documents: ${report.summary.totalDocuments} (${report.summary.healthyDocuments} healthy)`);
853
+ lines.push(`References: ${report.summary.validReferences}/${report.summary.totalReferences} valid`);
854
+ lines.push('');
855
+
856
+ // Per-document results
857
+ for (const doc of report.documents) {
858
+ if (doc.status === 'error') {
859
+ lines.push(`${doc.document} - ERROR: ${doc.error}`);
860
+ continue;
861
+ }
862
+
863
+ const emoji = doc.status === 'healthy' ? '\u2713' :
864
+ doc.status === 'needs_update' ? '\u26A0' : '\u2717';
865
+ lines.push(`${doc.document} - ${doc.healthScore}% ${emoji} ${doc.status}`);
866
+
867
+ if (doc.references.invalid.length > 0) {
868
+ for (const issue of doc.references.invalid.slice(0, 5)) {
869
+ lines.push(` \u2717 ${issue.original} - ${issue.issue}`);
870
+ if (issue.suggestion) {
871
+ lines.push(` ${issue.suggestion}`);
872
+ }
873
+ }
874
+ if (doc.references.invalid.length > 5) {
875
+ lines.push(` ... and ${doc.references.invalid.length - 5} more issues`);
876
+ }
877
+ }
878
+ }
879
+
880
+ lines.push('');
881
+
882
+ // Suggested fixes
883
+ if (report.suggestedFixes.length > 0) {
884
+ lines.push('Suggested Fixes:');
885
+ for (const fix of report.suggestedFixes.slice(0, 10)) {
886
+ lines.push(` ${fix.document}: ${fix.original}`);
887
+ lines.push(` -> ${fix.suggestion}`);
888
+ }
889
+ }
890
+
891
+ return lines.join('\n');
892
+ }
893
+
894
+ module.exports = {
895
+ // Core functions
896
+ extractAllReferences,
897
+ checkDocumentDrift,
898
+ generateDriftReport,
899
+ findDocumentationFiles,
900
+ formatDriftReportConsole,
901
+
902
+ // Validation functions
903
+ validateFilePath,
904
+ validateLineReference,
905
+ validateAnchorReference,
906
+ validateDirectory,
907
+ validateMarkdownLink,
908
+
909
+ // Utilities
910
+ extractSymbols,
911
+ getSymbolPatterns,
912
+ findSimilarFile,
913
+ findSimilarDirectory,
914
+ calculateDriftLevel,
915
+
916
+ // Constants
917
+ DRIFT_LEVEL,
918
+ HEALTH_THRESHOLDS,
919
+ REFERENCE_PATTERNS
920
+ };