create-universal-ai-context 2.4.0 → 2.6.0-final

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 (153) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +331 -294
  3. package/bin/create-ai-context.js +1507 -764
  4. package/lib/adapters/aider.js +131 -131
  5. package/lib/adapters/antigravity.js +205 -205
  6. package/lib/adapters/claude.js +397 -397
  7. package/lib/adapters/cline.js +125 -125
  8. package/lib/adapters/continue.js +138 -138
  9. package/lib/adapters/copilot.js +131 -131
  10. package/lib/adapters/index.js +78 -78
  11. package/lib/adapters/windsurf.js +138 -138
  12. package/lib/ai-context-generator.js +234 -234
  13. package/lib/ai-orchestrator.js +432 -432
  14. package/lib/call-tracer.js +444 -444
  15. package/lib/content-preservation.js +243 -243
  16. package/lib/cross-tool-sync/file-watcher.js +274 -274
  17. package/lib/cross-tool-sync/index.js +41 -40
  18. package/lib/cross-tool-sync/sync-manager.js +540 -512
  19. package/lib/cross-tool-sync/sync-service.js +297 -297
  20. package/lib/detector.js +726 -726
  21. package/lib/doc-discovery.js +741 -741
  22. package/lib/drift-checker.js +920 -920
  23. package/lib/environment-detector.js +239 -239
  24. package/lib/index.js +399 -399
  25. package/lib/install-hooks.js +82 -82
  26. package/lib/installer.js +419 -419
  27. package/lib/migrate.js +328 -328
  28. package/lib/placeholder.js +632 -632
  29. package/lib/prompts.js +341 -341
  30. package/lib/smart-merge.js +540 -540
  31. package/lib/spinner.js +60 -60
  32. package/lib/static-analyzer.js +729 -729
  33. package/lib/template-coordination.js +148 -148
  34. package/lib/template-populator.js +843 -843
  35. package/lib/template-renderer.js +392 -392
  36. package/lib/utils/fs-wrapper.js +79 -79
  37. package/lib/utils/path-utils.js +60 -60
  38. package/lib/validate.js +155 -155
  39. package/package.json +1 -1
  40. package/templates/AI_CONTEXT.md.template +245 -245
  41. package/templates/base/README.md +260 -257
  42. package/templates/base/RPI_WORKFLOW_PLAN.md +325 -320
  43. package/templates/base/agents/api-developer.md +76 -76
  44. package/templates/base/agents/context-engineer.md +525 -525
  45. package/templates/base/agents/core-architect.md +76 -76
  46. package/templates/base/agents/database-ops.md +76 -76
  47. package/templates/base/agents/deployment-ops.md +76 -76
  48. package/templates/base/agents/integration-hub.md +76 -76
  49. package/templates/base/analytics/README.md +114 -114
  50. package/templates/base/automation/config.json +58 -58
  51. package/templates/base/automation/generators/code-mapper.js +308 -308
  52. package/templates/base/automation/generators/index-builder.js +321 -321
  53. package/templates/base/automation/hooks/post-commit.sh +83 -83
  54. package/templates/base/automation/hooks/pre-commit.sh +103 -103
  55. package/templates/base/ci-templates/README.md +108 -108
  56. package/templates/base/ci-templates/github-actions/context-check.yml +144 -144
  57. package/templates/base/ci-templates/github-actions/validate-docs.yml +105 -105
  58. package/templates/base/commands/analytics.md +238 -238
  59. package/templates/base/commands/auto-sync.md +172 -172
  60. package/templates/base/commands/collab.md +194 -194
  61. package/templates/base/commands/context-optimize.md +226 -0
  62. package/templates/base/commands/help.md +485 -450
  63. package/templates/base/commands/rpi-implement.md +164 -115
  64. package/templates/base/commands/rpi-plan.md +147 -93
  65. package/templates/base/commands/rpi-research.md +145 -88
  66. package/templates/base/commands/session-resume.md +144 -144
  67. package/templates/base/commands/session-save.md +112 -112
  68. package/templates/base/commands/validate-all.md +77 -77
  69. package/templates/base/commands/verify-docs-current.md +86 -86
  70. package/templates/base/config/base.json +57 -57
  71. package/templates/base/config/environments/development.json +13 -13
  72. package/templates/base/config/environments/production.json +17 -17
  73. package/templates/base/config/environments/staging.json +13 -13
  74. package/templates/base/config/local.json.example +21 -21
  75. package/templates/base/context/.meta/generated-at.json +18 -18
  76. package/templates/base/context/ARCHITECTURE_SNAPSHOT.md +156 -156
  77. package/templates/base/context/CODE_TO_WORKFLOW_MAP.md +94 -94
  78. package/templates/base/context/FILE_OWNERSHIP.md +57 -57
  79. package/templates/base/context/INTEGRATION_POINTS.md +92 -92
  80. package/templates/base/context/KNOWN_GOTCHAS.md +195 -195
  81. package/templates/base/context/TESTING_MAP.md +95 -95
  82. package/templates/base/context/WORKFLOW_INDEX.md +129 -129
  83. package/templates/base/context/workflows/WORKFLOW_TEMPLATE.md +294 -294
  84. package/templates/base/indexes/agents/CAPABILITY_MATRIX.md +255 -255
  85. package/templates/base/indexes/agents/CATEGORY_INDEX.md +44 -44
  86. package/templates/base/indexes/code/CATEGORY_INDEX.md +38 -38
  87. package/templates/base/indexes/routing/CATEGORY_INDEX.md +39 -39
  88. package/templates/base/indexes/search/CATEGORY_INDEX.md +39 -39
  89. package/templates/base/indexes/workflows/CATEGORY_INDEX.md +38 -38
  90. package/templates/base/knowledge/README.md +98 -98
  91. package/templates/base/knowledge/sessions/README.md +88 -88
  92. package/templates/base/knowledge/sessions/TEMPLATE.md +150 -150
  93. package/templates/base/knowledge/shared/decisions/0001-adopt-context-engineering.md +144 -144
  94. package/templates/base/knowledge/shared/decisions/README.md +49 -49
  95. package/templates/base/knowledge/shared/decisions/TEMPLATE.md +123 -123
  96. package/templates/base/knowledge/shared/patterns/README.md +62 -62
  97. package/templates/base/knowledge/shared/patterns/TEMPLATE.md +120 -120
  98. package/templates/base/plans/PLAN_TEMPLATE.md +316 -250
  99. package/templates/base/research/RESEARCH_TEMPLATE.md +245 -153
  100. package/templates/base/schemas/agent.schema.json +141 -141
  101. package/templates/base/schemas/anchors.schema.json +54 -54
  102. package/templates/base/schemas/automation.schema.json +93 -93
  103. package/templates/base/schemas/command.schema.json +134 -134
  104. package/templates/base/schemas/hashes.schema.json +40 -40
  105. package/templates/base/schemas/manifest.schema.json +117 -117
  106. package/templates/base/schemas/plan.schema.json +136 -136
  107. package/templates/base/schemas/research.schema.json +115 -115
  108. package/templates/base/schemas/roles.schema.json +34 -34
  109. package/templates/base/schemas/session.schema.json +77 -77
  110. package/templates/base/schemas/settings.schema.json +244 -244
  111. package/templates/base/schemas/staleness.schema.json +53 -53
  112. package/templates/base/schemas/team-config.schema.json +42 -42
  113. package/templates/base/schemas/workflow.schema.json +126 -126
  114. package/templates/base/session/checkpoints/.gitkeep +2 -2
  115. package/templates/base/session/current/state.json +20 -20
  116. package/templates/base/session/history/.gitkeep +2 -2
  117. package/templates/base/settings.json +3 -3
  118. package/templates/base/standards/COMPATIBILITY.md +219 -219
  119. package/templates/base/standards/EXTENSION_GUIDELINES.md +280 -280
  120. package/templates/base/standards/QUALITY_CHECKLIST.md +211 -211
  121. package/templates/base/standards/README.md +66 -66
  122. package/templates/base/sync/anchors.json +6 -6
  123. package/templates/base/sync/hashes.json +6 -6
  124. package/templates/base/sync/staleness.json +10 -10
  125. package/templates/base/team/README.md +168 -168
  126. package/templates/base/team/config.json +79 -79
  127. package/templates/base/team/roles.json +145 -145
  128. package/templates/base/tools/bin/claude-context.js +151 -151
  129. package/templates/base/tools/lib/anchor-resolver.js +276 -276
  130. package/templates/base/tools/lib/config-loader.js +363 -363
  131. package/templates/base/tools/lib/detector.js +350 -350
  132. package/templates/base/tools/lib/diagnose.js +206 -206
  133. package/templates/base/tools/lib/drift-detector.js +373 -373
  134. package/templates/base/tools/lib/errors.js +199 -199
  135. package/templates/base/tools/lib/index.js +36 -36
  136. package/templates/base/tools/lib/init.js +192 -192
  137. package/templates/base/tools/lib/logger.js +230 -230
  138. package/templates/base/tools/lib/placeholder.js +201 -201
  139. package/templates/base/tools/lib/session-manager.js +354 -354
  140. package/templates/base/tools/lib/validate.js +521 -521
  141. package/templates/base/tools/package.json +49 -49
  142. package/templates/handlebars/aider-config.hbs +146 -80
  143. package/templates/handlebars/antigravity.hbs +377 -377
  144. package/templates/handlebars/claude.hbs +183 -183
  145. package/templates/handlebars/cline.hbs +62 -62
  146. package/templates/handlebars/continue-config.hbs +116 -116
  147. package/templates/handlebars/copilot.hbs +130 -130
  148. package/templates/handlebars/partials/gotcha-list.hbs +11 -11
  149. package/templates/handlebars/partials/header.hbs +3 -3
  150. package/templates/handlebars/partials/workflow-summary.hbs +16 -16
  151. package/templates/handlebars/windsurf-rules.hbs +69 -69
  152. package/templates/hooks/post-commit.hbs +28 -29
  153. package/templates/hooks/pre-commit.hbs +46 -46
@@ -1,920 +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
- };
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
+ };