#
tokens: 44265/50000 4/274 files (page 20/29)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 20 of 29. Use http://codebase.md/tosin2013/documcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .dockerignore
├── .eslintignore
├── .eslintrc.json
├── .github
│   ├── agents
│   │   ├── documcp-ast.md
│   │   ├── documcp-deploy.md
│   │   ├── documcp-memory.md
│   │   ├── documcp-test.md
│   │   └── documcp-tool.md
│   ├── copilot-instructions.md
│   ├── dependabot.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── automated-changelog.md
│   │   ├── bug_report.md
│   │   ├── bug_report.yml
│   │   ├── documentation_issue.md
│   │   ├── feature_request.md
│   │   ├── feature_request.yml
│   │   ├── npm-publishing-fix.md
│   │   └── release_improvements.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── release-drafter.yml
│   └── workflows
│       ├── auto-merge.yml
│       ├── ci.yml
│       ├── codeql.yml
│       ├── dependency-review.yml
│       ├── deploy-docs.yml
│       ├── README.md
│       ├── release-drafter.yml
│       └── release.yml
├── .gitignore
├── .husky
│   ├── commit-msg
│   └── pre-commit
├── .linkcheck.config.json
├── .markdown-link-check.json
├── .nvmrc
├── .pre-commit-config.yaml
├── .versionrc.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── commitlint.config.js
├── CONTRIBUTING.md
├── docker-compose.docs.yml
├── Dockerfile.docs
├── docs
│   ├── .docusaurus
│   │   ├── docusaurus-plugin-content-docs
│   │   │   └── default
│   │   │       └── __mdx-loader-dependency.json
│   │   └── docusaurus-plugin-content-pages
│   │       └── default
│   │           └── __plugin.json
│   ├── adrs
│   │   ├── 001-mcp-server-architecture.md
│   │   ├── 002-repository-analysis-engine.md
│   │   ├── 003-static-site-generator-recommendation-engine.md
│   │   ├── 004-diataxis-framework-integration.md
│   │   ├── 005-github-pages-deployment-automation.md
│   │   ├── 006-mcp-tools-api-design.md
│   │   ├── 007-mcp-prompts-and-resources-integration.md
│   │   ├── 008-intelligent-content-population-engine.md
│   │   ├── 009-content-accuracy-validation-framework.md
│   │   ├── 010-mcp-resource-pattern-redesign.md
│   │   └── README.md
│   ├── api
│   │   ├── .nojekyll
│   │   ├── assets
│   │   │   ├── hierarchy.js
│   │   │   ├── highlight.css
│   │   │   ├── icons.js
│   │   │   ├── icons.svg
│   │   │   ├── main.js
│   │   │   ├── navigation.js
│   │   │   ├── search.js
│   │   │   └── style.css
│   │   ├── hierarchy.html
│   │   ├── index.html
│   │   ├── modules.html
│   │   └── variables
│   │       └── TOOLS.html
│   ├── assets
│   │   └── logo.svg
│   ├── development
│   │   └── MCP_INSPECTOR_TESTING.md
│   ├── docusaurus.config.js
│   ├── explanation
│   │   ├── architecture.md
│   │   └── index.md
│   ├── guides
│   │   ├── link-validation.md
│   │   ├── playwright-integration.md
│   │   └── playwright-testing-workflow.md
│   ├── how-to
│   │   ├── analytics-setup.md
│   │   ├── custom-domains.md
│   │   ├── documentation-freshness-tracking.md
│   │   ├── github-pages-deployment.md
│   │   ├── index.md
│   │   ├── local-testing.md
│   │   ├── performance-optimization.md
│   │   ├── prompting-guide.md
│   │   ├── repository-analysis.md
│   │   ├── seo-optimization.md
│   │   ├── site-monitoring.md
│   │   ├── troubleshooting.md
│   │   └── usage-examples.md
│   ├── index.md
│   ├── knowledge-graph.md
│   ├── package-lock.json
│   ├── package.json
│   ├── phase-2-intelligence.md
│   ├── reference
│   │   ├── api-overview.md
│   │   ├── cli.md
│   │   ├── configuration.md
│   │   ├── deploy-pages.md
│   │   ├── index.md
│   │   ├── mcp-tools.md
│   │   └── prompt-templates.md
│   ├── research
│   │   ├── cross-domain-integration
│   │   │   └── README.md
│   │   ├── domain-1-mcp-architecture
│   │   │   ├── index.md
│   │   │   └── mcp-performance-research.md
│   │   ├── domain-2-repository-analysis
│   │   │   └── README.md
│   │   ├── domain-3-ssg-recommendation
│   │   │   ├── index.md
│   │   │   └── ssg-performance-analysis.md
│   │   ├── domain-4-diataxis-integration
│   │   │   └── README.md
│   │   ├── domain-5-github-deployment
│   │   │   ├── github-pages-security-analysis.md
│   │   │   └── index.md
│   │   ├── domain-6-api-design
│   │   │   └── README.md
│   │   ├── README.md
│   │   ├── research-integration-summary-2025-01-14.md
│   │   ├── research-progress-template.md
│   │   └── research-questions-2025-01-14.md
│   ├── robots.txt
│   ├── sidebars.js
│   ├── sitemap.xml
│   ├── src
│   │   └── css
│   │       └── custom.css
│   └── tutorials
│       ├── development-setup.md
│       ├── environment-setup.md
│       ├── first-deployment.md
│       ├── getting-started.md
│       ├── index.md
│       ├── memory-workflows.md
│       └── user-onboarding.md
├── jest.config.js
├── LICENSE
├── Makefile
├── MCP_PHASE2_IMPLEMENTATION.md
├── mcp-config-example.json
├── mcp.json
├── package-lock.json
├── package.json
├── README.md
├── release.sh
├── scripts
│   └── check-package-structure.cjs
├── SECURITY.md
├── setup-precommit.sh
├── src
│   ├── benchmarks
│   │   └── performance.ts
│   ├── index.ts
│   ├── memory
│   │   ├── contextual-retrieval.ts
│   │   ├── deployment-analytics.ts
│   │   ├── enhanced-manager.ts
│   │   ├── export-import.ts
│   │   ├── freshness-kg-integration.ts
│   │   ├── index.ts
│   │   ├── integration.ts
│   │   ├── kg-code-integration.ts
│   │   ├── kg-health.ts
│   │   ├── kg-integration.ts
│   │   ├── kg-link-validator.ts
│   │   ├── kg-storage.ts
│   │   ├── knowledge-graph.ts
│   │   ├── learning.ts
│   │   ├── manager.ts
│   │   ├── multi-agent-sharing.ts
│   │   ├── pruning.ts
│   │   ├── schemas.ts
│   │   ├── storage.ts
│   │   ├── temporal-analysis.ts
│   │   ├── user-preferences.ts
│   │   └── visualization.ts
│   ├── prompts
│   │   └── technical-writer-prompts.ts
│   ├── scripts
│   │   └── benchmark.ts
│   ├── templates
│   │   └── playwright
│   │       ├── accessibility.spec.template.ts
│   │       ├── Dockerfile.template
│   │       ├── docs-e2e.workflow.template.yml
│   │       ├── link-validation.spec.template.ts
│   │       └── playwright.config.template.ts
│   ├── tools
│   │   ├── analyze-deployments.ts
│   │   ├── analyze-readme.ts
│   │   ├── analyze-repository.ts
│   │   ├── check-documentation-links.ts
│   │   ├── deploy-pages.ts
│   │   ├── detect-gaps.ts
│   │   ├── evaluate-readme-health.ts
│   │   ├── generate-config.ts
│   │   ├── generate-contextual-content.ts
│   │   ├── generate-llm-context.ts
│   │   ├── generate-readme-template.ts
│   │   ├── generate-technical-writer-prompts.ts
│   │   ├── kg-health-check.ts
│   │   ├── manage-preferences.ts
│   │   ├── manage-sitemap.ts
│   │   ├── optimize-readme.ts
│   │   ├── populate-content.ts
│   │   ├── readme-best-practices.ts
│   │   ├── recommend-ssg.ts
│   │   ├── setup-playwright-tests.ts
│   │   ├── setup-structure.ts
│   │   ├── sync-code-to-docs.ts
│   │   ├── test-local-deployment.ts
│   │   ├── track-documentation-freshness.ts
│   │   ├── update-existing-documentation.ts
│   │   ├── validate-content.ts
│   │   ├── validate-documentation-freshness.ts
│   │   ├── validate-readme-checklist.ts
│   │   └── verify-deployment.ts
│   ├── types
│   │   └── api.ts
│   ├── utils
│   │   ├── ast-analyzer.ts
│   │   ├── code-scanner.ts
│   │   ├── content-extractor.ts
│   │   ├── drift-detector.ts
│   │   ├── freshness-tracker.ts
│   │   ├── language-parsers-simple.ts
│   │   ├── permission-checker.ts
│   │   └── sitemap-generator.ts
│   └── workflows
│       └── documentation-workflow.ts
├── test-docs-local.sh
├── tests
│   ├── api
│   │   └── mcp-responses.test.ts
│   ├── benchmarks
│   │   └── performance.test.ts
│   ├── edge-cases
│   │   └── error-handling.test.ts
│   ├── functional
│   │   └── tools.test.ts
│   ├── integration
│   │   ├── kg-documentation-workflow.test.ts
│   │   ├── knowledge-graph-workflow.test.ts
│   │   ├── mcp-readme-tools.test.ts
│   │   ├── memory-mcp-tools.test.ts
│   │   ├── readme-technical-writer.test.ts
│   │   └── workflow.test.ts
│   ├── memory
│   │   ├── contextual-retrieval.test.ts
│   │   ├── enhanced-manager.test.ts
│   │   ├── export-import.test.ts
│   │   ├── freshness-kg-integration.test.ts
│   │   ├── kg-code-integration.test.ts
│   │   ├── kg-health.test.ts
│   │   ├── kg-link-validator.test.ts
│   │   ├── kg-storage-validation.test.ts
│   │   ├── kg-storage.test.ts
│   │   ├── knowledge-graph-enhanced.test.ts
│   │   ├── knowledge-graph.test.ts
│   │   ├── learning.test.ts
│   │   ├── manager-advanced.test.ts
│   │   ├── manager.test.ts
│   │   ├── mcp-resource-integration.test.ts
│   │   ├── mcp-tool-persistence.test.ts
│   │   ├── schemas.test.ts
│   │   ├── storage.test.ts
│   │   ├── temporal-analysis.test.ts
│   │   └── user-preferences.test.ts
│   ├── performance
│   │   ├── memory-load-testing.test.ts
│   │   └── memory-stress-testing.test.ts
│   ├── prompts
│   │   ├── guided-workflow-prompts.test.ts
│   │   └── technical-writer-prompts.test.ts
│   ├── server.test.ts
│   ├── setup.ts
│   ├── tools
│   │   ├── all-tools.test.ts
│   │   ├── analyze-coverage.test.ts
│   │   ├── analyze-deployments.test.ts
│   │   ├── analyze-readme.test.ts
│   │   ├── analyze-repository.test.ts
│   │   ├── check-documentation-links.test.ts
│   │   ├── deploy-pages-kg-retrieval.test.ts
│   │   ├── deploy-pages-tracking.test.ts
│   │   ├── deploy-pages.test.ts
│   │   ├── detect-gaps.test.ts
│   │   ├── evaluate-readme-health.test.ts
│   │   ├── generate-contextual-content.test.ts
│   │   ├── generate-llm-context.test.ts
│   │   ├── generate-readme-template.test.ts
│   │   ├── generate-technical-writer-prompts.test.ts
│   │   ├── kg-health-check.test.ts
│   │   ├── manage-sitemap.test.ts
│   │   ├── optimize-readme.test.ts
│   │   ├── readme-best-practices.test.ts
│   │   ├── recommend-ssg-historical.test.ts
│   │   ├── recommend-ssg-preferences.test.ts
│   │   ├── recommend-ssg.test.ts
│   │   ├── simple-coverage.test.ts
│   │   ├── sync-code-to-docs.test.ts
│   │   ├── test-local-deployment.test.ts
│   │   ├── tool-error-handling.test.ts
│   │   ├── track-documentation-freshness.test.ts
│   │   ├── validate-content.test.ts
│   │   ├── validate-documentation-freshness.test.ts
│   │   └── validate-readme-checklist.test.ts
│   ├── types
│   │   └── type-safety.test.ts
│   └── utils
│       ├── ast-analyzer.test.ts
│       ├── content-extractor.test.ts
│       ├── drift-detector.test.ts
│       ├── freshness-tracker.test.ts
│       └── sitemap-generator.test.ts
├── tsconfig.json
└── typedoc.json
```

# Files

--------------------------------------------------------------------------------
/src/tools/analyze-repository.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { promises as fs } from "fs";
  2 | import path from "path";
  3 | import { z } from "zod";
  4 | import { MCPToolResponse, formatMCPResponse } from "../types/api.js";
  5 | import {
  6 |   createOrUpdateProject,
  7 |   getProjectContext,
  8 | } from "../memory/kg-integration.js";
  9 | import {
 10 |   extractRepositoryContent,
 11 |   ExtractedContent,
 12 | } from "../utils/content-extractor.js";
 13 | 
 14 | // Analysis result schema based on ADR-002
 15 | export interface RepositoryAnalysis {
 16 |   id: string;
 17 |   timestamp: string;
 18 |   path: string;
 19 |   structure: {
 20 |     totalFiles: number;
 21 |     totalDirectories: number;
 22 |     languages: Record<string, number>;
 23 |     hasTests: boolean;
 24 |     hasCI: boolean;
 25 |     hasDocs: boolean;
 26 |   };
 27 |   dependencies: {
 28 |     ecosystem: "javascript" | "python" | "ruby" | "go" | "unknown";
 29 |     packages: string[];
 30 |     devPackages: string[];
 31 |   };
 32 |   documentation: {
 33 |     hasReadme: boolean;
 34 |     hasContributing: boolean;
 35 |     hasLicense: boolean;
 36 |     existingDocs: string[];
 37 |     estimatedComplexity: "simple" | "moderate" | "complex";
 38 |     extractedContent?: ExtractedContent;
 39 |   };
 40 |   recommendations: {
 41 |     primaryLanguage: string;
 42 |     projectType: string;
 43 |     teamSize: "solo" | "small" | "medium" | "large";
 44 |   };
 45 | }
 46 | 
 47 | const inputSchema = z.object({
 48 |   path: z.string(),
 49 |   depth: z.enum(["quick", "standard", "deep"]).optional().default("standard"),
 50 | });
 51 | 
 52 | /**
 53 |  * Analyzes a repository to understand its structure, dependencies, and documentation needs.
 54 |  *
 55 |  * This is the core function of DocuMCP that performs multi-layered analysis of a codebase
 56 |  * to provide intelligent insights for documentation generation and deployment recommendations.
 57 |  * The analysis includes structural analysis, dependency detection, documentation assessment,
 58 |  * and generates recommendations for optimal documentation strategies.
 59 |  *
 60 |  * @param args - The input arguments for repository analysis
 61 |  * @param args.path - The file system path to the repository to analyze
 62 |  * @param args.depth - The analysis depth level: "quick" (basic), "standard" (comprehensive), or "deep" (thorough)
 63 |  *
 64 |  * @returns Promise resolving to analysis results with content array and error status
 65 |  * @returns content - Array containing the analysis results in MCP tool response format
 66 |  * @returns isError - Boolean flag indicating if the analysis encountered errors
 67 |  *
 68 |  * @throws {Error} When the repository path is inaccessible or invalid
 69 |  * @throws {Error} When permission is denied to read the repository
 70 |  * @throws {Error} When the repository structure cannot be analyzed
 71 |  *
 72 |  * @example
 73 |  * ```typescript
 74 |  * // Basic repository analysis
 75 |  * const result = await analyzeRepository({
 76 |  *   path: "/path/to/my/repository",
 77 |  *   depth: "standard"
 78 |  * });
 79 |  *
 80 |  * // Quick analysis for large repositories
 81 |  * const quickResult = await analyzeRepository({
 82 |  *   path: "/path/to/large/repo",
 83 |  *   depth: "quick"
 84 |  * });
 85 |  * ```
 86 |  *
 87 |  * @since 1.0.0
 88 |  * @version 1.2.0 - Added Knowledge Graph integration and historical context
 89 |  */
 90 | export async function analyzeRepository(
 91 |   args: unknown,
 92 |   context?: any,
 93 | ): Promise<{ content: any[]; isError?: boolean }> {
 94 |   const startTime = Date.now();
 95 |   const { path: repoPath, depth } = inputSchema.parse(args);
 96 | 
 97 |   try {
 98 |     // Report initial progress
 99 |     if (context?.meta?.progressToken) {
100 |       await context.meta.reportProgress?.({
101 |         progress: 0,
102 |         total: 100,
103 |       });
104 |     }
105 | 
106 |     await context?.info?.("🔍 Starting repository analysis...");
107 | 
108 |     // Verify path exists and is accessible
109 |     await context?.info?.(`📂 Verifying access to ${repoPath}...`);
110 |     await fs.access(repoPath, fs.constants.R_OK);
111 | 
112 |     // Try to read the directory to catch permission issues early
113 |     try {
114 |       await fs.readdir(repoPath);
115 |     } catch (error: any) {
116 |       if (error.code === "EACCES" || error.code === "EPERM") {
117 |         throw new Error(`Permission denied: Cannot read directory ${repoPath}`);
118 |       }
119 |       throw error;
120 |     }
121 | 
122 |     if (context?.meta?.progressToken) {
123 |       await context.meta.reportProgress?.({
124 |         progress: 10,
125 |         total: 100,
126 |       });
127 |     }
128 | 
129 |     // Phase 1.2: Get historical context from Knowledge Graph
130 |     await context?.info?.(
131 |       "📊 Retrieving historical context from Knowledge Graph...",
132 |     );
133 |     let projectContext;
134 |     try {
135 |       projectContext = await getProjectContext(repoPath);
136 |       if (projectContext.previousAnalyses > 0) {
137 |         await context?.info?.(
138 |           `✨ Found ${projectContext.previousAnalyses} previous analysis(es) of this project`,
139 |         );
140 |       }
141 |     } catch (error) {
142 |       console.warn("Failed to retrieve project context:", error);
143 |       projectContext = {
144 |         previousAnalyses: 0,
145 |         lastAnalyzed: null,
146 |         knownTechnologies: [],
147 |         similarProjects: [],
148 |       };
149 |     }
150 | 
151 |     if (context?.meta?.progressToken) {
152 |       await context.meta.reportProgress?.({
153 |         progress: 20,
154 |         total: 100,
155 |       });
156 |     }
157 | 
158 |     await context?.info?.("🔎 Analyzing repository structure...");
159 |     const structure = await analyzeStructure(repoPath, depth);
160 | 
161 |     if (context?.meta?.progressToken) {
162 |       await context.meta.reportProgress?.({
163 |         progress: 40,
164 |         total: 100,
165 |       });
166 |     }
167 | 
168 |     await context?.info?.("📦 Analyzing dependencies...");
169 |     const dependencies = await analyzeDependencies(repoPath);
170 | 
171 |     if (context?.meta?.progressToken) {
172 |       await context.meta.reportProgress?.({
173 |         progress: 60,
174 |         total: 100,
175 |       });
176 |     }
177 | 
178 |     await context?.info?.("📝 Analyzing documentation...");
179 |     const documentation = await analyzeDocumentation(repoPath);
180 | 
181 |     if (context?.meta?.progressToken) {
182 |       await context.meta.reportProgress?.({
183 |         progress: 75,
184 |         total: 100,
185 |       });
186 |     }
187 | 
188 |     await context?.info?.("💡 Generating recommendations...");
189 |     const recommendations = await generateRecommendations(repoPath);
190 | 
191 |     const analysis: RepositoryAnalysis = {
192 |       id: generateAnalysisId(),
193 |       timestamp: new Date().toISOString(),
194 |       path: repoPath,
195 |       structure,
196 |       dependencies,
197 |       documentation,
198 |       recommendations,
199 |     };
200 | 
201 |     // Phase 1.2: Store project in Knowledge Graph
202 |     if (context?.meta?.progressToken) {
203 |       await context.meta.reportProgress?.({
204 |         progress: 85,
205 |         total: 100,
206 |       });
207 |     }
208 | 
209 |     await context?.info?.("💾 Storing analysis in Knowledge Graph...");
210 |     try {
211 |       await createOrUpdateProject(analysis);
212 |     } catch (error) {
213 |       console.warn("Failed to store project in Knowledge Graph:", error);
214 |     }
215 | 
216 |     if (context?.meta?.progressToken) {
217 |       await context.meta.reportProgress?.({
218 |         progress: 90,
219 |         total: 100,
220 |       });
221 |     }
222 | 
223 |     // Phase 1.3: Get intelligent analysis enrichment
224 |     await context?.info?.("🧠 Enriching analysis with historical insights...");
225 |     let intelligentAnalysis;
226 |     let documentationHealth;
227 |     try {
228 |       const { getProjectInsights, getSimilarProjects } = await import(
229 |         "../memory/index.js"
230 |       );
231 |       const { getKnowledgeGraph } = await import("../memory/kg-integration.js");
232 | 
233 |       const insights = await getProjectInsights(repoPath);
234 |       const similar = await getSimilarProjects(analysis, 5);
235 | 
236 |       // Check documentation health from KG
237 |       try {
238 |         const kg = await getKnowledgeGraph();
239 |         const allEdges = await kg.getAllEdges();
240 | 
241 |         // Find outdated documentation
242 |         const outdatedEdges = allEdges.filter((e) => e.type === "outdated_for");
243 | 
244 |         // Find documentation coverage
245 |         const documentsEdges = allEdges.filter((e) => e.type === "documents");
246 |         const totalCodeFiles = allEdges.filter(
247 |           (e) => e.type === "depends_on" && e.target.startsWith("code_file:"),
248 |         ).length;
249 | 
250 |         const documentedFiles = new Set(documentsEdges.map((e) => e.source))
251 |           .size;
252 | 
253 |         const coveragePercent =
254 |           totalCodeFiles > 0
255 |             ? Math.round((documentedFiles / totalCodeFiles) * 100)
256 |             : 0;
257 | 
258 |         documentationHealth = {
259 |           outdatedCount: outdatedEdges.length,
260 |           coveragePercent,
261 |           totalCodeFiles,
262 |           documentedFiles,
263 |         };
264 |       } catch (error) {
265 |         console.warn("Failed to calculate documentation health:", error);
266 |       }
267 | 
268 |       intelligentAnalysis = {
269 |         insights,
270 |         similarProjects: similar.slice(0, 3).map((p: any) => ({
271 |           path: p.projectPath,
272 |           similarity: Math.round((p.similarity || 0) * 100) + "%",
273 |           technologies: p.technologies?.join(", ") || "unknown",
274 |         })),
275 |         ...(documentationHealth && { documentationHealth }),
276 |         recommendations: [
277 |           // Documentation health recommendations
278 |           ...(documentationHealth && documentationHealth.outdatedCount > 0
279 |             ? [
280 |                 `${documentationHealth.outdatedCount} documentation section(s) may be outdated - code has changed since docs were updated`,
281 |               ]
282 |             : []),
283 |           ...(documentationHealth &&
284 |           documentationHealth.coveragePercent < 50 &&
285 |           documentationHealth.totalCodeFiles > 0
286 |             ? [
287 |                 `Documentation covers only ${documentationHealth.coveragePercent}% of code files - consider documenting more`,
288 |               ]
289 |             : []),
290 |           // Only suggest creating README if it truly doesn't exist
291 |           // Don't suggest improvements yet - that requires deeper analysis
292 |           ...(analysis.documentation.hasReadme
293 |             ? []
294 |             : ["Consider creating a README.md for project documentation"]),
295 |           // Only suggest docs structure if no docs folder exists at all
296 |           ...(analysis.structure.hasDocs
297 |             ? []
298 |             : analysis.documentation.existingDocs.length === 0
299 |               ? [
300 |                   "Consider setting up documentation structure using Diataxis framework",
301 |                 ]
302 |               : []),
303 |           // Infrastructure recommendations are safe as they're objective
304 |           ...(analysis.structure.hasTests
305 |             ? []
306 |             : ["Consider adding test coverage to improve reliability"]),
307 |           ...(analysis.structure.hasCI
308 |             ? []
309 |             : ["Consider setting up CI/CD pipeline for automation"]),
310 |         ],
311 |       };
312 |     } catch (error) {
313 |       console.warn("Failed to get intelligent analysis:", error);
314 |     }
315 | 
316 |     // Enhance response with historical context
317 |     const contextInfo: string[] = [];
318 |     if (projectContext.previousAnalyses > 0) {
319 |       contextInfo.push(
320 |         `📊 Previously analyzed ${projectContext.previousAnalyses} time(s)`,
321 |       );
322 |       if (projectContext.lastAnalyzed) {
323 |         const lastDate = new Date(
324 |           projectContext.lastAnalyzed,
325 |         ).toLocaleDateString();
326 |         contextInfo.push(`📅 Last analyzed: ${lastDate}`);
327 |       }
328 |     }
329 | 
330 |     if (projectContext.knownTechnologies.length > 0) {
331 |       contextInfo.push(
332 |         `💡 Known technologies: ${projectContext.knownTechnologies.join(", ")}`,
333 |       );
334 |     }
335 | 
336 |     if (projectContext.similarProjects.length > 0) {
337 |       contextInfo.push(
338 |         `🔗 Found ${projectContext.similarProjects.length} similar project(s) in knowledge graph`,
339 |       );
340 |     }
341 | 
342 |     if (context?.meta?.progressToken) {
343 |       await context.meta.reportProgress?.({
344 |         progress: 100,
345 |         total: 100,
346 |       });
347 |     }
348 | 
349 |     const executionTime = Date.now() - startTime;
350 |     await context?.info?.(
351 |       `✅ Analysis complete! Processed ${
352 |         analysis.structure.totalFiles
353 |       } files in ${Math.round(executionTime / 1000)}s`,
354 |     );
355 | 
356 |     const response: MCPToolResponse<RepositoryAnalysis> = {
357 |       success: true,
358 |       data: analysis,
359 |       metadata: {
360 |         toolVersion: "1.0.0",
361 |         executionTime,
362 |         timestamp: new Date().toISOString(),
363 |         analysisId: analysis.id,
364 |         ...(intelligentAnalysis && { intelligentAnalysis }),
365 |       },
366 |       recommendations: [
367 |         {
368 |           type: "info",
369 |           title: "Analysis Complete",
370 |           description: `Successfully analyzed ${analysis.structure.totalFiles} files across ${analysis.structure.totalDirectories} directories`,
371 |         },
372 |         ...(contextInfo.length > 0
373 |           ? [
374 |               {
375 |                 type: "info" as const,
376 |                 title: "Historical Context",
377 |                 description: contextInfo.join("\n"),
378 |               },
379 |             ]
380 |           : []),
381 |         ...(intelligentAnalysis?.recommendations &&
382 |         intelligentAnalysis.recommendations.length > 0
383 |           ? [
384 |               {
385 |                 type: "info" as const,
386 |                 title: "AI Recommendations",
387 |                 description: intelligentAnalysis.recommendations.join("\n• "),
388 |               },
389 |             ]
390 |           : []),
391 |         ...(intelligentAnalysis?.similarProjects &&
392 |         intelligentAnalysis.similarProjects.length > 0
393 |           ? [
394 |               {
395 |                 type: "info" as const,
396 |                 title: "Similar Projects",
397 |                 description: intelligentAnalysis.similarProjects
398 |                   .map(
399 |                     (p: any) =>
400 |                       `${p.path} (${p.similarity} similar, ${p.technologies})`,
401 |                   )
402 |                   .join("\n"),
403 |               },
404 |             ]
405 |           : []),
406 |       ],
407 |       nextSteps: [
408 |         ...(analysis.documentation.hasReadme
409 |           ? [
410 |               {
411 |                 action: "Analyze README Quality",
412 |                 toolRequired: "analyze_readme",
413 |                 description:
414 |                   "Evaluate README completeness and suggest improvements",
415 |                 priority: "medium" as const,
416 |               },
417 |             ]
418 |           : []),
419 |         {
420 |           action: "Get SSG Recommendation",
421 |           toolRequired: "recommend_ssg",
422 |           description: `Use analysis ID: ${analysis.id}`,
423 |           priority: "high",
424 |         },
425 |       ],
426 |     };
427 | 
428 |     return formatMCPResponse(response);
429 |   } catch (error) {
430 |     const errorResponse: MCPToolResponse = {
431 |       success: false,
432 |       error: {
433 |         code: "ANALYSIS_FAILED",
434 |         message: `Failed to analyze repository: ${error}`,
435 |         resolution: "Ensure the repository path exists and is accessible",
436 |       },
437 |       metadata: {
438 |         toolVersion: "1.0.0",
439 |         executionTime: Date.now() - startTime,
440 |         timestamp: new Date().toISOString(),
441 |       },
442 |     };
443 |     return formatMCPResponse(errorResponse);
444 |   }
445 | }
446 | 
447 | // Helper function to generate unique analysis ID
448 | /**
449 |  * Generates a unique analysis ID for tracking repository analyses.
450 |  *
451 |  * Creates a unique identifier combining timestamp and random string for
452 |  * tracking individual repository analysis sessions and linking them to
453 |  * recommendations and deployments.
454 |  *
455 |  * @returns A unique analysis identifier string
456 |  *
457 |  * @example
458 |  * ```typescript
459 |  * const analysisId = generateAnalysisId();
460 |  * // Returns: "abc123def456"
461 |  * ```
462 |  *
463 |  * @since 1.0.0
464 |  */
465 | function generateAnalysisId(): string {
466 |   const timestamp = Date.now().toString(36);
467 |   const random = Math.random().toString(36).substring(2, 7);
468 |   return `analysis_${timestamp}_${random}`;
469 | }
470 | 
471 | // Map file extensions to languages
472 | function getLanguageFromExtension(ext: string): string | null {
473 |   const languageMap: Record<string, string> = {
474 |     ".js": "javascript",
475 |     ".jsx": "javascript",
476 |     ".ts": "typescript",
477 |     ".tsx": "typescript",
478 |     ".py": "python",
479 |     ".rb": "ruby",
480 |     ".go": "go",
481 |     ".java": "java",
482 |     ".c": "c",
483 |     ".cpp": "cpp",
484 |     ".cs": "csharp",
485 |     ".php": "php",
486 |     ".rs": "rust",
487 |     ".kt": "kotlin",
488 |     ".swift": "swift",
489 |     ".scala": "scala",
490 |     ".sh": "shell",
491 |     ".bash": "shell",
492 |     ".zsh": "shell",
493 |     ".fish": "shell",
494 |     ".ps1": "powershell",
495 |     ".r": "r",
496 |     ".sql": "sql",
497 |     ".html": "html",
498 |     ".css": "css",
499 |     ".scss": "scss",
500 |     ".sass": "sass",
501 |     ".less": "less",
502 |     ".vue": "vue",
503 |     ".svelte": "svelte",
504 |     ".dart": "dart",
505 |     ".lua": "lua",
506 |     ".pl": "perl",
507 |     ".elm": "elm",
508 |     ".clj": "clojure",
509 |     ".ex": "elixir",
510 |     ".exs": "elixir",
511 |     ".erl": "erlang",
512 |     ".hrl": "erlang",
513 |     ".hs": "haskell",
514 |     ".ml": "ocaml",
515 |     ".fs": "fsharp",
516 |     ".nim": "nim",
517 |     ".cr": "crystal",
518 |     ".d": "d",
519 |     ".jl": "julia",
520 |     ".zig": "zig",
521 |   };
522 | 
523 |   return languageMap[ext] || null;
524 | }
525 | 
526 | // Analyze repository structure
527 | /**
528 |  * Analyzes the structural characteristics of a repository.
529 |  *
530 |  * Performs comprehensive structural analysis including file counting, directory
531 |  * structure analysis, language detection, and identification of key project
532 |  * components like tests, CI/CD, and documentation.
533 |  *
534 |  * @param repoPath - The file system path to the repository
535 |  * @param depth - Analysis depth level affecting thoroughness and performance
536 |  *
537 |  * @returns Promise resolving to repository structure analysis results
538 |  * @returns totalFiles - Total number of files in the repository
539 |  * @returns totalDirectories - Total number of directories
540 |  * @returns languages - Mapping of file extensions to counts
541 |  * @returns hasTests - Whether test files/directories are present
542 |  * @returns hasCI - Whether CI/CD configuration files are present
543 |  * @returns hasDocs - Whether documentation files are present
544 |  *
545 |  * @throws {Error} When repository structure cannot be analyzed
546 |  *
547 |  * @example
548 |  * ```typescript
549 |  * const structure = await analyzeStructure("/path/to/repo", "standard");
550 |  * console.log(`Found ${structure.totalFiles} files in ${structure.totalDirectories} directories`);
551 |  * ```
552 |  *
553 |  * @since 1.0.0
554 |  */
555 | async function analyzeStructure(
556 |   repoPath: string,
557 |   depth: "quick" | "standard" | "deep",
558 | ): Promise<RepositoryAnalysis["structure"]> {
559 |   const stats = {
560 |     totalFiles: 0,
561 |     totalDirectories: 0,
562 |     languages: {} as Record<string, number>,
563 |     hasTests: false,
564 |     hasCI: false,
565 |     hasDocs: false,
566 |   };
567 | 
568 |   const maxDepth = depth === "quick" ? 2 : depth === "standard" ? 5 : 10;
569 | 
570 |   async function walkDirectory(
571 |     dirPath: string,
572 |     currentDepth: number = 0,
573 |   ): Promise<void> {
574 |     if (currentDepth > maxDepth) return;
575 | 
576 |     try {
577 |       const entries = await fs.readdir(dirPath, { withFileTypes: true });
578 | 
579 |       for (const entry of entries) {
580 |         const fullPath = path.join(dirPath, entry.name);
581 | 
582 |         if (entry.isDirectory()) {
583 |           stats.totalDirectories++;
584 | 
585 |           // Check for special directories
586 |           if (
587 |             entry.name.includes("test") ||
588 |             entry.name.includes("spec") ||
589 |             entry.name === "__tests__"
590 |           ) {
591 |             stats.hasTests = true;
592 |           }
593 |           if (
594 |             entry.name === ".github" ||
595 |             entry.name === ".gitlab-ci" ||
596 |             entry.name === ".circleci"
597 |           ) {
598 |             stats.hasCI = true;
599 |           }
600 |           if (
601 |             entry.name === "docs" ||
602 |             entry.name === "documentation" ||
603 |             entry.name === "doc"
604 |           ) {
605 |             stats.hasDocs = true;
606 |           }
607 | 
608 |           // Skip node_modules and other common ignored directories
609 |           if (
610 |             ![
611 |               "node_modules",
612 |               ".git",
613 |               "dist",
614 |               "build",
615 |               ".next",
616 |               ".nuxt",
617 |             ].includes(entry.name)
618 |           ) {
619 |             await walkDirectory(fullPath, currentDepth + 1);
620 |           }
621 |         } else if (entry.isFile()) {
622 |           // Skip hidden files (starting with .)
623 |           if (!entry.name.startsWith(".")) {
624 |             stats.totalFiles++;
625 | 
626 |             // Track languages by file extension
627 |             const ext = path.extname(entry.name).toLowerCase();
628 |             if (ext && getLanguageFromExtension(ext)) {
629 |               stats.languages[ext] = (stats.languages[ext] || 0) + 1;
630 |             }
631 | 
632 |             // Check for CI files
633 |             if (
634 |               entry.name.match(/\.(yml|yaml)$/) &&
635 |               entry.name.includes("ci")
636 |             ) {
637 |               stats.hasCI = true;
638 |             }
639 | 
640 |             // Check for test files
641 |             if (entry.name.includes("test") || entry.name.includes("spec")) {
642 |               stats.hasTests = true;
643 |             }
644 |           }
645 |         }
646 |       }
647 |     } catch (error) {
648 |       // Skip directories we can't read
649 |     }
650 |   }
651 | 
652 |   await walkDirectory(repoPath);
653 |   return stats;
654 | }
655 | 
656 | // Analyze project dependencies
657 | async function analyzeDependencies(
658 |   repoPath: string,
659 | ): Promise<RepositoryAnalysis["dependencies"]> {
660 |   const result: RepositoryAnalysis["dependencies"] = {
661 |     ecosystem: "unknown",
662 |     packages: [],
663 |     devPackages: [],
664 |   };
665 | 
666 |   try {
667 |     // Check for package.json (JavaScript/TypeScript)
668 |     const packageJsonPath = path.join(repoPath, "package.json");
669 |     try {
670 |       const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
671 |       const packageJson = JSON.parse(packageJsonContent);
672 |       result.ecosystem = "javascript";
673 |       result.packages = Object.keys(packageJson.dependencies || {});
674 |       result.devPackages = Object.keys(packageJson.devDependencies || {});
675 |       return result;
676 |     } catch {
677 |       // Continue to check other ecosystems
678 |     }
679 | 
680 |     // Check for requirements.txt or pyproject.toml (Python)
681 |     const requirementsPath = path.join(repoPath, "requirements.txt");
682 |     const pyprojectPath = path.join(repoPath, "pyproject.toml");
683 |     try {
684 |       try {
685 |         const requirementsContent = await fs.readFile(
686 |           requirementsPath,
687 |           "utf-8",
688 |         );
689 |         result.ecosystem = "python";
690 |         result.packages = requirementsContent
691 |           .split("\n")
692 |           .filter((line) => line.trim() && !line.startsWith("#"))
693 |           .map((line) =>
694 |             line.split("==")[0].split(">=")[0].split("<=")[0].trim(),
695 |           );
696 |         return result;
697 |       } catch {
698 |         const pyprojectContent = await fs.readFile(pyprojectPath, "utf-8");
699 |         result.ecosystem = "python";
700 |         // Basic parsing for pyproject.toml dependencies
701 |         const dependencyMatches = pyprojectContent.match(
702 |           /dependencies\s*=\s*\[([\s\S]*?)\]/,
703 |         );
704 |         if (dependencyMatches) {
705 |           result.packages = dependencyMatches[1]
706 |             .split(",")
707 |             .map(
708 |               (dep) =>
709 |                 dep
710 |                   .trim()
711 |                   .replace(/["']/g, "")
712 |                   .split("==")[0]
713 |                   .split(">=")[0]
714 |                   .split("<=")[0],
715 |             )
716 |             .filter((dep) => dep.length > 0);
717 |         }
718 |         return result;
719 |       }
720 |     } catch {
721 |       // Continue to check other ecosystems
722 |     }
723 | 
724 |     // Check for Gemfile (Ruby)
725 |     const gemfilePath = path.join(repoPath, "Gemfile");
726 |     try {
727 |       const gemfileContent = await fs.readFile(gemfilePath, "utf-8");
728 |       result.ecosystem = "ruby";
729 |       const gemMatches = gemfileContent.match(/gem\s+['"]([^'"]+)['"]/g);
730 |       if (gemMatches) {
731 |         result.packages = gemMatches.map(
732 |           (match) => match.match(/gem\s+['"]([^'"]+)['"]/)![1],
733 |         );
734 |       }
735 |       return result;
736 |     } catch {
737 |       // Continue to check other ecosystems
738 |     }
739 | 
740 |     // Check for go.mod (Go)
741 |     const goModPath = path.join(repoPath, "go.mod");
742 |     try {
743 |       const goModContent = await fs.readFile(goModPath, "utf-8");
744 |       result.ecosystem = "go";
745 |       const requireMatches = goModContent.match(/require\s+\(([\s\S]*?)\)/);
746 |       if (requireMatches) {
747 |         result.packages = requireMatches[1]
748 |           .split("\n")
749 |           .map((line) => line.trim().split(" ")[0])
750 |           .filter((pkg) => pkg && !pkg.startsWith("//"));
751 |       }
752 |       return result;
753 |     } catch {
754 |       // No recognized dependency files found
755 |     }
756 | 
757 |     return result;
758 |   } catch (error) {
759 |     return result;
760 |   }
761 | }
762 | 
763 | // Analyze documentation structure
764 | async function analyzeDocumentation(
765 |   repoPath: string,
766 | ): Promise<RepositoryAnalysis["documentation"]> {
767 |   const result: RepositoryAnalysis["documentation"] = {
768 |     hasReadme: false,
769 |     hasContributing: false,
770 |     hasLicense: false,
771 |     existingDocs: [],
772 |     estimatedComplexity: "simple",
773 |   };
774 | 
775 |   try {
776 |     const entries = await fs.readdir(repoPath);
777 | 
778 |     // Check for standard files
779 |     for (const entry of entries) {
780 |       const lowerEntry = entry.toLowerCase();
781 |       if (lowerEntry.startsWith("readme")) {
782 |         result.hasReadme = true;
783 |       } else if (lowerEntry.startsWith("contributing")) {
784 |         result.hasContributing = true;
785 |       } else if (lowerEntry.startsWith("license")) {
786 |         result.hasLicense = true;
787 |       }
788 |     }
789 | 
790 |     // Find documentation files
791 |     const docExtensions = [".md", ".rst", ".txt", ".adoc"];
792 |     const commonDocDirs = ["docs", "documentation", "doc", "wiki"];
793 | 
794 |     // Check root directory for docs
795 |     for (const entry of entries) {
796 |       const entryPath = path.join(repoPath, entry);
797 |       const stat = await fs.stat(entryPath);
798 | 
799 |       if (
800 |         stat.isFile() &&
801 |         docExtensions.some((ext) => entry.toLowerCase().endsWith(ext))
802 |       ) {
803 |         result.existingDocs.push(entry);
804 |       } else if (
805 |         stat.isDirectory() &&
806 |         commonDocDirs.includes(entry.toLowerCase())
807 |       ) {
808 |         try {
809 |           const docFiles = await fs.readdir(entryPath);
810 |           for (const docFile of docFiles) {
811 |             if (
812 |               docExtensions.some((ext) => docFile.toLowerCase().endsWith(ext))
813 |             ) {
814 |               result.existingDocs.push(path.join(entry, docFile));
815 |             }
816 |           }
817 |         } catch {
818 |           // Skip if can't read directory
819 |         }
820 |       }
821 |     }
822 | 
823 |     // Estimate complexity based on documentation found
824 |     const docCount = result.existingDocs.length;
825 |     if (docCount <= 3) {
826 |       result.estimatedComplexity = "simple";
827 |     } else if (docCount <= 10) {
828 |       result.estimatedComplexity = "moderate";
829 |     } else {
830 |       result.estimatedComplexity = "complex";
831 |     }
832 | 
833 |     // Extract comprehensive documentation content
834 |     try {
835 |       result.extractedContent = await extractRepositoryContent(repoPath);
836 |     } catch (error) {
837 |       console.warn("Failed to extract repository content:", error);
838 |       // Continue without extracted content
839 |     }
840 | 
841 |     return result;
842 |   } catch (error) {
843 |     return result;
844 |   }
845 | }
846 | 
847 | // Helper function to count languages in a directory
848 | async function countLanguagesInDirectory(
849 |   dirPath: string,
850 |   languages: Record<string, number>,
851 |   depth: number = 0,
852 | ): Promise<void> {
853 |   if (depth > 3) return; // Limit depth for performance
854 | 
855 |   try {
856 |     const entries = await fs.readdir(dirPath, { withFileTypes: true });
857 | 
858 |     for (const entry of entries) {
859 |       if (entry.isFile()) {
860 |         const ext = path.extname(entry.name).toLowerCase();
861 |         if (ext && getLanguageFromExtension(ext)) {
862 |           languages[ext] = (languages[ext] || 0) + 1;
863 |         }
864 |       } else if (
865 |         entry.isDirectory() &&
866 |         !["node_modules", ".git", "dist"].includes(entry.name)
867 |       ) {
868 |         await countLanguagesInDirectory(
869 |           path.join(dirPath, entry.name),
870 |           languages,
871 |           depth + 1,
872 |         );
873 |       }
874 |     }
875 |   } catch {
876 |     // Skip directories we can't read
877 |   }
878 | }
879 | 
880 | // Generate recommendations based on analysis
881 | async function generateRecommendations(
882 |   repoPath: string,
883 | ): Promise<RepositoryAnalysis["recommendations"]> {
884 |   const result: RepositoryAnalysis["recommendations"] = {
885 |     primaryLanguage: "unknown",
886 |     projectType: "unknown",
887 |     teamSize: "solo",
888 |   };
889 | 
890 |   try {
891 |     // Determine primary language by counting files
892 |     const languages: Record<string, number> = {};
893 |     await countLanguagesInDirectory(repoPath, languages);
894 | 
895 |     // Find primary language
896 |     let primaryExt = "";
897 |     if (Object.keys(languages).length > 0) {
898 |       primaryExt = Object.entries(languages).reduce((a, b) =>
899 |         a[1] > b[1] ? a : b,
900 |       )[0];
901 |       const primaryLanguage = getLanguageFromExtension(primaryExt);
902 |       result.primaryLanguage = primaryLanguage || "unknown";
903 |     }
904 | 
905 |     // Determine project type based on files and structure
906 |     const entries = await fs.readdir(repoPath);
907 |     const hasPackageJson = entries.includes("package.json");
908 |     const hasDockerfile = entries.includes("Dockerfile");
909 |     const hasK8sFiles = entries.some(
910 |       (entry) => entry.endsWith(".yaml") || entry.endsWith(".yml"),
911 |     );
912 |     const hasTests = entries.some(
913 |       (entry) => entry.includes("test") || entry.includes("spec"),
914 |     );
915 | 
916 |     if (hasPackageJson && entries.includes("src") && hasTests) {
917 |       result.projectType = "library";
918 |     } else if (hasDockerfile || hasK8sFiles) {
919 |       result.projectType = "application";
920 |     } else if (entries.includes("docs") || entries.includes("documentation")) {
921 |       result.projectType = "documentation";
922 |     } else if (hasTests && primaryExt && languages[primaryExt] > 10) {
923 |       result.projectType = "application";
924 |     } else {
925 |       result.projectType = "script";
926 |     }
927 | 
928 |     // Estimate team size based on complexity and structure
929 |     const totalFiles = Object.values(languages).reduce(
930 |       (sum, count) => sum + count,
931 |       0,
932 |     );
933 |     const hasCI = entries.some(
934 |       (entry) => entry.includes(".github") || entry.includes(".gitlab"),
935 |     );
936 |     const hasContributing = entries.some((entry) =>
937 |       entry.toLowerCase().includes("contributing"),
938 |     );
939 | 
940 |     if (totalFiles > 100 || (hasCI && hasContributing)) {
941 |       result.teamSize = "large";
942 |     } else if (totalFiles > 50 || hasCI) {
943 |       result.teamSize = "medium";
944 |     } else if (totalFiles > 20 || hasTests) {
945 |       result.teamSize = "small";
946 |     } else {
947 |       result.teamSize = "solo";
948 |     }
949 | 
950 |     return result;
951 |   } catch (error) {
952 |     return result;
953 |   }
954 | }
955 | 
```

--------------------------------------------------------------------------------
/tests/tools/check-documentation-links.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { checkDocumentationLinks } from "../../src/tools/check-documentation-links.js";
  2 | import { formatMCPResponse } from "../../src/types/api.js";
  3 | import { writeFile, mkdir, rm } from "fs/promises";
  4 | import { join } from "path";
  5 | 
  6 | describe("checkDocumentationLinks", () => {
  7 |   const testDir = join(process.cwd(), "test-docs-temp");
  8 | 
  9 |   beforeEach(async () => {
 10 |     // Create test directory structure
 11 |     await mkdir(testDir, { recursive: true });
 12 |     await mkdir(join(testDir, "guides"), { recursive: true });
 13 |     await mkdir(join(testDir, "api"), { recursive: true });
 14 |   });
 15 | 
 16 |   afterEach(async () => {
 17 |     // Clean up test directory
 18 |     try {
 19 |       await rm(testDir, { recursive: true, force: true });
 20 |     } catch (error) {
 21 |       // Ignore cleanup errors
 22 |     }
 23 |   });
 24 | 
 25 |   describe("Input Validation", () => {
 26 |     test("should use default values for optional parameters", async () => {
 27 |       await writeFile(
 28 |         join(testDir, "README.md"),
 29 |         "# Test\n[Link](./guides/test.md)",
 30 |       );
 31 |       await writeFile(join(testDir, "guides", "test.md"), "# Guide");
 32 | 
 33 |       const result = await checkDocumentationLinks({
 34 |         documentation_path: testDir,
 35 |       });
 36 | 
 37 |       const formatted = formatMCPResponse(result);
 38 |       expect(formatted.isError).toBe(false);
 39 |       const contentText = formatted.content.map((c) => c.text).join(" ");
 40 |       expect(contentText).toContain('"totalLinks": 1');
 41 |     });
 42 | 
 43 |     test("should validate timeout_ms parameter", async () => {
 44 |       const result = await checkDocumentationLinks({
 45 |         documentation_path: testDir,
 46 |         timeout_ms: 500, // Below minimum
 47 |       });
 48 | 
 49 |       const formatted = formatMCPResponse(result);
 50 |       expect(formatted.isError).toBe(true);
 51 |       const contentText = formatted.content.map((c) => c.text).join(" ");
 52 |       expect(contentText).toContain(
 53 |         "Number must be greater than or equal to 1000",
 54 |       );
 55 |     });
 56 | 
 57 |     test("should validate max_concurrent_checks parameter", async () => {
 58 |       const result = await checkDocumentationLinks({
 59 |         documentation_path: testDir,
 60 |         max_concurrent_checks: 25, // Above maximum
 61 |       });
 62 | 
 63 |       const formatted = formatMCPResponse(result);
 64 |       expect(formatted.isError).toBe(true);
 65 |       const contentText = formatted.content.map((c) => c.text).join(" ");
 66 |       expect(contentText).toContain("Number must be less than or equal to 20");
 67 |     });
 68 |   });
 69 | 
 70 |   describe("File Scanning", () => {
 71 |     test("should find markdown files in nested directories", async () => {
 72 |       await writeFile(join(testDir, "README.md"), "# Root");
 73 |       await writeFile(join(testDir, "guides", "guide1.md"), "# Guide 1");
 74 |       await writeFile(join(testDir, "api", "reference.mdx"), "# API Reference");
 75 | 
 76 |       const result = await checkDocumentationLinks({
 77 |         documentation_path: testDir,
 78 |         check_external_links: false,
 79 |       });
 80 | 
 81 |       const formatted = formatMCPResponse(result);
 82 |       expect(formatted.isError).toBe(false);
 83 |       const contentText = formatted.content.map((c) => c.text).join(" ");
 84 |       expect(contentText).toContain('"filesScanned": 3');
 85 |     });
 86 | 
 87 |     test("should handle empty documentation directory", async () => {
 88 |       const result = await checkDocumentationLinks({
 89 |         documentation_path: testDir,
 90 |       });
 91 | 
 92 |       const formatted = formatMCPResponse(result);
 93 |       expect(formatted.isError).toBe(true);
 94 |       const contentText = formatted.content.map((c) => c.text).join(" ");
 95 |       expect(contentText).toContain("No documentation files found");
 96 |     });
 97 | 
 98 |     test("should handle non-existent directory", async () => {
 99 |       const result = await checkDocumentationLinks({
100 |         documentation_path: "/non/existent/path",
101 |       });
102 | 
103 |       const formatted = formatMCPResponse(result);
104 |       expect(formatted.isError).toBe(true);
105 |     });
106 |   });
107 | 
108 |   describe("Link Extraction", () => {
109 |     test("should extract markdown links", async () => {
110 |       const content = `# Test Document
111 | [Internal Link](./guides/test.md)
112 | [External Link](https://example.com)
113 | `;
114 |       await writeFile(join(testDir, "test.md"), content);
115 | 
116 |       const result = await checkDocumentationLinks({
117 |         documentation_path: testDir,
118 |         check_external_links: false,
119 |         check_internal_links: true,
120 |       });
121 | 
122 |       const formatted = formatMCPResponse(result);
123 |       expect(formatted.isError).toBe(false);
124 |       const contentText = formatted.content.map((c) => c.text).join(" ");
125 |       expect(contentText).toContain('"totalLinks": 1');
126 |     });
127 | 
128 |     test("should extract HTML links", async () => {
129 |       const content = `# Test Document
130 | <a href="./guides/test.md">Internal Link</a>
131 | <a href="https://example.com">External Link</a>
132 | `;
133 |       await writeFile(join(testDir, "test.md"), content);
134 | 
135 |       const result = await checkDocumentationLinks({
136 |         documentation_path: testDir,
137 |         check_external_links: false,
138 |       });
139 | 
140 |       const formatted = formatMCPResponse(result);
141 |       expect(formatted.isError).toBe(false);
142 |     });
143 | 
144 |     test("should skip mailto and tel links", async () => {
145 |       const content = `# Contact
146 | [Email](mailto:[email protected])
147 | [Phone](tel:+1234567890)
148 | [Valid Link](./test.md)
149 | `;
150 |       await writeFile(join(testDir, "contact.md"), content);
151 |       await writeFile(join(testDir, "test.md"), "# Test");
152 | 
153 |       const result = await checkDocumentationLinks({
154 |         documentation_path: testDir,
155 |         check_external_links: false,
156 |       });
157 | 
158 |       const formatted = formatMCPResponse(result);
159 |       expect(formatted.isError).toBe(false);
160 |       // Should only check the valid link, not mailto/tel
161 |     });
162 |   });
163 | 
164 |   describe("Internal Link Checking", () => {
165 |     test("should validate existing internal links", async () => {
166 |       await writeFile(join(testDir, "README.md"), "[Valid](./guides/test.md)");
167 |       await mkdir(join(testDir, "guides"), { recursive: true });
168 |       await writeFile(join(testDir, "guides", "test.md"), "# Test");
169 | 
170 |       const result = await checkDocumentationLinks({
171 |         documentation_path: testDir,
172 |         check_external_links: false,
173 |         check_internal_links: true,
174 |       });
175 | 
176 |       const formatted = formatMCPResponse(result);
177 |       expect(formatted.isError).toBe(false);
178 |       const contentText = formatted.content.map((c) => c.text).join(" ");
179 |       expect(contentText).toContain('"status": "valid"');
180 |     });
181 | 
182 |     test("should detect broken internal links", async () => {
183 |       await writeFile(join(testDir, "README.md"), "[Broken](./missing.md)");
184 | 
185 |       const result = await checkDocumentationLinks({
186 |         documentation_path: testDir,
187 |         check_external_links: false,
188 |         check_internal_links: true,
189 |         fail_on_broken_links: false,
190 |       });
191 | 
192 |       const formatted = formatMCPResponse(result);
193 |       expect(formatted.isError).toBe(false);
194 |       const contentText = formatted.content.map((c) => c.text).join(" ");
195 |       expect(contentText).toContain('"status": "broken"');
196 |     });
197 | 
198 |     test("should handle relative path navigation", async () => {
199 |       await writeFile(
200 |         join(testDir, "guides", "guide1.md"),
201 |         "[Back](../README.md)",
202 |       );
203 |       await writeFile(join(testDir, "README.md"), "# Root");
204 | 
205 |       const result = await checkDocumentationLinks({
206 |         documentation_path: testDir,
207 |         check_external_links: false,
208 |         check_internal_links: true,
209 |       });
210 | 
211 |       const formatted = formatMCPResponse(result);
212 |       expect(formatted.isError).toBe(false);
213 |       const contentText = formatted.content.map((c) => c.text).join(" ");
214 |       expect(contentText).toContain('"status": "valid"');
215 |     });
216 | 
217 |     test("should handle anchor links in internal files", async () => {
218 |       await writeFile(
219 |         join(testDir, "README.md"),
220 |         "[Section](./guide.md#section)",
221 |       );
222 |       await writeFile(join(testDir, "guide.md"), "# Guide\n## Section");
223 | 
224 |       const result = await checkDocumentationLinks({
225 |         documentation_path: testDir,
226 |         check_external_links: false,
227 |         check_internal_links: true,
228 |       });
229 | 
230 |       const formatted = formatMCPResponse(result);
231 |       expect(formatted.isError).toBe(false);
232 |     });
233 |   });
234 | 
235 |   describe("External Link Checking", () => {
236 |     test("should skip external links when disabled", async () => {
237 |       await writeFile(
238 |         join(testDir, "README.md"),
239 |         "[External](https://example.com)",
240 |       );
241 | 
242 |       const result = await checkDocumentationLinks({
243 |         documentation_path: testDir,
244 |         check_external_links: false,
245 |       });
246 | 
247 |       const formatted = formatMCPResponse(result);
248 |       expect(formatted.isError).toBe(false);
249 |       // Should have 0 links checked since external checking is disabled
250 |     });
251 | 
252 |     test("should respect allowed domains", async () => {
253 |       await writeFile(
254 |         join(testDir, "README.md"),
255 |         `
256 | [Allowed](https://github.com/test)
257 | [Not Allowed](https://example.com)
258 | `,
259 |       );
260 | 
261 |       const result = await checkDocumentationLinks({
262 |         documentation_path: testDir,
263 |         check_external_links: true,
264 |         allowed_domains: ["github.com"],
265 |       });
266 | 
267 |       const formatted = formatMCPResponse(result);
268 |       expect(formatted.isError).toBe(false);
269 |     });
270 | 
271 |     test("should handle timeout for slow external links", async () => {
272 |       await writeFile(
273 |         join(testDir, "README.md"),
274 |         "[Slow](https://httpstat.us/200?sleep=10000)",
275 |       );
276 | 
277 |       const result = await checkDocumentationLinks({
278 |         documentation_path: testDir,
279 |         check_external_links: true,
280 |         timeout_ms: 1000,
281 |       });
282 | 
283 |       const formatted = formatMCPResponse(result);
284 |       expect(formatted.isError).toBe(false);
285 |       // Should timeout and mark as warning
286 |     });
287 |   });
288 | 
289 |   describe("Link Filtering", () => {
290 |     test("should ignore links matching ignore patterns", async () => {
291 |       await writeFile(
292 |         join(testDir, "README.md"),
293 |         `
294 | [Ignored](./temp/file.md)
295 | [Valid](./guides/test.md)
296 | `,
297 |       );
298 |       await writeFile(join(testDir, "guides", "test.md"), "# Test");
299 | 
300 |       const result = await checkDocumentationLinks({
301 |         documentation_path: testDir,
302 |         check_external_links: false,
303 |         ignore_patterns: ["temp/"],
304 |       });
305 | 
306 |       const formatted = formatMCPResponse(result);
307 |       expect(formatted.isError).toBe(false);
308 |       // Should only check the valid link, ignore the temp/ link
309 |     });
310 | 
311 |     test("should filter by link types", async () => {
312 |       await writeFile(
313 |         join(testDir, "README.md"),
314 |         `
315 | [Internal](./test.md)
316 | [External](https://example.com)
317 | [Anchor](#section)
318 | `,
319 |       );
320 |       await writeFile(join(testDir, "test.md"), "# Test");
321 | 
322 |       const result = await checkDocumentationLinks({
323 |         documentation_path: testDir,
324 |         check_external_links: false,
325 |         check_internal_links: true,
326 |         check_anchor_links: false,
327 |       });
328 | 
329 |       const formatted = formatMCPResponse(result);
330 |       expect(formatted.isError).toBe(false);
331 |       // Should only check internal links
332 |     });
333 |   });
334 | 
335 |   describe("Failure Modes", () => {
336 |     test("should fail when fail_on_broken_links is true and links are broken", async () => {
337 |       await writeFile(join(testDir, "README.md"), "[Broken](./missing.md)");
338 | 
339 |       const result = await checkDocumentationLinks({
340 |         documentation_path: testDir,
341 |         check_external_links: false,
342 |         fail_on_broken_links: true,
343 |       });
344 | 
345 |       const formatted = formatMCPResponse(result);
346 |       expect(formatted.isError).toBe(true);
347 |       const contentText = formatted.content.map((c) => c.text).join(" ");
348 |       expect(contentText).toContain("Found 1 broken links");
349 |     });
350 | 
351 |     test("should not fail when fail_on_broken_links is false", async () => {
352 |       await writeFile(join(testDir, "README.md"), "[Broken](./missing.md)");
353 | 
354 |       const result = await checkDocumentationLinks({
355 |         documentation_path: testDir,
356 |         check_external_links: false,
357 |         fail_on_broken_links: false,
358 |       });
359 | 
360 |       const formatted = formatMCPResponse(result);
361 |       expect(formatted.isError).toBe(false);
362 |     });
363 |   });
364 | 
365 |   describe("Report Generation", () => {
366 |     test("should generate comprehensive report with summary", async () => {
367 |       await writeFile(
368 |         join(testDir, "README.md"),
369 |         `
370 | [Valid Internal](./test.md)
371 | [Broken Internal](./missing.md)
372 | `,
373 |       );
374 |       await writeFile(join(testDir, "test.md"), "# Test");
375 | 
376 |       const result = await checkDocumentationLinks({
377 |         documentation_path: testDir,
378 |         check_external_links: false,
379 |         fail_on_broken_links: false,
380 |       });
381 | 
382 |       const formatted = formatMCPResponse(result);
383 |       expect(formatted.isError).toBe(false);
384 |       const contentText = formatted.content.map((c) => c.text).join(" ");
385 |       expect(contentText).toContain('"summary"');
386 |       expect(contentText).toContain('"results"');
387 |       expect(contentText).toContain('"recommendations"');
388 |       expect(contentText).toContain('"totalLinks": 2');
389 |     });
390 | 
391 |     test("should include execution metrics", async () => {
392 |       await writeFile(join(testDir, "README.md"), "[Test](./test.md)");
393 |       await writeFile(join(testDir, "test.md"), "# Test");
394 | 
395 |       const result = await checkDocumentationLinks({
396 |         documentation_path: testDir,
397 |         check_external_links: false,
398 |       });
399 | 
400 |       const formatted = formatMCPResponse(result);
401 |       expect(formatted.isError).toBe(false);
402 |       const contentText = formatted.content.map((c) => c.text).join(" ");
403 |       expect(contentText).toContain('"executionTime"');
404 |       expect(contentText).toContain('"filesScanned"');
405 |     });
406 | 
407 |     test("should provide recommendations based on results", async () => {
408 |       await writeFile(join(testDir, "README.md"), "[Valid](./test.md)");
409 |       await writeFile(join(testDir, "test.md"), "# Test");
410 | 
411 |       const result = await checkDocumentationLinks({
412 |         documentation_path: testDir,
413 |         check_external_links: false,
414 |       });
415 | 
416 |       const formatted = formatMCPResponse(result);
417 |       expect(formatted.isError).toBe(false);
418 |       const contentText = formatted.content.map((c) => c.text).join(" ");
419 |       expect(contentText).toContain(
420 |         "All links are valid - excellent documentation quality!",
421 |       );
422 |     });
423 |   });
424 | 
425 |   describe("Concurrency Control", () => {
426 |     test("should respect max_concurrent_checks limit", async () => {
427 |       // Create multiple files with links
428 |       for (let i = 0; i < 10; i++) {
429 |         await writeFile(
430 |           join(testDir, `file${i}.md`),
431 |           `[Link](./target${i}.md)`,
432 |         );
433 |         await writeFile(join(testDir, `target${i}.md`), `# Target ${i}`);
434 |       }
435 | 
436 |       const result = await checkDocumentationLinks({
437 |         documentation_path: testDir,
438 |         check_external_links: false,
439 |         max_concurrent_checks: 2,
440 |       });
441 | 
442 |       const formatted = formatMCPResponse(result);
443 |       expect(formatted.isError).toBe(false);
444 |       // Should complete successfully with concurrency control
445 |     });
446 |   });
447 | 
448 |   describe("Edge Cases", () => {
449 |     test("should handle files with no links", async () => {
450 |       await writeFile(
451 |         join(testDir, "README.md"),
452 |         "# No Links Here\nJust plain text.",
453 |       );
454 | 
455 |       const result = await checkDocumentationLinks({
456 |         documentation_path: testDir,
457 |       });
458 | 
459 |       const formatted = formatMCPResponse(result);
460 |       expect(formatted.isError).toBe(false);
461 |       const contentText = formatted.content.map((c) => c.text).join(" ");
462 |       expect(contentText).toContain('"totalLinks": 0');
463 |     });
464 | 
465 |     test("should handle malformed markdown", async () => {
466 |       await writeFile(
467 |         join(testDir, "README.md"),
468 |         `
469 | # Malformed
470 | [Incomplete link](
471 | [Missing closing](test.md
472 | [Valid](./test.md)
473 | `,
474 |       );
475 |       await writeFile(join(testDir, "test.md"), "# Test");
476 | 
477 |       const result = await checkDocumentationLinks({
478 |         documentation_path: testDir,
479 |         check_external_links: false,
480 |       });
481 | 
482 |       const formatted = formatMCPResponse(result);
483 |       expect(formatted.isError).toBe(false);
484 |       // Should handle malformed links gracefully
485 |     });
486 | 
487 |     test("should handle binary files gracefully", async () => {
488 |       await writeFile(join(testDir, "README.md"), "[Test](./test.md)");
489 |       await writeFile(join(testDir, "test.md"), "# Test");
490 |       // Create a binary file that should be ignored
491 |       await writeFile(
492 |         join(testDir, "image.png"),
493 |         Buffer.from([0x89, 0x50, 0x4e, 0x47]),
494 |       );
495 | 
496 |       const result = await checkDocumentationLinks({
497 |         documentation_path: testDir,
498 |         check_external_links: false,
499 |       });
500 | 
501 |       const formatted = formatMCPResponse(result);
502 |       expect(formatted.isError).toBe(false);
503 |       // Should ignore binary files and process markdown files
504 |     });
505 |   });
506 | 
507 |   describe("Advanced Branch Coverage Tests", () => {
508 |     test("should handle reference links", async () => {
509 |       const content = `# Test Document
510 | [Reference Link][ref1]
511 | [Another Reference][ref2]
512 | 
513 | [ref1]: ./guides/test.md
514 | [ref2]: https://example.com
515 | `;
516 |       await writeFile(join(testDir, "test.md"), content);
517 |       await writeFile(join(testDir, "guides", "test.md"), "# Guide");
518 | 
519 |       const result = await checkDocumentationLinks({
520 |         documentation_path: testDir,
521 |         check_external_links: false,
522 |       });
523 | 
524 |       const formatted = formatMCPResponse(result);
525 |       expect(formatted.isError).toBe(false);
526 |       const contentText = formatted.content.map((c) => c.text).join(" ");
527 |       expect(contentText).toContain('"totalLinks": 1');
528 |     });
529 | 
530 |     test("should handle anchor-only links", async () => {
531 |       const content = `# Test Document
532 | [Anchor Only](#section)
533 | [Valid Link](./test.md)
534 | `;
535 |       await writeFile(join(testDir, "README.md"), content);
536 |       await writeFile(join(testDir, "test.md"), "# Test");
537 | 
538 |       const result = await checkDocumentationLinks({
539 |         documentation_path: testDir,
540 |         check_external_links: false,
541 |         check_anchor_links: true,
542 |       });
543 | 
544 |       const formatted = formatMCPResponse(result);
545 |       expect(formatted.isError).toBe(false);
546 |     });
547 | 
548 |     test("should handle empty URL in links", async () => {
549 |       const content = `# Test Document
550 | [Empty Link]()
551 | [Valid Link](./test.md)
552 | `;
553 |       await writeFile(join(testDir, "README.md"), content);
554 |       await writeFile(join(testDir, "test.md"), "# Test");
555 | 
556 |       const result = await checkDocumentationLinks({
557 |         documentation_path: testDir,
558 |         check_external_links: false,
559 |       });
560 | 
561 |       const formatted = formatMCPResponse(result);
562 |       expect(formatted.isError).toBe(false);
563 |     });
564 | 
565 |     test("should handle different internal link path formats", async () => {
566 |       await mkdir(join(testDir, "subdir"), { recursive: true });
567 |       await mkdir(join(testDir, "guides"), { recursive: true });
568 |       await writeFile(
569 |         join(testDir, "subdir", "nested.md"),
570 |         `
571 | [Current Dir](./file.md)
572 | [Parent Dir](../README.md)
573 | [Absolute](/guides/test.md)
574 | [Relative](file.md)
575 | `,
576 |       );
577 |       await writeFile(join(testDir, "subdir", "file.md"), "# File");
578 |       await writeFile(join(testDir, "README.md"), "# Root");
579 |       await writeFile(join(testDir, "guides", "test.md"), "# Guide");
580 | 
581 |       const result = await checkDocumentationLinks({
582 |         documentation_path: testDir,
583 |         check_external_links: false,
584 |       });
585 | 
586 |       const formatted = formatMCPResponse(result);
587 |       expect(formatted.isError).toBe(false);
588 |     });
589 | 
590 |     test("should handle external link domain filtering", async () => {
591 |       await writeFile(
592 |         join(testDir, "README.md"),
593 |         `
594 | [GitHub](https://github.com/test)
595 | [Subdomain](https://api.github.com/test)
596 | [Not Allowed](https://example.com)
597 | `,
598 |       );
599 | 
600 |       const result = await checkDocumentationLinks({
601 |         documentation_path: testDir,
602 |         check_external_links: true,
603 |         allowed_domains: ["github.com"],
604 |       });
605 | 
606 |       const formatted = formatMCPResponse(result);
607 |       expect(formatted.isError).toBe(false);
608 |     });
609 | 
610 |     test("should handle external link fetch errors", async () => {
611 |       await writeFile(
612 |         join(testDir, "README.md"),
613 |         "[Invalid URL](https://invalid-domain-that-does-not-exist-12345.com)",
614 |       );
615 | 
616 |       const result = await checkDocumentationLinks({
617 |         documentation_path: testDir,
618 |         check_external_links: true,
619 |         timeout_ms: 2000,
620 |       });
621 | 
622 |       const formatted = formatMCPResponse(result);
623 |       expect(formatted.isError).toBe(false);
624 |       const contentText = formatted.content.map((c) => c.text).join(" ");
625 |       expect(contentText).toContain('"status": "broken"');
626 |     });
627 | 
628 |     test("should handle HTTP error status codes", async () => {
629 |       await writeFile(
630 |         join(testDir, "README.md"),
631 |         "[Not Found](https://httpstat.us/404)",
632 |       );
633 | 
634 |       const result = await checkDocumentationLinks({
635 |         documentation_path: testDir,
636 |         check_external_links: true,
637 |         timeout_ms: 5000,
638 |       });
639 | 
640 |       const formatted = formatMCPResponse(result);
641 |       expect(formatted.isError).toBe(false);
642 |     });
643 | 
644 |     test("should handle directory scanning errors", async () => {
645 |       // Create a directory with restricted permissions
646 |       await mkdir(join(testDir, "restricted"), { recursive: true });
647 |       await writeFile(join(testDir, "README.md"), "[Test](./test.md)");
648 |       await writeFile(join(testDir, "test.md"), "# Test");
649 | 
650 |       const result = await checkDocumentationLinks({
651 |         documentation_path: testDir,
652 |         check_external_links: false,
653 |       });
654 | 
655 |       const formatted = formatMCPResponse(result);
656 |       expect(formatted.isError).toBe(false);
657 |     });
658 | 
659 |     test("should handle file reading errors gracefully", async () => {
660 |       await writeFile(join(testDir, "README.md"), "[Test](./test.md)");
661 |       await writeFile(join(testDir, "test.md"), "# Test");
662 |       // Create a file that might cause reading issues
663 |       await writeFile(join(testDir, "problematic.md"), "# Test\x00\x01\x02");
664 | 
665 |       const result = await checkDocumentationLinks({
666 |         documentation_path: testDir,
667 |         check_external_links: false,
668 |       });
669 | 
670 |       const formatted = formatMCPResponse(result);
671 |       expect(formatted.isError).toBe(false);
672 |     });
673 | 
674 |     test("should generate recommendations for large link counts", async () => {
675 |       let content = "# Test Document\n";
676 |       for (let i = 0; i < 101; i++) {
677 |         content += `[Link ${i}](./file${i}.md)\n`;
678 |         await writeFile(join(testDir, `file${i}.md`), `# File ${i}`);
679 |       }
680 |       await writeFile(join(testDir, "README.md"), content);
681 | 
682 |       const result = await checkDocumentationLinks({
683 |         documentation_path: testDir,
684 |         check_external_links: false,
685 |       });
686 | 
687 |       const formatted = formatMCPResponse(result);
688 |       expect(formatted.isError).toBe(false);
689 |       const contentText = formatted.content.map((c) => c.text).join(" ");
690 |       expect(contentText).toContain(
691 |         "Consider implementing automated link checking in CI/CD pipeline",
692 |       );
693 |     });
694 | 
695 |     test("should handle mixed link types with warnings", async () => {
696 |       await writeFile(
697 |         join(testDir, "README.md"),
698 |         `
699 | [Valid](./test.md)
700 | [Broken](./missing.md)
701 | [Timeout](https://httpstat.us/200?sleep=10000)
702 | `,
703 |       );
704 |       await writeFile(join(testDir, "test.md"), "# Test");
705 | 
706 |       const result = await checkDocumentationLinks({
707 |         documentation_path: testDir,
708 |         check_external_links: true,
709 |         timeout_ms: 1000,
710 |         fail_on_broken_links: false,
711 |       });
712 | 
713 |       const formatted = formatMCPResponse(result);
714 |       expect(formatted.isError).toBe(false);
715 |       const contentText = formatted.content.map((c) => c.text).join(" ");
716 |       expect(contentText).toContain('"brokenLinks"');
717 |       expect(contentText).toContain('"warningLinks"');
718 |     });
719 | 
720 |     test("should handle node_modules and hidden directory exclusion", async () => {
721 |       await mkdir(join(testDir, "node_modules"), { recursive: true });
722 |       await mkdir(join(testDir, ".hidden"), { recursive: true });
723 |       await writeFile(
724 |         join(testDir, "node_modules", "package.md"),
725 |         "# Should be ignored",
726 |       );
727 |       await writeFile(
728 |         join(testDir, ".hidden", "secret.md"),
729 |         "# Should be ignored",
730 |       );
731 |       await writeFile(join(testDir, "README.md"), "[Test](./test.md)");
732 |       await writeFile(join(testDir, "test.md"), "# Test");
733 | 
734 |       const result = await checkDocumentationLinks({
735 |         documentation_path: testDir,
736 |         check_external_links: false,
737 |       });
738 | 
739 |       const formatted = formatMCPResponse(result);
740 |       expect(formatted.isError).toBe(false);
741 |       const contentText = formatted.content.map((c) => c.text).join(" ");
742 |       expect(contentText).toContain('"filesScanned": 2'); // Only README.md and test.md
743 |     });
744 | 
745 |     test("should handle different markdown file extensions", async () => {
746 |       await writeFile(join(testDir, "README.md"), "[MD](./test.md)");
747 |       await writeFile(join(testDir, "guide.mdx"), "[MDX](./test.md)");
748 |       await writeFile(join(testDir, "doc.markdown"), "[MARKDOWN](./test.md)");
749 |       await writeFile(join(testDir, "test.md"), "# Test");
750 | 
751 |       const result = await checkDocumentationLinks({
752 |         documentation_path: testDir,
753 |         check_external_links: false,
754 |       });
755 | 
756 |       const formatted = formatMCPResponse(result);
757 |       expect(formatted.isError).toBe(false);
758 |       const contentText = formatted.content.map((c) => c.text).join(" ");
759 |       expect(contentText).toContain('"filesScanned": 4');
760 |     });
761 |   });
762 | 
763 |   describe("Special Link Types", () => {
764 |     test("should skip mailto links during filtering", async () => {
765 |       await writeFile(
766 |         join(testDir, "README.md"),
767 |         "[Email](mailto:[email protected])\n[Valid](./guide.md)",
768 |       );
769 |       await writeFile(join(testDir, "guide.md"), "# Guide");
770 | 
771 |       const result = await checkDocumentationLinks({
772 |         documentation_path: testDir,
773 |         check_external_links: true,
774 |         check_internal_links: true,
775 |       });
776 | 
777 |       const formatted = formatMCPResponse(result);
778 |       expect(formatted.isError).toBe(false);
779 |       const contentText = formatted.content.map((c) => c.text).join(" ");
780 |       // Should find only the internal link, mailto is filtered
781 |       expect(contentText).toContain('"totalLinks": 1');
782 |       expect(contentText).toContain("./guide.md");
783 |     });
784 | 
785 |     test("should skip tel links during filtering", async () => {
786 |       await writeFile(
787 |         join(testDir, "README.md"),
788 |         "[Phone](tel:+1234567890)\n[Valid](./guide.md)",
789 |       );
790 |       await writeFile(join(testDir, "guide.md"), "# Guide");
791 | 
792 |       const result = await checkDocumentationLinks({
793 |         documentation_path: testDir,
794 |         check_external_links: true,
795 |         check_internal_links: true,
796 |       });
797 | 
798 |       const formatted = formatMCPResponse(result);
799 |       expect(formatted.isError).toBe(false);
800 |       const contentText = formatted.content.map((c) => c.text).join(" ");
801 |       // Should find only the internal link, tel is filtered
802 |       expect(contentText).toContain('"totalLinks": 1');
803 |       expect(contentText).toContain("./guide.md");
804 |     });
805 | 
806 |     test("should check anchor links when enabled and file exists", async () => {
807 |       await writeFile(
808 |         join(testDir, "README.md"),
809 |         "[Guide Anchor](./guide.md#introduction)",
810 |       );
811 |       await writeFile(
812 |         join(testDir, "guide.md"),
813 |         "# Guide\n\n## Introduction\n\nContent here.",
814 |       );
815 | 
816 |       const result = await checkDocumentationLinks({
817 |         documentation_path: testDir,
818 |         check_anchor_links: true,
819 |         check_internal_links: true,
820 |         check_external_links: false,
821 |       });
822 | 
823 |       const formatted = formatMCPResponse(result);
824 |       expect(formatted.isError).toBe(false);
825 |       const contentText = formatted.content.map((c) => c.text).join(" ");
826 |       expect(contentText).toContain('"totalLinks": 1');
827 |       expect(contentText).toContain("./guide.md#introduction");
828 |     });
829 | 
830 |     test("should handle anchor links to other files", async () => {
831 |       await writeFile(
832 |         join(testDir, "README.md"),
833 |         "[Guide Section](./guide.md#setup)",
834 |       );
835 |       await writeFile(
836 |         join(testDir, "guide.md"),
837 |         "# Guide\n\n## Setup\n\nSetup instructions.",
838 |       );
839 | 
840 |       const result = await checkDocumentationLinks({
841 |         documentation_path: testDir,
842 |         check_anchor_links: true,
843 |         check_internal_links: true,
844 |         check_external_links: false,
845 |       });
846 | 
847 |       const formatted = formatMCPResponse(result);
848 |       expect(formatted.isError).toBe(false);
849 |       const contentText = formatted.content.map((c) => c.text).join(" ");
850 |       expect(contentText).toContain('"totalLinks": 1');
851 |     });
852 |   });
853 | 
854 |   describe("Error Handling Edge Cases", () => {
855 |     test("should handle internal link check errors gracefully", async () => {
856 |       await writeFile(
857 |         join(testDir, "README.md"),
858 |         "[Broken](./nonexistent/deeply/nested/file.md)",
859 |       );
860 | 
861 |       const result = await checkDocumentationLinks({
862 |         documentation_path: testDir,
863 |         check_internal_links: true,
864 |         check_external_links: false,
865 |       });
866 | 
867 |       const formatted = formatMCPResponse(result);
868 |       expect(formatted.isError).toBe(false);
869 |       const contentText = formatted.content.map((c) => c.text).join(" ");
870 |       // Should report broken link
871 |       expect(contentText).toContain('"brokenLinks": 1');
872 |     });
873 | 
874 |     test("should handle network errors for external links", async () => {
875 |       await writeFile(
876 |         join(testDir, "README.md"),
877 |         "[Invalid](https://this-domain-should-not-exist-12345.com)",
878 |       );
879 | 
880 |       const result = await checkDocumentationLinks({
881 |         documentation_path: testDir,
882 |         check_external_links: true,
883 |         timeout_ms: 2000,
884 |       });
885 | 
886 |       const formatted = formatMCPResponse(result);
887 |       expect(formatted.isError).toBe(false);
888 |       // Should handle as broken or warning
889 |     });
890 | 
891 |     test("should handle multiple link types in same document", async () => {
892 |       await writeFile(
893 |         join(testDir, "README.md"),
894 |         `
895 | # Documentation
896 | 
897 | [Internal](./guide.md)
898 | [External](https://github.com)
899 | [Email](mailto:[email protected])
900 | [Phone](tel:123-456-7890)
901 | [Anchor](./guide.md#section)
902 | 
903 | ## Section
904 | Content here.
905 | `,
906 |       );
907 |       await writeFile(
908 |         join(testDir, "guide.md"),
909 |         "# Guide\n\n## Section\n\nContent",
910 |       );
911 | 
912 |       const result = await checkDocumentationLinks({
913 |         documentation_path: testDir,
914 |         check_internal_links: true,
915 |         check_external_links: true,
916 |         check_anchor_links: true,
917 |       });
918 | 
919 |       const formatted = formatMCPResponse(result);
920 |       expect(formatted.isError).toBe(false);
921 |       const contentText = formatted.content.map((c) => c.text).join(" ");
922 |       // Should process checkable link types (internal, external, anchor to file)
923 |       // mailto and tel are filtered out
924 |       expect(contentText).toContain('"totalLinks": 3');
925 |       expect(contentText).toContain("./guide.md");
926 |       expect(contentText).toContain("https://github.com");
927 |     });
928 |   });
929 | });
930 | 
```

--------------------------------------------------------------------------------
/src/utils/ast-analyzer.ts:
--------------------------------------------------------------------------------

```typescript
   1 | /**
   2 |  * AST-based Code Analyzer (Phase 3)
   3 |  *
   4 |  * Uses tree-sitter parsers for multi-language AST analysis
   5 |  * Provides deep code structure extraction for drift detection
   6 |  */
   7 | 
   8 | import { parse as parseTypeScript } from "@typescript-eslint/typescript-estree";
   9 | import { promises as fs } from "fs";
  10 | import path from "path";
  11 | import crypto from "crypto";
  12 | 
  13 | // Language configuration
  14 | const LANGUAGE_CONFIGS: Record<
  15 |   string,
  16 |   { parser: string; extensions: string[] }
  17 | > = {
  18 |   typescript: { parser: "tree-sitter-typescript", extensions: [".ts", ".tsx"] },
  19 |   javascript: {
  20 |     parser: "tree-sitter-javascript",
  21 |     extensions: [".js", ".jsx", ".mjs"],
  22 |   },
  23 |   python: { parser: "tree-sitter-python", extensions: [".py"] },
  24 |   rust: { parser: "tree-sitter-rust", extensions: [".rs"] },
  25 |   go: { parser: "tree-sitter-go", extensions: [".go"] },
  26 |   java: { parser: "tree-sitter-java", extensions: [".java"] },
  27 |   ruby: { parser: "tree-sitter-ruby", extensions: [".rb"] },
  28 |   bash: { parser: "tree-sitter-bash", extensions: [".sh", ".bash"] },
  29 | };
  30 | 
  31 | export interface FunctionSignature {
  32 |   name: string;
  33 |   parameters: ParameterInfo[];
  34 |   returnType: string | null;
  35 |   isAsync: boolean;
  36 |   isExported: boolean;
  37 |   isPublic: boolean;
  38 |   docComment: string | null;
  39 |   startLine: number;
  40 |   endLine: number;
  41 |   complexity: number;
  42 |   dependencies: string[];
  43 | }
  44 | 
  45 | export interface ParameterInfo {
  46 |   name: string;
  47 |   type: string | null;
  48 |   optional: boolean;
  49 |   defaultValue: string | null;
  50 | }
  51 | 
  52 | export interface ClassInfo {
  53 |   name: string;
  54 |   isExported: boolean;
  55 |   extends: string | null;
  56 |   implements: string[];
  57 |   methods: FunctionSignature[];
  58 |   properties: PropertyInfo[];
  59 |   docComment: string | null;
  60 |   startLine: number;
  61 |   endLine: number;
  62 | }
  63 | 
  64 | export interface PropertyInfo {
  65 |   name: string;
  66 |   type: string | null;
  67 |   isStatic: boolean;
  68 |   isReadonly: boolean;
  69 |   visibility: "public" | "private" | "protected";
  70 | }
  71 | 
  72 | export interface InterfaceInfo {
  73 |   name: string;
  74 |   isExported: boolean;
  75 |   extends: string[];
  76 |   properties: PropertyInfo[];
  77 |   methods: FunctionSignature[];
  78 |   docComment: string | null;
  79 |   startLine: number;
  80 |   endLine: number;
  81 | }
  82 | 
  83 | export interface TypeInfo {
  84 |   name: string;
  85 |   isExported: boolean;
  86 |   definition: string;
  87 |   docComment: string | null;
  88 |   startLine: number;
  89 |   endLine: number;
  90 | }
  91 | 
  92 | export interface ImportInfo {
  93 |   source: string;
  94 |   imports: Array<{ name: string; alias?: string }>;
  95 |   isDefault: boolean;
  96 |   startLine: number;
  97 | }
  98 | 
  99 | export interface ASTAnalysisResult {
 100 |   filePath: string;
 101 |   language: string;
 102 |   functions: FunctionSignature[];
 103 |   classes: ClassInfo[];
 104 |   interfaces: InterfaceInfo[];
 105 |   types: TypeInfo[];
 106 |   imports: ImportInfo[];
 107 |   exports: string[];
 108 |   contentHash: string;
 109 |   lastModified: string;
 110 |   linesOfCode: number;
 111 |   complexity: number;
 112 | }
 113 | 
 114 | export interface CodeDiff {
 115 |   type: "added" | "removed" | "modified" | "unchanged";
 116 |   category: "function" | "class" | "interface" | "type" | "import" | "export";
 117 |   name: string;
 118 |   details: string;
 119 |   oldSignature?: string;
 120 |   newSignature?: string;
 121 |   impactLevel: "breaking" | "major" | "minor" | "patch";
 122 | }
 123 | 
 124 | /**
 125 |  * Main AST Analyzer class
 126 |  */
 127 | export class ASTAnalyzer {
 128 |   private parsers: Map<string, any> = new Map();
 129 |   private initialized = false;
 130 | 
 131 |   /**
 132 |    * Initialize tree-sitter parsers for all languages
 133 |    */
 134 |   async initialize(): Promise<void> {
 135 |     if (this.initialized) return;
 136 | 
 137 |     // Note: Tree-sitter initialization would happen here in a full implementation
 138 |     // For now, we're primarily using TypeScript/JavaScript parser
 139 |     // console.log(
 140 |     //   "AST Analyzer initialized with language support:",
 141 |     //   Object.keys(LANGUAGE_CONFIGS),
 142 |     // );
 143 |     this.initialized = true;
 144 |   }
 145 | 
 146 |   /**
 147 |    * Analyze a single file and extract AST information
 148 |    */
 149 |   async analyzeFile(filePath: string): Promise<ASTAnalysisResult | null> {
 150 |     if (!this.initialized) {
 151 |       await this.initialize();
 152 |     }
 153 | 
 154 |     const ext = path.extname(filePath);
 155 |     const language = this.detectLanguage(ext);
 156 | 
 157 |     if (!language) {
 158 |       console.warn(`Unsupported file extension: ${ext}`);
 159 |       return null;
 160 |     }
 161 | 
 162 |     const content = await fs.readFile(filePath, "utf-8");
 163 |     const stats = await fs.stat(filePath);
 164 | 
 165 |     // Use TypeScript parser for .ts/.tsx files
 166 |     if (language === "typescript" || language === "javascript") {
 167 |       return this.analyzeTypeScript(
 168 |         filePath,
 169 |         content,
 170 |         stats.mtime.toISOString(),
 171 |       );
 172 |     }
 173 | 
 174 |     // For other languages, use tree-sitter (placeholder)
 175 |     return this.analyzeWithTreeSitter(
 176 |       filePath,
 177 |       content,
 178 |       language,
 179 |       stats.mtime.toISOString(),
 180 |     );
 181 |   }
 182 | 
 183 |   /**
 184 |    * Analyze TypeScript/JavaScript using typescript-estree
 185 |    */
 186 |   private async analyzeTypeScript(
 187 |     filePath: string,
 188 |     content: string,
 189 |     lastModified: string,
 190 |   ): Promise<ASTAnalysisResult> {
 191 |     const functions: FunctionSignature[] = [];
 192 |     const classes: ClassInfo[] = [];
 193 |     const interfaces: InterfaceInfo[] = [];
 194 |     const types: TypeInfo[] = [];
 195 |     const imports: ImportInfo[] = [];
 196 |     const exports: string[] = [];
 197 | 
 198 |     try {
 199 |       const ast = parseTypeScript(content, {
 200 |         loc: true,
 201 |         range: true,
 202 |         tokens: false,
 203 |         comment: true,
 204 |         jsx: filePath.endsWith(".tsx") || filePath.endsWith(".jsx"),
 205 |       });
 206 | 
 207 |       // Extract functions
 208 |       this.extractFunctions(ast, content, functions);
 209 | 
 210 |       // Extract classes
 211 |       this.extractClasses(ast, content, classes);
 212 | 
 213 |       // Extract interfaces
 214 |       this.extractInterfaces(ast, content, interfaces);
 215 | 
 216 |       // Extract type aliases
 217 |       this.extractTypes(ast, content, types);
 218 | 
 219 |       // Extract imports
 220 |       this.extractImports(ast, imports);
 221 | 
 222 |       // Extract exports
 223 |       this.extractExports(ast, exports);
 224 |     } catch (error) {
 225 |       console.warn(`Failed to parse TypeScript file ${filePath}:`, error);
 226 |     }
 227 | 
 228 |     const contentHash = crypto
 229 |       .createHash("sha256")
 230 |       .update(content)
 231 |       .digest("hex");
 232 |     const linesOfCode = content.split("\n").length;
 233 |     const complexity = this.calculateComplexity(functions, classes);
 234 | 
 235 |     return {
 236 |       filePath,
 237 |       language:
 238 |         filePath.endsWith(".ts") || filePath.endsWith(".tsx")
 239 |           ? "typescript"
 240 |           : "javascript",
 241 |       functions,
 242 |       classes,
 243 |       interfaces,
 244 |       types,
 245 |       imports,
 246 |       exports,
 247 |       contentHash,
 248 |       lastModified,
 249 |       linesOfCode,
 250 |       complexity,
 251 |     };
 252 |   }
 253 | 
 254 |   /**
 255 |    * Analyze using tree-sitter (placeholder for other languages)
 256 |    */
 257 |   private async analyzeWithTreeSitter(
 258 |     filePath: string,
 259 |     content: string,
 260 |     language: string,
 261 |     lastModified: string,
 262 |   ): Promise<ASTAnalysisResult> {
 263 |     // Placeholder for tree-sitter analysis
 264 |     // In a full implementation, we'd parse the content using tree-sitter
 265 |     // and extract language-specific constructs
 266 | 
 267 |     const contentHash = crypto
 268 |       .createHash("sha256")
 269 |       .update(content)
 270 |       .digest("hex");
 271 |     const linesOfCode = content.split("\n").length;
 272 | 
 273 |     return {
 274 |       filePath,
 275 |       language,
 276 |       functions: [],
 277 |       classes: [],
 278 |       interfaces: [],
 279 |       types: [],
 280 |       imports: [],
 281 |       exports: [],
 282 |       contentHash,
 283 |       lastModified,
 284 |       linesOfCode,
 285 |       complexity: 0,
 286 |     };
 287 |   }
 288 | 
 289 |   /**
 290 |    * Extract function declarations from AST
 291 |    */
 292 |   private extractFunctions(
 293 |     ast: any,
 294 |     content: string,
 295 |     functions: FunctionSignature[],
 296 |   ): void {
 297 |     const lines = content.split("\n");
 298 | 
 299 |     const traverse = (node: any, isExported = false) => {
 300 |       if (!node) return;
 301 | 
 302 |       // Handle export declarations
 303 |       if (
 304 |         node.type === "ExportNamedDeclaration" ||
 305 |         node.type === "ExportDefaultDeclaration"
 306 |       ) {
 307 |         if (node.declaration) {
 308 |           traverse(node.declaration, true);
 309 |         }
 310 |         return;
 311 |       }
 312 | 
 313 |       // Function declarations
 314 |       if (node.type === "FunctionDeclaration") {
 315 |         const func = this.parseFunctionNode(node, lines, isExported);
 316 |         if (func) functions.push(func);
 317 |       }
 318 | 
 319 |       // Arrow functions assigned to variables
 320 |       if (node.type === "VariableDeclaration") {
 321 |         for (const declarator of node.declarations || []) {
 322 |           if (declarator.init?.type === "ArrowFunctionExpression") {
 323 |             const func = this.parseArrowFunction(declarator, lines, isExported);
 324 |             if (func) functions.push(func);
 325 |           }
 326 |         }
 327 |       }
 328 | 
 329 |       // Traverse children
 330 |       for (const key in node) {
 331 |         if (typeof node[key] === "object" && node[key] !== null) {
 332 |           if (Array.isArray(node[key])) {
 333 |             node[key].forEach((child: any) => traverse(child, false));
 334 |           } else {
 335 |             traverse(node[key], false);
 336 |           }
 337 |         }
 338 |       }
 339 |     };
 340 | 
 341 |     traverse(ast);
 342 |   }
 343 | 
 344 |   /**
 345 |    * Parse function node
 346 |    */
 347 |   private parseFunctionNode(
 348 |     node: any,
 349 |     lines: string[],
 350 |     isExported: boolean,
 351 |   ): FunctionSignature | null {
 352 |     if (!node.id?.name) return null;
 353 | 
 354 |     const docComment = this.extractDocComment(node.loc?.start.line - 1, lines);
 355 |     const parameters = this.extractParameters(node.params);
 356 | 
 357 |     return {
 358 |       name: node.id.name,
 359 |       parameters,
 360 |       returnType: this.extractReturnType(node),
 361 |       isAsync: node.async || false,
 362 |       isExported,
 363 |       isPublic: true,
 364 |       docComment,
 365 |       startLine: node.loc?.start.line || 0,
 366 |       endLine: node.loc?.end.line || 0,
 367 |       complexity: this.calculateFunctionComplexity(node),
 368 |       dependencies: [],
 369 |     };
 370 |   }
 371 | 
 372 |   /**
 373 |    * Parse arrow function
 374 |    */
 375 |   private parseArrowFunction(
 376 |     declarator: any,
 377 |     lines: string[],
 378 |     isExported: boolean,
 379 |   ): FunctionSignature | null {
 380 |     if (!declarator.id?.name) return null;
 381 | 
 382 |     const node = declarator.init;
 383 |     const docComment = this.extractDocComment(
 384 |       declarator.loc?.start.line - 1,
 385 |       lines,
 386 |     );
 387 |     const parameters = this.extractParameters(node.params);
 388 | 
 389 |     return {
 390 |       name: declarator.id.name,
 391 |       parameters,
 392 |       returnType: this.extractReturnType(node),
 393 |       isAsync: node.async || false,
 394 |       isExported,
 395 |       isPublic: true,
 396 |       docComment,
 397 |       startLine: declarator.loc?.start.line || 0,
 398 |       endLine: declarator.loc?.end.line || 0,
 399 |       complexity: this.calculateFunctionComplexity(node),
 400 |       dependencies: [],
 401 |     };
 402 |   }
 403 | 
 404 |   /**
 405 |    * Extract classes from AST
 406 |    */
 407 |   private extractClasses(
 408 |     ast: any,
 409 |     content: string,
 410 |     classes: ClassInfo[],
 411 |   ): void {
 412 |     const lines = content.split("\n");
 413 | 
 414 |     const traverse = (node: any, isExported = false) => {
 415 |       if (!node) return;
 416 | 
 417 |       // Handle export declarations
 418 |       if (
 419 |         node.type === "ExportNamedDeclaration" ||
 420 |         node.type === "ExportDefaultDeclaration"
 421 |       ) {
 422 |         if (node.declaration) {
 423 |           traverse(node.declaration, true);
 424 |         }
 425 |         return;
 426 |       }
 427 | 
 428 |       if (node.type === "ClassDeclaration" && node.id?.name) {
 429 |         const classInfo = this.parseClassNode(node, lines, isExported);
 430 |         if (classInfo) classes.push(classInfo);
 431 |       }
 432 | 
 433 |       for (const key in node) {
 434 |         if (typeof node[key] === "object" && node[key] !== null) {
 435 |           if (Array.isArray(node[key])) {
 436 |             node[key].forEach((child: any) => traverse(child, false));
 437 |           } else {
 438 |             traverse(node[key], false);
 439 |           }
 440 |         }
 441 |       }
 442 |     };
 443 | 
 444 |     traverse(ast);
 445 |   }
 446 | 
 447 |   /**
 448 |    * Parse class node
 449 |    */
 450 |   private parseClassNode(
 451 |     node: any,
 452 |     lines: string[],
 453 |     isExported: boolean,
 454 |   ): ClassInfo | null {
 455 |     const methods: FunctionSignature[] = [];
 456 |     const properties: PropertyInfo[] = [];
 457 | 
 458 |     // Extract methods and properties
 459 |     if (node.body?.body) {
 460 |       for (const member of node.body.body) {
 461 |         if (member.type === "MethodDefinition") {
 462 |           const method = this.parseMethodNode(member, lines);
 463 |           if (method) methods.push(method);
 464 |         } else if (member.type === "PropertyDefinition") {
 465 |           const property = this.parsePropertyNode(member);
 466 |           if (property) properties.push(property);
 467 |         }
 468 |       }
 469 |     }
 470 | 
 471 |     return {
 472 |       name: node.id.name,
 473 |       isExported,
 474 |       extends: node.superClass?.name || null,
 475 |       implements:
 476 |         node.implements?.map((i: any) => i.expression?.name || "unknown") || [],
 477 |       methods,
 478 |       properties,
 479 |       docComment: this.extractDocComment(node.loc?.start.line - 1, lines),
 480 |       startLine: node.loc?.start.line || 0,
 481 |       endLine: node.loc?.end.line || 0,
 482 |     };
 483 |   }
 484 | 
 485 |   /**
 486 |    * Parse method node
 487 |    */
 488 |   private parseMethodNode(
 489 |     node: any,
 490 |     lines: string[],
 491 |   ): FunctionSignature | null {
 492 |     if (!node.key?.name) return null;
 493 | 
 494 |     return {
 495 |       name: node.key.name,
 496 |       parameters: this.extractParameters(node.value?.params || []),
 497 |       returnType: this.extractReturnType(node.value),
 498 |       isAsync: node.value?.async || false,
 499 |       isExported: false,
 500 |       isPublic: !node.key.name.startsWith("_"),
 501 |       docComment: this.extractDocComment(node.loc?.start.line - 1, lines),
 502 |       startLine: node.loc?.start.line || 0,
 503 |       endLine: node.loc?.end.line || 0,
 504 |       complexity: this.calculateFunctionComplexity(node.value),
 505 |       dependencies: [],
 506 |     };
 507 |   }
 508 | 
 509 |   /**
 510 |    * Parse property node
 511 |    */
 512 |   private parsePropertyNode(node: any): PropertyInfo | null {
 513 |     if (!node.key?.name) return null;
 514 | 
 515 |     return {
 516 |       name: node.key.name,
 517 |       type: this.extractTypeAnnotation(node.typeAnnotation),
 518 |       isStatic: node.static || false,
 519 |       isReadonly: node.readonly || false,
 520 |       visibility: this.determineVisibility(node),
 521 |     };
 522 |   }
 523 | 
 524 |   /**
 525 |    * Extract interfaces from AST
 526 |    */
 527 |   private extractInterfaces(
 528 |     ast: any,
 529 |     content: string,
 530 |     interfaces: InterfaceInfo[],
 531 |   ): void {
 532 |     const lines = content.split("\n");
 533 | 
 534 |     const traverse = (node: any, isExported = false) => {
 535 |       if (!node) return;
 536 | 
 537 |       // Handle export declarations
 538 |       if (
 539 |         node.type === "ExportNamedDeclaration" ||
 540 |         node.type === "ExportDefaultDeclaration"
 541 |       ) {
 542 |         if (node.declaration) {
 543 |           traverse(node.declaration, true);
 544 |         }
 545 |         return;
 546 |       }
 547 | 
 548 |       if (node.type === "TSInterfaceDeclaration" && node.id?.name) {
 549 |         const interfaceInfo = this.parseInterfaceNode(node, lines, isExported);
 550 |         if (interfaceInfo) interfaces.push(interfaceInfo);
 551 |       }
 552 | 
 553 |       for (const key in node) {
 554 |         if (typeof node[key] === "object" && node[key] !== null) {
 555 |           if (Array.isArray(node[key])) {
 556 |             node[key].forEach((child: any) => traverse(child, false));
 557 |           } else {
 558 |             traverse(node[key], false);
 559 |           }
 560 |         }
 561 |       }
 562 |     };
 563 | 
 564 |     traverse(ast);
 565 |   }
 566 | 
 567 |   /**
 568 |    * Parse interface node
 569 |    */
 570 |   private parseInterfaceNode(
 571 |     node: any,
 572 |     lines: string[],
 573 |     isExported: boolean,
 574 |   ): InterfaceInfo | null {
 575 |     const properties: PropertyInfo[] = [];
 576 |     const methods: FunctionSignature[] = [];
 577 | 
 578 |     if (node.body?.body) {
 579 |       for (const member of node.body.body) {
 580 |         if (member.type === "TSPropertySignature") {
 581 |           const prop = this.parseInterfaceProperty(member);
 582 |           if (prop) properties.push(prop);
 583 |         } else if (member.type === "TSMethodSignature") {
 584 |           const method = this.parseInterfaceMethod(member);
 585 |           if (method) methods.push(method);
 586 |         }
 587 |       }
 588 |     }
 589 | 
 590 |     return {
 591 |       name: node.id.name,
 592 |       isExported,
 593 |       extends:
 594 |         node.extends?.map((e: any) => e.expression?.name || "unknown") || [],
 595 |       properties,
 596 |       methods,
 597 |       docComment: this.extractDocComment(node.loc?.start.line - 1, lines),
 598 |       startLine: node.loc?.start.line || 0,
 599 |       endLine: node.loc?.end.line || 0,
 600 |     };
 601 |   }
 602 | 
 603 |   /**
 604 |    * Parse interface property
 605 |    */
 606 |   private parseInterfaceProperty(node: any): PropertyInfo | null {
 607 |     if (!node.key?.name) return null;
 608 | 
 609 |     return {
 610 |       name: node.key.name,
 611 |       type: this.extractTypeAnnotation(node.typeAnnotation),
 612 |       isStatic: false,
 613 |       isReadonly: node.readonly || false,
 614 |       visibility: "public",
 615 |     };
 616 |   }
 617 | 
 618 |   /**
 619 |    * Parse interface method
 620 |    */
 621 |   private parseInterfaceMethod(node: any): FunctionSignature | null {
 622 |     if (!node.key?.name) return null;
 623 | 
 624 |     return {
 625 |       name: node.key.name,
 626 |       parameters: this.extractParameters(node.params || []),
 627 |       returnType: this.extractTypeAnnotation(node.returnType),
 628 |       isAsync: false,
 629 |       isExported: false,
 630 |       isPublic: true,
 631 |       docComment: null,
 632 |       startLine: node.loc?.start.line || 0,
 633 |       endLine: node.loc?.end.line || 0,
 634 |       complexity: 0,
 635 |       dependencies: [],
 636 |     };
 637 |   }
 638 | 
 639 |   /**
 640 |    * Extract type aliases from AST
 641 |    */
 642 |   private extractTypes(ast: any, content: string, types: TypeInfo[]): void {
 643 |     const lines = content.split("\n");
 644 | 
 645 |     const traverse = (node: any, isExported = false) => {
 646 |       if (!node) return;
 647 | 
 648 |       // Handle export declarations
 649 |       if (
 650 |         node.type === "ExportNamedDeclaration" ||
 651 |         node.type === "ExportDefaultDeclaration"
 652 |       ) {
 653 |         if (node.declaration) {
 654 |           traverse(node.declaration, true);
 655 |         }
 656 |         return;
 657 |       }
 658 | 
 659 |       if (node.type === "TSTypeAliasDeclaration" && node.id?.name) {
 660 |         const typeInfo = this.parseTypeNode(node, lines, isExported);
 661 |         if (typeInfo) types.push(typeInfo);
 662 |       }
 663 | 
 664 |       for (const key in node) {
 665 |         if (typeof node[key] === "object" && node[key] !== null) {
 666 |           if (Array.isArray(node[key])) {
 667 |             node[key].forEach((child: any) => traverse(child, false));
 668 |           } else {
 669 |             traverse(node[key], false);
 670 |           }
 671 |         }
 672 |       }
 673 |     };
 674 | 
 675 |     traverse(ast);
 676 |   }
 677 | 
 678 |   /**
 679 |    * Parse type alias node
 680 |    */
 681 |   private parseTypeNode(
 682 |     node: any,
 683 |     lines: string[],
 684 |     isExported: boolean,
 685 |   ): TypeInfo | null {
 686 |     return {
 687 |       name: node.id.name,
 688 |       isExported,
 689 |       definition: this.extractTypeDefinition(node.typeAnnotation),
 690 |       docComment: this.extractDocComment(node.loc?.start.line - 1, lines),
 691 |       startLine: node.loc?.start.line || 0,
 692 |       endLine: node.loc?.end.line || 0,
 693 |     };
 694 |   }
 695 | 
 696 |   /**
 697 |    * Extract imports from AST
 698 |    */
 699 |   private extractImports(ast: any, imports: ImportInfo[]): void {
 700 |     const traverse = (node: any) => {
 701 |       if (!node) return;
 702 | 
 703 |       if (node.type === "ImportDeclaration") {
 704 |         const importInfo: ImportInfo = {
 705 |           source: node.source?.value || "",
 706 |           imports: [],
 707 |           isDefault: false,
 708 |           startLine: node.loc?.start.line || 0,
 709 |         };
 710 | 
 711 |         for (const specifier of node.specifiers || []) {
 712 |           if (specifier.type === "ImportDefaultSpecifier") {
 713 |             importInfo.isDefault = true;
 714 |             importInfo.imports.push({
 715 |               name: specifier.local?.name || "default",
 716 |             });
 717 |           } else if (specifier.type === "ImportSpecifier") {
 718 |             importInfo.imports.push({
 719 |               name: specifier.imported?.name || "",
 720 |               alias:
 721 |                 specifier.local?.name !== specifier.imported?.name
 722 |                   ? specifier.local?.name
 723 |                   : undefined,
 724 |             });
 725 |           }
 726 |         }
 727 | 
 728 |         imports.push(importInfo);
 729 |       }
 730 | 
 731 |       for (const key in node) {
 732 |         if (typeof node[key] === "object" && node[key] !== null) {
 733 |           if (Array.isArray(node[key])) {
 734 |             node[key].forEach((child: any) => traverse(child));
 735 |           } else {
 736 |             traverse(node[key]);
 737 |           }
 738 |         }
 739 |       }
 740 |     };
 741 | 
 742 |     traverse(ast);
 743 |   }
 744 | 
 745 |   /**
 746 |    * Extract exports from AST
 747 |    */
 748 |   private extractExports(ast: any, exports: string[]): void {
 749 |     const traverse = (node: any) => {
 750 |       if (!node) return;
 751 | 
 752 |       // Named exports
 753 |       if (node.type === "ExportNamedDeclaration") {
 754 |         if (node.declaration) {
 755 |           if (node.declaration.id?.name) {
 756 |             exports.push(node.declaration.id.name);
 757 |           } else if (node.declaration.declarations) {
 758 |             for (const decl of node.declaration.declarations) {
 759 |               if (decl.id?.name) exports.push(decl.id.name);
 760 |             }
 761 |           }
 762 |         }
 763 |         for (const specifier of node.specifiers || []) {
 764 |           if (specifier.exported?.name) exports.push(specifier.exported.name);
 765 |         }
 766 |       }
 767 | 
 768 |       // Default export
 769 |       if (node.type === "ExportDefaultDeclaration") {
 770 |         if (node.declaration?.id?.name) {
 771 |           exports.push(node.declaration.id.name);
 772 |         } else {
 773 |           exports.push("default");
 774 |         }
 775 |       }
 776 | 
 777 |       for (const key in node) {
 778 |         if (typeof node[key] === "object" && node[key] !== null) {
 779 |           if (Array.isArray(node[key])) {
 780 |             node[key].forEach((child: any) => traverse(child));
 781 |           } else {
 782 |             traverse(node[key]);
 783 |           }
 784 |         }
 785 |       }
 786 |     };
 787 | 
 788 |     traverse(ast);
 789 |   }
 790 | 
 791 |   // Helper methods
 792 | 
 793 |   private extractParameters(params: any[]): ParameterInfo[] {
 794 |     return params.map((param) => ({
 795 |       name: param.name || param.argument?.name || param.left?.name || "unknown",
 796 |       type: this.extractTypeAnnotation(param.typeAnnotation),
 797 |       optional: param.optional || false,
 798 |       defaultValue: param.right ? this.extractDefaultValue(param.right) : null,
 799 |     }));
 800 |   }
 801 | 
 802 |   private extractReturnType(node: any): string | null {
 803 |     return this.extractTypeAnnotation(node?.returnType);
 804 |   }
 805 | 
 806 |   private extractTypeAnnotation(typeAnnotation: any): string | null {
 807 |     if (!typeAnnotation) return null;
 808 |     if (typeAnnotation.typeAnnotation)
 809 |       return this.extractTypeDefinition(typeAnnotation.typeAnnotation);
 810 |     return this.extractTypeDefinition(typeAnnotation);
 811 |   }
 812 | 
 813 |   private extractTypeDefinition(typeNode: any): string {
 814 |     if (!typeNode) return "unknown";
 815 |     if (typeNode.type === "TSStringKeyword") return "string";
 816 |     if (typeNode.type === "TSNumberKeyword") return "number";
 817 |     if (typeNode.type === "TSBooleanKeyword") return "boolean";
 818 |     if (typeNode.type === "TSAnyKeyword") return "any";
 819 |     if (typeNode.type === "TSVoidKeyword") return "void";
 820 |     if (typeNode.type === "TSTypeReference")
 821 |       return typeNode.typeName?.name || "unknown";
 822 |     return "unknown";
 823 |   }
 824 | 
 825 |   private extractDefaultValue(node: any): string | null {
 826 |     if (node.type === "Literal") return String(node.value);
 827 |     if (node.type === "Identifier") return node.name;
 828 |     return null;
 829 |   }
 830 | 
 831 |   private extractDocComment(
 832 |     lineNumber: number,
 833 |     lines: string[],
 834 |   ): string | null {
 835 |     if (lineNumber < 0 || lineNumber >= lines.length) return null;
 836 | 
 837 |     const comment: string[] = [];
 838 |     let currentLine = lineNumber;
 839 | 
 840 |     // Look backwards for JSDoc comment
 841 |     while (currentLine >= 0) {
 842 |       const line = lines[currentLine].trim();
 843 |       if (line.startsWith("*/")) {
 844 |         comment.unshift(line);
 845 |         currentLine--;
 846 |         continue;
 847 |       }
 848 |       if (line.startsWith("*") || line.startsWith("/**")) {
 849 |         comment.unshift(line);
 850 |         if (line.startsWith("/**")) break;
 851 |         currentLine--;
 852 |         continue;
 853 |       }
 854 |       if (comment.length > 0) break;
 855 |       currentLine--;
 856 |     }
 857 | 
 858 |     return comment.length > 0 ? comment.join("\n") : null;
 859 |   }
 860 | 
 861 |   private isExported(node: any): boolean {
 862 |     if (!node) return false;
 863 | 
 864 |     // Check parent for export
 865 |     let current = node;
 866 |     while (current) {
 867 |       if (
 868 |         current.type === "ExportNamedDeclaration" ||
 869 |         current.type === "ExportDefaultDeclaration"
 870 |       ) {
 871 |         return true;
 872 |       }
 873 |       current = current.parent;
 874 |     }
 875 | 
 876 |     return false;
 877 |   }
 878 | 
 879 |   private determineVisibility(node: any): "public" | "private" | "protected" {
 880 |     if (node.accessibility) return node.accessibility;
 881 |     if (node.key?.name?.startsWith("_")) return "private";
 882 |     if (node.key?.name?.startsWith("#")) return "private";
 883 |     return "public";
 884 |   }
 885 | 
 886 |   private calculateFunctionComplexity(node: any): number {
 887 |     // Simplified cyclomatic complexity
 888 |     let complexity = 1;
 889 | 
 890 |     const traverse = (n: any) => {
 891 |       if (!n) return;
 892 | 
 893 |       // Increment for control flow statements
 894 |       if (
 895 |         [
 896 |           "IfStatement",
 897 |           "ConditionalExpression",
 898 |           "ForStatement",
 899 |           "WhileStatement",
 900 |           "DoWhileStatement",
 901 |           "SwitchCase",
 902 |           "CatchClause",
 903 |         ].includes(n.type)
 904 |       ) {
 905 |         complexity++;
 906 |       }
 907 | 
 908 |       for (const key in n) {
 909 |         if (typeof n[key] === "object" && n[key] !== null) {
 910 |           if (Array.isArray(n[key])) {
 911 |             n[key].forEach((child: any) => traverse(child));
 912 |           } else {
 913 |             traverse(n[key]);
 914 |           }
 915 |         }
 916 |       }
 917 |     };
 918 | 
 919 |     traverse(node);
 920 |     return complexity;
 921 |   }
 922 | 
 923 |   private calculateComplexity(
 924 |     functions: FunctionSignature[],
 925 |     classes: ClassInfo[],
 926 |   ): number {
 927 |     const functionComplexity = functions.reduce(
 928 |       (sum, f) => sum + f.complexity,
 929 |       0,
 930 |     );
 931 |     const classComplexity = classes.reduce(
 932 |       (sum, c) =>
 933 |         sum + c.methods.reduce((methodSum, m) => methodSum + m.complexity, 0),
 934 |       0,
 935 |     );
 936 |     return functionComplexity + classComplexity;
 937 |   }
 938 | 
 939 |   private detectLanguage(ext: string): string | null {
 940 |     for (const [lang, config] of Object.entries(LANGUAGE_CONFIGS)) {
 941 |       if (config.extensions.includes(ext)) return lang;
 942 |     }
 943 |     return null;
 944 |   }
 945 | 
 946 |   /**
 947 |    * Compare two AST analysis results and detect changes
 948 |    */
 949 |   async detectDrift(
 950 |     oldAnalysis: ASTAnalysisResult,
 951 |     newAnalysis: ASTAnalysisResult,
 952 |   ): Promise<CodeDiff[]> {
 953 |     const diffs: CodeDiff[] = [];
 954 | 
 955 |     // Compare functions
 956 |     diffs.push(
 957 |       ...this.compareFunctions(oldAnalysis.functions, newAnalysis.functions),
 958 |     );
 959 | 
 960 |     // Compare classes
 961 |     diffs.push(
 962 |       ...this.compareClasses(oldAnalysis.classes, newAnalysis.classes),
 963 |     );
 964 | 
 965 |     // Compare interfaces
 966 |     diffs.push(
 967 |       ...this.compareInterfaces(oldAnalysis.interfaces, newAnalysis.interfaces),
 968 |     );
 969 | 
 970 |     // Compare types
 971 |     diffs.push(...this.compareTypes(oldAnalysis.types, newAnalysis.types));
 972 | 
 973 |     return diffs;
 974 |   }
 975 | 
 976 |   private compareFunctions(
 977 |     oldFuncs: FunctionSignature[],
 978 |     newFuncs: FunctionSignature[],
 979 |   ): CodeDiff[] {
 980 |     const diffs: CodeDiff[] = [];
 981 |     const oldMap = new Map(oldFuncs.map((f) => [f.name, f]));
 982 |     const newMap = new Map(newFuncs.map((f) => [f.name, f]));
 983 | 
 984 |     // Check for removed functions
 985 |     for (const [name, func] of oldMap) {
 986 |       if (!newMap.has(name)) {
 987 |         diffs.push({
 988 |           type: "removed",
 989 |           category: "function",
 990 |           name,
 991 |           details: `Function '${name}' was removed`,
 992 |           oldSignature: this.formatFunctionSignature(func),
 993 |           impactLevel: func.isExported ? "breaking" : "minor",
 994 |         });
 995 |       }
 996 |     }
 997 | 
 998 |     // Check for added functions
 999 |     for (const [name, func] of newMap) {
1000 |       if (!oldMap.has(name)) {
1001 |         diffs.push({
1002 |           type: "added",
1003 |           category: "function",
1004 |           name,
1005 |           details: `Function '${name}' was added`,
1006 |           newSignature: this.formatFunctionSignature(func),
1007 |           impactLevel: "patch",
1008 |         });
1009 |       }
1010 |     }
1011 | 
1012 |     // Check for modified functions
1013 |     for (const [name, newFunc] of newMap) {
1014 |       const oldFunc = oldMap.get(name);
1015 |       if (oldFunc) {
1016 |         const changes = this.detectFunctionChanges(oldFunc, newFunc);
1017 |         if (changes.length > 0) {
1018 |           diffs.push({
1019 |             type: "modified",
1020 |             category: "function",
1021 |             name,
1022 |             details: changes.join("; "),
1023 |             oldSignature: this.formatFunctionSignature(oldFunc),
1024 |             newSignature: this.formatFunctionSignature(newFunc),
1025 |             impactLevel: this.determineFunctionImpact(oldFunc, newFunc),
1026 |           });
1027 |         }
1028 |       }
1029 |     }
1030 | 
1031 |     return diffs;
1032 |   }
1033 | 
1034 |   private compareClasses(
1035 |     oldClasses: ClassInfo[],
1036 |     newClasses: ClassInfo[],
1037 |   ): CodeDiff[] {
1038 |     const diffs: CodeDiff[] = [];
1039 |     const oldMap = new Map(oldClasses.map((c) => [c.name, c]));
1040 |     const newMap = new Map(newClasses.map((c) => [c.name, c]));
1041 | 
1042 |     for (const [name, oldClass] of oldMap) {
1043 |       if (!newMap.has(name)) {
1044 |         diffs.push({
1045 |           type: "removed",
1046 |           category: "class",
1047 |           name,
1048 |           details: `Class '${name}' was removed`,
1049 |           impactLevel: oldClass.isExported ? "breaking" : "minor",
1050 |         });
1051 |       }
1052 |     }
1053 | 
1054 |     for (const [name] of newMap) {
1055 |       if (!oldMap.has(name)) {
1056 |         diffs.push({
1057 |           type: "added",
1058 |           category: "class",
1059 |           name,
1060 |           details: `Class '${name}' was added`,
1061 |           impactLevel: "patch",
1062 |         });
1063 |       }
1064 |     }
1065 | 
1066 |     return diffs;
1067 |   }
1068 | 
1069 |   private compareInterfaces(
1070 |     oldInterfaces: InterfaceInfo[],
1071 |     newInterfaces: InterfaceInfo[],
1072 |   ): CodeDiff[] {
1073 |     const diffs: CodeDiff[] = [];
1074 |     const oldMap = new Map(oldInterfaces.map((i) => [i.name, i]));
1075 |     const newMap = new Map(newInterfaces.map((i) => [i.name, i]));
1076 | 
1077 |     for (const [name, oldInterface] of oldMap) {
1078 |       if (!newMap.has(name)) {
1079 |         diffs.push({
1080 |           type: "removed",
1081 |           category: "interface",
1082 |           name,
1083 |           details: `Interface '${name}' was removed`,
1084 |           impactLevel: oldInterface.isExported ? "breaking" : "minor",
1085 |         });
1086 |       }
1087 |     }
1088 | 
1089 |     for (const [name] of newMap) {
1090 |       if (!oldMap.has(name)) {
1091 |         diffs.push({
1092 |           type: "added",
1093 |           category: "interface",
1094 |           name,
1095 |           details: `Interface '${name}' was added`,
1096 |           impactLevel: "patch",
1097 |         });
1098 |       }
1099 |     }
1100 | 
1101 |     return diffs;
1102 |   }
1103 | 
1104 |   private compareTypes(oldTypes: TypeInfo[], newTypes: TypeInfo[]): CodeDiff[] {
1105 |     const diffs: CodeDiff[] = [];
1106 |     const oldMap = new Map(oldTypes.map((t) => [t.name, t]));
1107 |     const newMap = new Map(newTypes.map((t) => [t.name, t]));
1108 | 
1109 |     for (const [name, oldType] of oldMap) {
1110 |       if (!newMap.has(name)) {
1111 |         diffs.push({
1112 |           type: "removed",
1113 |           category: "type",
1114 |           name,
1115 |           details: `Type '${name}' was removed`,
1116 |           impactLevel: oldType.isExported ? "breaking" : "minor",
1117 |         });
1118 |       }
1119 |     }
1120 | 
1121 |     for (const [name] of newMap) {
1122 |       if (!oldMap.has(name)) {
1123 |         diffs.push({
1124 |           type: "added",
1125 |           category: "type",
1126 |           name,
1127 |           details: `Type '${name}' was added`,
1128 |           impactLevel: "patch",
1129 |         });
1130 |       }
1131 |     }
1132 | 
1133 |     return diffs;
1134 |   }
1135 | 
1136 |   private detectFunctionChanges(
1137 |     oldFunc: FunctionSignature,
1138 |     newFunc: FunctionSignature,
1139 |   ): string[] {
1140 |     const changes: string[] = [];
1141 | 
1142 |     // Check parameter changes
1143 |     if (oldFunc.parameters.length !== newFunc.parameters.length) {
1144 |       changes.push(
1145 |         `Parameter count changed from ${oldFunc.parameters.length} to ${newFunc.parameters.length}`,
1146 |       );
1147 |     }
1148 | 
1149 |     // Check return type changes
1150 |     if (oldFunc.returnType !== newFunc.returnType) {
1151 |       changes.push(
1152 |         `Return type changed from '${oldFunc.returnType}' to '${newFunc.returnType}'`,
1153 |       );
1154 |     }
1155 | 
1156 |     // Check async changes
1157 |     if (oldFunc.isAsync !== newFunc.isAsync) {
1158 |       changes.push(
1159 |         newFunc.isAsync
1160 |           ? "Function became async"
1161 |           : "Function is no longer async",
1162 |       );
1163 |     }
1164 | 
1165 |     // Check export changes
1166 |     if (oldFunc.isExported !== newFunc.isExported) {
1167 |       changes.push(
1168 |         newFunc.isExported
1169 |           ? "Function is now exported"
1170 |           : "Function is no longer exported",
1171 |       );
1172 |     }
1173 | 
1174 |     return changes;
1175 |   }
1176 | 
1177 |   private determineFunctionImpact(
1178 |     oldFunc: FunctionSignature,
1179 |     newFunc: FunctionSignature,
1180 |   ): "breaking" | "major" | "minor" | "patch" {
1181 |     // Breaking changes
1182 |     if (oldFunc.isExported) {
1183 |       if (oldFunc.parameters.length !== newFunc.parameters.length)
1184 |         return "breaking";
1185 |       if (oldFunc.returnType !== newFunc.returnType) return "breaking";
1186 |       // If a function was exported and is no longer exported, that's breaking
1187 |       if (oldFunc.isExported && !newFunc.isExported) return "breaking";
1188 |     }
1189 | 
1190 |     // Major changes
1191 |     if (oldFunc.isAsync !== newFunc.isAsync) return "major";
1192 | 
1193 |     // Minor changes (new API surface)
1194 |     // If a function becomes exported, that's a minor change (new feature/API)
1195 |     if (!oldFunc.isExported && newFunc.isExported) return "minor";
1196 | 
1197 |     return "patch";
1198 |   }
1199 | 
1200 |   private formatFunctionSignature(func: FunctionSignature): string {
1201 |     const params = func.parameters
1202 |       .map((p) => `${p.name}: ${p.type || "any"}`)
1203 |       .join(", ");
1204 |     const returnType = func.returnType || "void";
1205 |     const asyncPrefix = func.isAsync ? "async " : "";
1206 |     return `${asyncPrefix}${func.name}(${params}): ${returnType}`;
1207 |   }
1208 | }
1209 | 
```

--------------------------------------------------------------------------------
/src/memory/knowledge-graph.ts:
--------------------------------------------------------------------------------

```typescript
   1 | /**
   2 |  * Knowledge Graph Architecture for DocuMCP
   3 |  * Implements Phase 1.1: Enhanced Knowledge Graph Schema Implementation
   4 |  * Previously: Issue #48: Knowledge Graph Architecture
   5 |  *
   6 |  * Creates entity relationship graphs for projects, technologies, patterns, and dependencies
   7 |  * to enable advanced reasoning and recommendation improvements.
   8 |  *
   9 |  * Enhanced with comprehensive entity types and relationship schemas following NEW_PRD.md
  10 |  */
  11 | 
  12 | import { MemoryManager } from "./manager.js";
  13 | import { MemoryEntry } from "./storage.js";
  14 | import {
  15 |   validateEntity,
  16 |   validateRelationship,
  17 |   SCHEMA_METADATA,
  18 | } from "./schemas.js";
  19 | 
  20 | export interface GraphNode {
  21 |   id: string;
  22 |   type:
  23 |     | "project"
  24 |     | "technology"
  25 |     | "pattern"
  26 |     | "user"
  27 |     | "outcome"
  28 |     | "recommendation"
  29 |     | "configuration"
  30 |     | "documentation"
  31 |     | "code_file"
  32 |     | "documentation_section"
  33 |     | "link_validation"
  34 |     | "sync_event"
  35 |     | "documentation_freshness_event";
  36 |   label: string;
  37 |   properties: Record<string, any>;
  38 |   weight: number;
  39 |   lastUpdated: string;
  40 | }
  41 | 
  42 | export interface GraphEdge {
  43 |   id: string;
  44 |   source: string;
  45 |   target: string;
  46 |   type:
  47 |     | "uses"
  48 |     | "similar_to"
  49 |     | "depends_on"
  50 |     | "recommends"
  51 |     | "results_in"
  52 |     | "created_by"
  53 |     | "project_uses_technology"
  54 |     | "user_prefers_ssg"
  55 |     | "project_deployed_with"
  56 |     | "documents"
  57 |     | "references"
  58 |     | "outdated_for"
  59 |     | "has_link_validation"
  60 |     | "requires_fix"
  61 |     | "project_has_freshness_event"
  62 |     | (string & NonNullable<unknown>); // Allow any string (for timestamped types like "project_deployed_with:2024-...")
  63 |   weight: number;
  64 |   properties: Record<string, any>;
  65 |   confidence: number;
  66 |   lastUpdated: string;
  67 | }
  68 | 
  69 | export interface GraphPath {
  70 |   nodes: GraphNode[];
  71 |   edges: GraphEdge[];
  72 |   totalWeight: number;
  73 |   confidence: number;
  74 | }
  75 | 
  76 | export interface GraphQuery {
  77 |   nodeTypes?: string[];
  78 |   edgeTypes?: string[];
  79 |   properties?: Record<string, any>;
  80 |   minWeight?: number;
  81 |   maxDepth?: number;
  82 |   startNode?: string;
  83 | }
  84 | 
  85 | export interface RecommendationPath {
  86 |   from: GraphNode;
  87 |   to: GraphNode;
  88 |   path: GraphPath;
  89 |   reasoning: string[];
  90 |   confidence: number;
  91 | }
  92 | 
  93 | export class KnowledgeGraph {
  94 |   private nodes: Map<string, GraphNode>;
  95 |   private edges: Map<string, GraphEdge>;
  96 |   private adjacencyList: Map<string, Set<string>>;
  97 |   private memoryManager: MemoryManager;
  98 |   private lastUpdate: string;
  99 | 
 100 |   constructor(memoryManager: MemoryManager) {
 101 |     this.nodes = new Map();
 102 |     this.edges = new Map();
 103 |     this.adjacencyList = new Map();
 104 |     this.memoryManager = memoryManager;
 105 |     this.lastUpdate = new Date().toISOString();
 106 |   }
 107 | 
 108 |   async initialize(): Promise<void> {
 109 |     await this.loadFromMemory();
 110 |     await this.buildFromMemories();
 111 |   }
 112 | 
 113 |   /**
 114 |    * Add or update a node in the knowledge graph
 115 |    */
 116 |   addNode(node: Omit<GraphNode, "lastUpdated">): GraphNode {
 117 |     const fullNode: GraphNode = {
 118 |       ...node,
 119 |       lastUpdated: new Date().toISOString(),
 120 |     };
 121 | 
 122 |     this.nodes.set(node.id, fullNode);
 123 | 
 124 |     if (!this.adjacencyList.has(node.id)) {
 125 |       this.adjacencyList.set(node.id, new Set());
 126 |     }
 127 | 
 128 |     return fullNode;
 129 |   }
 130 | 
 131 |   /**
 132 |    * Add or update an edge in the knowledge graph
 133 |    */
 134 |   addEdge(edge: Omit<GraphEdge, "id" | "lastUpdated">): GraphEdge {
 135 |     const edgeId = `${edge.source}-${edge.type}-${edge.target}`;
 136 |     const fullEdge: GraphEdge = {
 137 |       ...edge,
 138 |       id: edgeId,
 139 |       lastUpdated: new Date().toISOString(),
 140 |     };
 141 | 
 142 |     this.edges.set(edgeId, fullEdge);
 143 | 
 144 |     // Update adjacency list
 145 |     if (!this.adjacencyList.has(edge.source)) {
 146 |       this.adjacencyList.set(edge.source, new Set());
 147 |     }
 148 |     if (!this.adjacencyList.has(edge.target)) {
 149 |       this.adjacencyList.set(edge.target, new Set());
 150 |     }
 151 | 
 152 |     this.adjacencyList.get(edge.source)!.add(edge.target);
 153 | 
 154 |     return fullEdge;
 155 |   }
 156 | 
 157 |   /**
 158 |    * Build knowledge graph from memory entries
 159 |    */
 160 |   async buildFromMemories(): Promise<void> {
 161 |     const memories = await this.memoryManager.search("", {
 162 |       sortBy: "timestamp",
 163 |     });
 164 | 
 165 |     for (const memory of memories) {
 166 |       await this.processMemoryEntry(memory);
 167 |     }
 168 | 
 169 |     await this.computeRelationships();
 170 |     await this.updateWeights();
 171 |   }
 172 | 
 173 |   /**
 174 |    * Process a single memory entry to extract graph entities
 175 |    */
 176 |   private async processMemoryEntry(memory: MemoryEntry): Promise<void> {
 177 |     // Create project node
 178 |     if (memory.metadata.projectId) {
 179 |       const projectNode = this.addNode({
 180 |         id: `project:${memory.metadata.projectId}`,
 181 |         type: "project",
 182 |         label: memory.metadata.projectId,
 183 |         properties: {
 184 |           repository: memory.metadata.repository,
 185 |           lastActivity: memory.timestamp,
 186 |         },
 187 |         weight: 1.0,
 188 |       });
 189 | 
 190 |       // Create technology nodes
 191 |       if (memory.type === "analysis" && memory.data.language) {
 192 |         const langNode = this.addNode({
 193 |           id: `tech:${memory.data.language.primary}`,
 194 |           type: "technology",
 195 |           label: memory.data.language.primary,
 196 |           properties: {
 197 |             category: "language",
 198 |             popularity: this.getTechnologyPopularity(
 199 |               memory.data.language.primary,
 200 |             ),
 201 |           },
 202 |           weight: 1.0,
 203 |         });
 204 | 
 205 |         this.addEdge({
 206 |           source: projectNode.id,
 207 |           target: langNode.id,
 208 |           type: "uses",
 209 |           weight: 1.0,
 210 |           confidence: 0.9,
 211 |           properties: { source: "analysis" },
 212 |         });
 213 |       }
 214 | 
 215 |       // Create framework nodes
 216 |       if (memory.data.framework?.name) {
 217 |         const frameworkNode = this.addNode({
 218 |           id: `tech:${memory.data.framework.name}`,
 219 |           type: "technology",
 220 |           label: memory.data.framework.name,
 221 |           properties: {
 222 |             category: "framework",
 223 |             version: memory.data.framework.version,
 224 |           },
 225 |           weight: 1.0,
 226 |         });
 227 | 
 228 |         this.addEdge({
 229 |           source: projectNode.id,
 230 |           target: frameworkNode.id,
 231 |           type: "uses",
 232 |           weight: 1.0,
 233 |           confidence: 0.8,
 234 |           properties: { source: "analysis" },
 235 |         });
 236 |       }
 237 | 
 238 |       // Create SSG recommendation nodes
 239 |       if (memory.type === "recommendation" && memory.data.recommended) {
 240 |         const ssgNode = this.addNode({
 241 |           id: `tech:${memory.data.recommended}`,
 242 |           type: "technology",
 243 |           label: memory.data.recommended,
 244 |           properties: {
 245 |             category: "ssg",
 246 |             score: memory.data.score,
 247 |           },
 248 |           weight: 1.0,
 249 |         });
 250 | 
 251 |         this.addEdge({
 252 |           source: projectNode.id,
 253 |           target: ssgNode.id,
 254 |           type: "recommends",
 255 |           weight: memory.data.score || 1.0,
 256 |           confidence: memory.data.confidence || 0.5,
 257 |           properties: {
 258 |             source: "recommendation",
 259 |             reasoning: memory.data.reasoning,
 260 |           },
 261 |         });
 262 |       }
 263 | 
 264 |       // Create outcome nodes
 265 |       if (memory.type === "deployment") {
 266 |         const outcomeNode = this.addNode({
 267 |           id: `outcome:${memory.data.status}:${memory.metadata.ssg}`,
 268 |           type: "outcome",
 269 |           label: `${memory.data.status} with ${memory.metadata.ssg}`,
 270 |           properties: {
 271 |             status: memory.data.status,
 272 |             ssg: memory.metadata.ssg,
 273 |             duration: memory.data.duration,
 274 |           },
 275 |           weight: memory.data.status === "success" ? 1.0 : 0.5,
 276 |         });
 277 | 
 278 |         this.addEdge({
 279 |           source: projectNode.id,
 280 |           target: outcomeNode.id,
 281 |           type: "results_in",
 282 |           weight: 1.0,
 283 |           confidence: 1.0,
 284 |           properties: {
 285 |             timestamp: memory.timestamp,
 286 |             details: memory.data.details,
 287 |           },
 288 |         });
 289 |       }
 290 |     }
 291 |   }
 292 | 
 293 |   /**
 294 |    * Compute additional relationships based on patterns
 295 |    */
 296 |   private async computeRelationships(): Promise<void> {
 297 |     // Find similar projects
 298 |     await this.computeProjectSimilarity();
 299 | 
 300 |     // Find technology dependencies
 301 |     await this.computeTechnologyDependencies();
 302 | 
 303 |     // Find pattern relationships
 304 |     await this.computePatternRelationships();
 305 |   }
 306 | 
 307 |   /**
 308 |    * Compute project similarity relationships
 309 |    */
 310 |   private async computeProjectSimilarity(): Promise<void> {
 311 |     const projectNodes = Array.from(this.nodes.values()).filter(
 312 |       (node) => node.type === "project",
 313 |     );
 314 | 
 315 |     for (let i = 0; i < projectNodes.length; i++) {
 316 |       for (let j = i + 1; j < projectNodes.length; j++) {
 317 |         const similarity = this.calculateProjectSimilarity(
 318 |           projectNodes[i],
 319 |           projectNodes[j],
 320 |         );
 321 | 
 322 |         if (similarity > 0.7) {
 323 |           this.addEdge({
 324 |             source: projectNodes[i].id,
 325 |             target: projectNodes[j].id,
 326 |             type: "similar_to",
 327 |             weight: similarity,
 328 |             confidence: similarity,
 329 |             properties: {
 330 |               computed: true,
 331 |               similarityScore: similarity,
 332 |             },
 333 |           });
 334 |         }
 335 |       }
 336 |     }
 337 |   }
 338 | 
 339 |   /**
 340 |    * Calculate similarity between two projects
 341 |    */
 342 |   private calculateProjectSimilarity(
 343 |     project1: GraphNode,
 344 |     project2: GraphNode,
 345 |   ): number {
 346 |     const tech1 = this.getConnectedTechnologies(project1.id);
 347 |     const tech2 = this.getConnectedTechnologies(project2.id);
 348 | 
 349 |     if (tech1.size === 0 || tech2.size === 0) return 0;
 350 | 
 351 |     const intersection = new Set([...tech1].filter((x) => tech2.has(x)));
 352 |     const union = new Set([...tech1, ...tech2]);
 353 | 
 354 |     return intersection.size / union.size; // Jaccard similarity
 355 |   }
 356 | 
 357 |   /**
 358 |    * Get technologies connected to a project
 359 |    */
 360 |   private getConnectedTechnologies(projectId: string): Set<string> {
 361 |     const technologies = new Set<string>();
 362 |     const adjacents = this.adjacencyList.get(projectId) || new Set();
 363 | 
 364 |     for (const nodeId of adjacents) {
 365 |       const node = this.nodes.get(nodeId);
 366 |       if (node && node.type === "technology") {
 367 |         technologies.add(nodeId);
 368 |       }
 369 |     }
 370 | 
 371 |     return technologies;
 372 |   }
 373 | 
 374 |   /**
 375 |    * Compute technology dependency relationships
 376 |    */
 377 |   private async computeTechnologyDependencies(): Promise<void> {
 378 |     // Define known technology dependencies
 379 |     const dependencies = new Map([
 380 |       ["tech:react", ["tech:javascript", "tech:nodejs"]],
 381 |       ["tech:vue", ["tech:javascript", "tech:nodejs"]],
 382 |       ["tech:angular", ["tech:typescript", "tech:nodejs"]],
 383 |       ["tech:gatsby", ["tech:react", "tech:graphql"]],
 384 |       ["tech:next.js", ["tech:react", "tech:nodejs"]],
 385 |       ["tech:nuxt.js", ["tech:vue", "tech:nodejs"]],
 386 |       ["tech:docusaurus", ["tech:react", "tech:markdown"]],
 387 |       ["tech:jekyll", ["tech:ruby", "tech:markdown"]],
 388 |       ["tech:hugo", ["tech:go", "tech:markdown"]],
 389 |       ["tech:mkdocs", ["tech:python", "tech:markdown"]],
 390 |     ]);
 391 | 
 392 |     for (const [tech, deps] of dependencies) {
 393 |       for (const dep of deps) {
 394 |         const techNode = this.nodes.get(tech);
 395 |         const depNode = this.nodes.get(dep);
 396 | 
 397 |         if (techNode && depNode) {
 398 |           this.addEdge({
 399 |             source: tech,
 400 |             target: dep,
 401 |             type: "depends_on",
 402 |             weight: 0.8,
 403 |             confidence: 0.9,
 404 |             properties: {
 405 |               computed: true,
 406 |               dependency_type: "runtime",
 407 |             },
 408 |           });
 409 |         }
 410 |       }
 411 |     }
 412 |   }
 413 | 
 414 |   /**
 415 |    * Compute pattern relationships from successful combinations
 416 |    */
 417 |   private async computePatternRelationships(): Promise<void> {
 418 |     const successfulOutcomes = Array.from(this.nodes.values()).filter(
 419 |       (node) => node.type === "outcome" && node.properties.status === "success",
 420 |     );
 421 | 
 422 |     for (const outcome of successfulOutcomes) {
 423 |       // Find the path that led to this successful outcome
 424 |       const incomingEdges = Array.from(this.edges.values()).filter(
 425 |         (edge) => edge.target === outcome.id,
 426 |       );
 427 | 
 428 |       for (const edge of incomingEdges) {
 429 |         const sourceNode = this.nodes.get(edge.source);
 430 |         if (sourceNode && sourceNode.type === "project") {
 431 |           // Strengthen relationships for successful patterns
 432 |           this.strengthenSuccessPattern(sourceNode.id, outcome.properties.ssg);
 433 |         }
 434 |       }
 435 |     }
 436 |   }
 437 | 
 438 |   /**
 439 |    * Strengthen relationships for successful patterns
 440 |    */
 441 |   private strengthenSuccessPattern(projectId: string, ssg: string): void {
 442 |     const ssgNodeId = `tech:${ssg}`;
 443 |     const edgeId = `${projectId}-recommends-${ssgNodeId}`;
 444 |     const edge = this.edges.get(edgeId);
 445 | 
 446 |     if (edge) {
 447 |       edge.weight = Math.min(edge.weight * 1.2, 2.0);
 448 |       edge.confidence = Math.min(edge.confidence * 1.1, 1.0);
 449 |     }
 450 |   }
 451 | 
 452 |   /**
 453 |    * Update node and edge weights based on usage patterns
 454 |    */
 455 |   private async updateWeights(): Promise<void> {
 456 |     // Update node weights based on connections
 457 |     for (const node of this.nodes.values()) {
 458 |       const connections = this.adjacencyList.get(node.id)?.size || 0;
 459 |       node.weight = Math.log(connections + 1) / Math.log(10); // Logarithmic scaling
 460 |     }
 461 | 
 462 |     // Update edge weights based on frequency and success
 463 |     for (const edge of this.edges.values()) {
 464 |       if (edge.type === "recommends") {
 465 |         // Find successful outcomes for this recommendation
 466 |         const targetNode = this.nodes.get(edge.target);
 467 |         if (targetNode && targetNode.type === "technology") {
 468 |           const successRate = this.calculateSuccessRate(targetNode.id);
 469 |           edge.weight *= 1 + successRate;
 470 |         }
 471 |       }
 472 |     }
 473 |   }
 474 | 
 475 |   /**
 476 |    * Calculate success rate for a technology
 477 |    */
 478 |   private calculateSuccessRate(techId: string): number {
 479 |     const tech = techId.replace("tech:", "");
 480 |     const outcomes = Array.from(this.nodes.values()).filter(
 481 |       (node) => node.type === "outcome" && node.properties.ssg === tech,
 482 |     );
 483 | 
 484 |     if (outcomes.length === 0) return 0;
 485 | 
 486 |     const successes = outcomes.filter(
 487 |       (node) => node.properties.status === "success",
 488 |     ).length;
 489 |     return successes / outcomes.length;
 490 |   }
 491 | 
 492 |   /**
 493 |    * Find the shortest path between two nodes
 494 |    */
 495 |   findPath(
 496 |     sourceId: string,
 497 |     targetId: string,
 498 |     maxDepth: number = 5,
 499 |   ): GraphPath | null {
 500 |     const visited = new Set<string>();
 501 |     const queue: { nodeId: string; path: GraphPath }[] = [
 502 |       {
 503 |         nodeId: sourceId,
 504 |         path: {
 505 |           nodes: [this.nodes.get(sourceId)!],
 506 |           edges: [],
 507 |           totalWeight: 0,
 508 |           confidence: 1.0,
 509 |         },
 510 |       },
 511 |     ];
 512 | 
 513 |     while (queue.length > 0) {
 514 |       const current = queue.shift()!;
 515 | 
 516 |       if (current.nodeId === targetId) {
 517 |         return current.path;
 518 |       }
 519 | 
 520 |       if (current.path.nodes.length >= maxDepth) {
 521 |         continue;
 522 |       }
 523 | 
 524 |       visited.add(current.nodeId);
 525 |       const neighbors = this.adjacencyList.get(current.nodeId) || new Set();
 526 | 
 527 |       for (const neighborId of neighbors) {
 528 |         if (visited.has(neighborId)) continue;
 529 | 
 530 |         const edge = this.findEdge(current.nodeId, neighborId);
 531 |         const neighborNode = this.nodes.get(neighborId);
 532 | 
 533 |         if (edge && neighborNode) {
 534 |           const newPath: GraphPath = {
 535 |             nodes: [...current.path.nodes, neighborNode],
 536 |             edges: [...current.path.edges, edge],
 537 |             totalWeight: current.path.totalWeight + edge.weight,
 538 |             confidence: current.path.confidence * edge.confidence,
 539 |           };
 540 | 
 541 |           queue.push({ nodeId: neighborId, path: newPath });
 542 |         }
 543 |       }
 544 |     }
 545 | 
 546 |     return null;
 547 |   }
 548 | 
 549 |   /**
 550 |    * Find edge between two nodes
 551 |    */
 552 |   private findEdge(sourceId: string, targetId: string): GraphEdge | null {
 553 |     for (const edge of this.edges.values()) {
 554 |       if (edge.source === sourceId && edge.target === targetId) {
 555 |         return edge;
 556 |       }
 557 |     }
 558 |     return null;
 559 |   }
 560 | 
 561 |   /**
 562 |    * Query the knowledge graph
 563 |    */
 564 |   query(query: GraphQuery): {
 565 |     nodes: GraphNode[];
 566 |     edges: GraphEdge[];
 567 |     paths?: GraphPath[];
 568 |   } {
 569 |     let nodes = Array.from(this.nodes.values());
 570 |     let edges = Array.from(this.edges.values());
 571 | 
 572 |     // Filter by node types
 573 |     if (query.nodeTypes) {
 574 |       nodes = nodes.filter((node) => query.nodeTypes!.includes(node.type));
 575 |     }
 576 | 
 577 |     // Filter by edge types
 578 |     if (query.edgeTypes) {
 579 |       edges = edges.filter((edge) => query.edgeTypes!.includes(edge.type));
 580 |     }
 581 | 
 582 |     // Filter by properties
 583 |     if (query.properties) {
 584 |       nodes = nodes.filter((node) =>
 585 |         Object.entries(query.properties!).every(
 586 |           ([key, value]) => node.properties[key] === value,
 587 |         ),
 588 |       );
 589 |     }
 590 | 
 591 |     // Filter by minimum weight
 592 |     if (query.minWeight) {
 593 |       nodes = nodes.filter((node) => node.weight >= query.minWeight!);
 594 |       edges = edges.filter((edge) => edge.weight >= query.minWeight!);
 595 |     }
 596 | 
 597 |     const result = { nodes, edges };
 598 | 
 599 |     // Find paths from start node if specified
 600 |     if (query.startNode && query.maxDepth) {
 601 |       const paths: GraphPath[] = [];
 602 |       const visited = new Set<string>();
 603 | 
 604 |       const emptyPath: GraphPath = {
 605 |         nodes: [],
 606 |         edges: [],
 607 |         totalWeight: 0,
 608 |         confidence: 1.0,
 609 |       };
 610 |       this.explorePaths(
 611 |         query.startNode,
 612 |         emptyPath,
 613 |         paths,
 614 |         visited,
 615 |         query.maxDepth,
 616 |       );
 617 |       (result as any).paths = paths;
 618 |     }
 619 | 
 620 |     return result;
 621 |   }
 622 | 
 623 |   /**
 624 |    * Explore paths from a starting node
 625 |    */
 626 |   private explorePaths(
 627 |     nodeId: string,
 628 |     currentPath: GraphPath,
 629 |     allPaths: GraphPath[],
 630 |     visited: Set<string>,
 631 |     maxDepth: number,
 632 |   ): void {
 633 |     if (currentPath.nodes.length >= maxDepth) return;
 634 | 
 635 |     visited.add(nodeId);
 636 |     const neighbors = this.adjacencyList.get(nodeId) || new Set();
 637 | 
 638 |     for (const neighborId of neighbors) {
 639 |       if (visited.has(neighborId)) continue;
 640 | 
 641 |       const edge = this.findEdge(nodeId, neighborId);
 642 |       const neighborNode = this.nodes.get(neighborId);
 643 | 
 644 |       if (edge && neighborNode) {
 645 |         const newPath: GraphPath = {
 646 |           nodes: [...(currentPath.nodes || []), neighborNode],
 647 |           edges: [...(currentPath.edges || []), edge],
 648 |           totalWeight: (currentPath.totalWeight || 0) + edge.weight,
 649 |           confidence: (currentPath.confidence || 1.0) * edge.confidence,
 650 |         };
 651 | 
 652 |         allPaths.push(newPath);
 653 |         this.explorePaths(
 654 |           neighborId,
 655 |           newPath,
 656 |           allPaths,
 657 |           new Set(visited),
 658 |           maxDepth,
 659 |         );
 660 |       }
 661 |     }
 662 |   }
 663 | 
 664 |   /**
 665 |    * Get enhanced recommendations using knowledge graph
 666 |    */
 667 |   async getGraphBasedRecommendation(
 668 |     projectFeatures: any,
 669 |     candidateSSGs: string[],
 670 |   ): Promise<RecommendationPath[]> {
 671 |     const recommendations: RecommendationPath[] = [];
 672 | 
 673 |     // Create a temporary project node
 674 |     const tempProjectId = `temp:${Date.now()}`;
 675 |     const projectNode = this.addNode({
 676 |       id: tempProjectId,
 677 |       type: "project",
 678 |       label: "Query Project",
 679 |       properties: projectFeatures,
 680 |       weight: 1.0,
 681 |     });
 682 | 
 683 |     for (const ssg of candidateSSGs) {
 684 |       const ssgNodeId = `tech:${ssg}`;
 685 |       const ssgNode = this.nodes.get(ssgNodeId);
 686 | 
 687 |       if (ssgNode) {
 688 |         // Find paths from similar projects to this SSG
 689 |         const similarProjects = this.findSimilarProjects(projectFeatures);
 690 | 
 691 |         for (const similarProject of similarProjects) {
 692 |           const path = this.findPath(similarProject.id, ssgNodeId);
 693 | 
 694 |           if (path) {
 695 |             const reasoning = this.generateReasoning(path);
 696 |             const confidence = this.calculatePathConfidence(
 697 |               path,
 698 |               projectFeatures,
 699 |             );
 700 | 
 701 |             recommendations.push({
 702 |               from: projectNode,
 703 |               to: ssgNode,
 704 |               path,
 705 |               reasoning,
 706 |               confidence,
 707 |             });
 708 |           }
 709 |         }
 710 |       }
 711 |     }
 712 | 
 713 |     // Clean up temporary node
 714 |     this.nodes.delete(tempProjectId);
 715 | 
 716 |     return recommendations.sort((a, b) => b.confidence - a.confidence);
 717 |   }
 718 | 
 719 |   /**
 720 |    * Find projects similar to given features
 721 |    */
 722 |   private findSimilarProjects(features: any): GraphNode[] {
 723 |     const projectNodes = Array.from(this.nodes.values()).filter(
 724 |       (node) => node.type === "project",
 725 |     );
 726 | 
 727 |     return projectNodes
 728 |       .map((project) => ({
 729 |         project,
 730 |         similarity: this.calculateFeatureSimilarity(
 731 |           features,
 732 |           project.properties,
 733 |         ),
 734 |       }))
 735 |       .filter(({ similarity }) => similarity > 0.6)
 736 |       .sort((a, b) => b.similarity - a.similarity)
 737 |       .slice(0, 5)
 738 |       .map(({ project }) => project);
 739 |   }
 740 | 
 741 |   /**
 742 |    * Calculate similarity between features and project properties
 743 |    */
 744 |   private calculateFeatureSimilarity(features: any, properties: any): number {
 745 |     let score = 0;
 746 |     let factors = 0;
 747 | 
 748 |     if (features.language === properties.language) {
 749 |       score += 0.4;
 750 |     }
 751 |     factors++;
 752 | 
 753 |     if (features.framework === properties.framework) {
 754 |       score += 0.3;
 755 |     }
 756 |     factors++;
 757 | 
 758 |     if (features.size === properties.size) {
 759 |       score += 0.2;
 760 |     }
 761 |     factors++;
 762 | 
 763 |     if (features.complexity === properties.complexity) {
 764 |       score += 0.1;
 765 |     }
 766 |     factors++;
 767 | 
 768 |     return factors > 0 ? score / factors : 0;
 769 |   }
 770 | 
 771 |   /**
 772 |    * Generate human-readable reasoning for a recommendation path
 773 |    */
 774 |   private generateReasoning(path: GraphPath): string[] {
 775 |     const reasoning: string[] = [];
 776 | 
 777 |     for (let i = 0; i < path.edges.length; i++) {
 778 |       const edge = path.edges[i];
 779 |       const sourceNode = path.nodes[i];
 780 |       const targetNode = path.nodes[i + 1];
 781 | 
 782 |       switch (edge.type) {
 783 |         case "similar_to":
 784 |           reasoning.push(
 785 |             `Similar to ${sourceNode.label} (${(edge.confidence * 100).toFixed(
 786 |               0,
 787 |             )}% similarity)`,
 788 |           );
 789 |           break;
 790 |         case "recommends":
 791 |           reasoning.push(
 792 |             `Successfully used ${
 793 |               targetNode.label
 794 |             } (score: ${edge.weight.toFixed(1)})`,
 795 |           );
 796 |           break;
 797 |         case "results_in":
 798 |           reasoning.push(
 799 |             `Resulted in ${targetNode.properties.status} deployment`,
 800 |           );
 801 |           break;
 802 |         case "uses":
 803 |           reasoning.push(`Uses ${targetNode.label}`);
 804 |           break;
 805 |       }
 806 |     }
 807 | 
 808 |     return reasoning;
 809 |   }
 810 | 
 811 |   /**
 812 |    * Calculate confidence for a recommendation path
 813 |    */
 814 |   private calculatePathConfidence(path: GraphPath, _features: any): number {
 815 |     let confidence = path.confidence;
 816 | 
 817 |     // Boost confidence for shorter paths
 818 |     confidence *= 1 / Math.max(path.edges.length, 1);
 819 | 
 820 |     // Boost confidence for recent data
 821 |     const avgAge =
 822 |       path.nodes.reduce((sum, node) => {
 823 |         const age = Date.now() - new Date(node.lastUpdated).getTime();
 824 |         return sum + age;
 825 |       }, 0) / path.nodes.length;
 826 | 
 827 |     const daysSinceUpdate = avgAge / (1000 * 60 * 60 * 24);
 828 |     confidence *= Math.exp(-daysSinceUpdate / 30); // Exponential decay over 30 days
 829 | 
 830 |     return Math.min(confidence, 1.0);
 831 |   }
 832 | 
 833 |   /**
 834 |    * Get technology popularity score
 835 |    */
 836 |   private getTechnologyPopularity(tech: string): number {
 837 |     // Simple popularity scoring - could be enhanced with real data
 838 |     const popularityMap = new Map([
 839 |       ["javascript", 0.9],
 840 |       ["typescript", 0.8],
 841 |       ["python", 0.8],
 842 |       ["react", 0.9],
 843 |       ["vue", 0.7],
 844 |       ["angular", 0.6],
 845 |       ["go", 0.7],
 846 |       ["ruby", 0.5],
 847 |       ["rust", 0.6],
 848 |     ]);
 849 | 
 850 |     return popularityMap.get(tech.toLowerCase()) || 0.3;
 851 |   }
 852 | 
 853 |   /**
 854 |    * Save knowledge graph to persistent memory
 855 |    */
 856 |   async saveToMemory(): Promise<void> {
 857 |     const graphData = {
 858 |       nodes: Array.from(this.nodes.entries()),
 859 |       edges: Array.from(this.edges.entries()),
 860 |       lastUpdate: this.lastUpdate,
 861 |       statistics: this.getStatistics(),
 862 |     };
 863 | 
 864 |     await this.memoryManager.remember(
 865 |       "interaction",
 866 |       {
 867 |         graph: graphData,
 868 |         type: "knowledge_graph",
 869 |       },
 870 |       {
 871 |         tags: ["knowledge_graph", "structure"],
 872 |       },
 873 |     );
 874 |   }
 875 | 
 876 |   /**
 877 |    * Load knowledge graph from persistent memory
 878 |    */
 879 |   async loadFromMemory(): Promise<void> {
 880 |     try {
 881 |       const graphMemories = await this.memoryManager.search("knowledge_graph");
 882 | 
 883 |       if (graphMemories.length > 0) {
 884 |         const latestGraph = graphMemories.sort(
 885 |           (a, b) =>
 886 |             new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
 887 |         )[0];
 888 | 
 889 |         if (latestGraph.data.graph) {
 890 |           const { nodes, edges } = latestGraph.data.graph;
 891 | 
 892 |           // Restore nodes
 893 |           for (const [id, node] of nodes) {
 894 |             this.nodes.set(id, node);
 895 |           }
 896 | 
 897 |           // Restore edges and adjacency list
 898 |           for (const [id, edge] of edges) {
 899 |             this.edges.set(id, edge);
 900 | 
 901 |             if (!this.adjacencyList.has(edge.source)) {
 902 |               this.adjacencyList.set(edge.source, new Set());
 903 |             }
 904 |             this.adjacencyList.get(edge.source)!.add(edge.target);
 905 |           }
 906 | 
 907 |           this.lastUpdate = latestGraph.data.graph.lastUpdate;
 908 |         }
 909 |       }
 910 |     } catch (error) {
 911 |       console.error("Failed to load knowledge graph from memory:", error);
 912 |     }
 913 |   }
 914 | 
 915 |   /**
 916 |    * Get all nodes in the knowledge graph
 917 |    */
 918 |   async getAllNodes(): Promise<GraphNode[]> {
 919 |     return Array.from(this.nodes.values());
 920 |   }
 921 | 
 922 |   /**
 923 |    * Get all edges in the knowledge graph
 924 |    */
 925 |   async getAllEdges(): Promise<GraphEdge[]> {
 926 |     return Array.from(this.edges.values());
 927 |   }
 928 | 
 929 |   /**
 930 |    * Get a node by its ID
 931 |    */
 932 |   async getNodeById(nodeId: string): Promise<GraphNode | null> {
 933 |     return this.nodes.get(nodeId) || null;
 934 |   }
 935 | 
 936 |   /**
 937 |    * Remove a node from the knowledge graph
 938 |    */
 939 |   async removeNode(nodeId: string): Promise<boolean> {
 940 |     const node = this.nodes.get(nodeId);
 941 |     if (!node) {
 942 |       return false;
 943 |     }
 944 | 
 945 |     // Remove the node
 946 |     this.nodes.delete(nodeId);
 947 | 
 948 |     // Remove all edges connected to this node
 949 |     const edgesToRemove: string[] = [];
 950 |     for (const [edgeId, edge] of this.edges) {
 951 |       if (edge.source === nodeId || edge.target === nodeId) {
 952 |         edgesToRemove.push(edgeId);
 953 |       }
 954 |     }
 955 | 
 956 |     for (const edgeId of edgesToRemove) {
 957 |       this.edges.delete(edgeId);
 958 |     }
 959 | 
 960 |     // Update adjacency list
 961 |     this.adjacencyList.delete(nodeId);
 962 |     for (const [, targets] of this.adjacencyList) {
 963 |       targets.delete(nodeId);
 964 |     }
 965 | 
 966 |     return true;
 967 |   }
 968 | 
 969 |   /**
 970 |    * Get connections for a specific node
 971 |    */
 972 |   async getConnections(nodeId: string): Promise<string[]> {
 973 |     const connections = this.adjacencyList.get(nodeId);
 974 |     return connections ? Array.from(connections) : [];
 975 |   }
 976 | 
 977 |   /**
 978 |    * Get knowledge graph statistics
 979 |    */
 980 |   async getStatistics(): Promise<{
 981 |     nodeCount: number;
 982 |     edgeCount: number;
 983 |     nodesByType: Record<string, number>;
 984 |     edgesByType: Record<string, number>;
 985 |     averageConnectivity: number;
 986 |     mostConnectedNodes: Array<{ id: string; connections: number }>;
 987 |   }> {
 988 |     const nodesByType: Record<string, number> = {};
 989 |     const edgesByType: Record<string, number> = {};
 990 | 
 991 |     for (const node of this.nodes.values()) {
 992 |       nodesByType[node.type] = (nodesByType[node.type] || 0) + 1;
 993 |     }
 994 | 
 995 |     for (const edge of this.edges.values()) {
 996 |       edgesByType[edge.type] = (edgesByType[edge.type] || 0) + 1;
 997 |     }
 998 | 
 999 |     const connectivityCounts = Array.from(this.adjacencyList.entries())
1000 |       .map(([id, connections]) => ({ id, connections: connections.size }))
1001 |       .sort((a, b) => b.connections - a.connections);
1002 | 
1003 |     const averageConnectivity =
1004 |       connectivityCounts.length > 0
1005 |         ? connectivityCounts.reduce(
1006 |             (sum, { connections }) => sum + connections,
1007 |             0,
1008 |           ) / connectivityCounts.length
1009 |         : 0;
1010 | 
1011 |     return {
1012 |       nodeCount: this.nodes.size,
1013 |       edgeCount: this.edges.size,
1014 |       nodesByType,
1015 |       edgesByType,
1016 |       averageConnectivity,
1017 |       mostConnectedNodes: connectivityCounts.slice(0, 10),
1018 |     };
1019 |   }
1020 | 
1021 |   // ============================================================================
1022 |   // Phase 1.1: Enhanced Node Query Methods
1023 |   // ============================================================================
1024 | 
1025 |   /**
1026 |    * Find a single node matching criteria
1027 |    */
1028 |   async findNode(criteria: {
1029 |     type?: string;
1030 |     properties?: Record<string, any>;
1031 |   }): Promise<GraphNode | null> {
1032 |     for (const node of this.nodes.values()) {
1033 |       if (criteria.type && node.type !== criteria.type) continue;
1034 | 
1035 |       if (criteria.properties) {
1036 |         let matches = true;
1037 |         for (const [key, value] of Object.entries(criteria.properties)) {
1038 |           if (node.properties[key] !== value) {
1039 |             matches = false;
1040 |             break;
1041 |           }
1042 |         }
1043 |         if (!matches) continue;
1044 |       }
1045 | 
1046 |       return node;
1047 |     }
1048 | 
1049 |     return null;
1050 |   }
1051 | 
1052 |   /**
1053 |    * Find all nodes matching criteria
1054 |    */
1055 |   async findNodes(criteria: {
1056 |     type?: string;
1057 |     properties?: Record<string, any>;
1058 |   }): Promise<GraphNode[]> {
1059 |     const results: GraphNode[] = [];
1060 | 
1061 |     for (const node of this.nodes.values()) {
1062 |       if (criteria.type && node.type !== criteria.type) continue;
1063 | 
1064 |       if (criteria.properties) {
1065 |         let matches = true;
1066 |         for (const [key, value] of Object.entries(criteria.properties)) {
1067 |           if (node.properties[key] !== value) {
1068 |             matches = false;
1069 |             break;
1070 |           }
1071 |         }
1072 |         if (!matches) continue;
1073 |       }
1074 | 
1075 |       results.push(node);
1076 |     }
1077 | 
1078 |     return results;
1079 |   }
1080 | 
1081 |   /**
1082 |    * Find edges matching criteria
1083 |    */
1084 |   async findEdges(criteria: {
1085 |     source?: string;
1086 |     target?: string;
1087 |     type?: string;
1088 |     properties?: Record<string, any>;
1089 |   }): Promise<GraphEdge[]> {
1090 |     const results: GraphEdge[] = [];
1091 | 
1092 |     for (const edge of this.edges.values()) {
1093 |       if (criteria.source && edge.source !== criteria.source) continue;
1094 |       if (criteria.target && edge.target !== criteria.target) continue;
1095 |       if (criteria.type && edge.type !== criteria.type) continue;
1096 | 
1097 |       // Match properties if provided
1098 |       if (criteria.properties) {
1099 |         let propertiesMatch = true;
1100 |         for (const [key, value] of Object.entries(criteria.properties)) {
1101 |           if (edge.properties[key] !== value) {
1102 |             propertiesMatch = false;
1103 |             break;
1104 |           }
1105 |         }
1106 |         if (!propertiesMatch) continue;
1107 |       }
1108 | 
1109 |       results.push(edge);
1110 |     }
1111 | 
1112 |     return results;
1113 |   }
1114 | 
1115 |   /**
1116 |    * Find all paths between two nodes up to a maximum depth
1117 |    */
1118 |   async findPaths(criteria: {
1119 |     startNode: string;
1120 |     endNode?: string;
1121 |     edgeTypes?: string[];
1122 |     maxDepth: number;
1123 |   }): Promise<GraphPath[]> {
1124 |     const paths: GraphPath[] = [];
1125 |     const visited = new Set<string>();
1126 | 
1127 |     const emptyPath: GraphPath = {
1128 |       nodes: [this.nodes.get(criteria.startNode)!],
1129 |       edges: [],
1130 |       totalWeight: 0,
1131 |       confidence: 1.0,
1132 |     };
1133 | 
1134 |     this.findPathsRecursive(
1135 |       criteria.startNode,
1136 |       emptyPath,
1137 |       paths,
1138 |       visited,
1139 |       criteria.maxDepth,
1140 |       criteria.endNode,
1141 |       criteria.edgeTypes,
1142 |     );
1143 | 
1144 |     return paths;
1145 |   }
1146 | 
1147 |   /**
1148 |    * Recursive helper for finding paths
1149 |    */
1150 |   private findPathsRecursive(
1151 |     currentNodeId: string,
1152 |     currentPath: GraphPath,
1153 |     allPaths: GraphPath[],
1154 |     visited: Set<string>,
1155 |     maxDepth: number,
1156 |     endNode?: string,
1157 |     edgeTypes?: string[],
1158 |   ): void {
1159 |     if (currentPath.nodes.length >= maxDepth) return;
1160 | 
1161 |     visited.add(currentNodeId);
1162 |     const neighbors = this.adjacencyList.get(currentNodeId) || new Set();
1163 | 
1164 |     for (const neighborId of neighbors) {
1165 |       if (visited.has(neighborId)) continue;
1166 | 
1167 |       const edge = this.findEdge(currentNodeId, neighborId);
1168 |       if (!edge) continue;
1169 | 
1170 |       // Filter by edge type if specified
1171 |       if (edgeTypes && !edgeTypes.includes(edge.type)) continue;
1172 | 
1173 |       const neighborNode = this.nodes.get(neighborId);
1174 |       if (!neighborNode) continue;
1175 | 
1176 |       const newPath: GraphPath = {
1177 |         nodes: [...currentPath.nodes, neighborNode],
1178 |         edges: [...currentPath.edges, edge],
1179 |         totalWeight: currentPath.totalWeight + edge.weight,
1180 |         confidence: currentPath.confidence * edge.confidence,
1181 |       };
1182 | 
1183 |       // If we've reached the end node, add this path
1184 |       if (endNode && neighborId === endNode) {
1185 |         allPaths.push(newPath);
1186 |         continue;
1187 |       }
1188 | 
1189 |       // If no end node specified, add all paths
1190 |       if (!endNode) {
1191 |         allPaths.push(newPath);
1192 |       }
1193 | 
1194 |       // Continue exploring
1195 |       this.findPathsRecursive(
1196 |         neighborId,
1197 |         newPath,
1198 |         allPaths,
1199 |         new Set(visited),
1200 |         maxDepth,
1201 |         endNode,
1202 |         edgeTypes,
1203 |       );
1204 |     }
1205 |   }
1206 | 
1207 |   /**
1208 |    * Get node history (all changes to a node over time)
1209 |    */
1210 |   async getNodeHistory(nodeId: string): Promise<MemoryEntry[]> {
1211 |     const node = this.nodes.get(nodeId);
1212 |     if (!node) return [];
1213 | 
1214 |     // Search memory for all entries related to this node
1215 |     const projectId = node.properties.projectId || node.properties.path;
1216 |     if (!projectId) return [];
1217 | 
1218 |     return await this.memoryManager.search(projectId);
1219 |   }
1220 | 
1221 |   /**
1222 |    * Get schema version
1223 |    */
1224 |   getSchemaVersion(): string {
1225 |     return SCHEMA_METADATA.version;
1226 |   }
1227 | 
1228 |   /**
1229 |    * Validate node against schema
1230 |    */
1231 |   validateNode(node: GraphNode): boolean {
1232 |     try {
1233 |       const entityData = {
1234 |         ...node.properties,
1235 |         type: node.type,
1236 |       };
1237 |       validateEntity(entityData);
1238 |       return true;
1239 |     } catch (error) {
1240 |       console.error(`Node validation failed for ${node.id}:`, error);
1241 |       return false;
1242 |     }
1243 |   }
1244 | 
1245 |   /**
1246 |    * Validate edge against schema
1247 |    */
1248 |   validateEdge(edge: GraphEdge): boolean {
1249 |     try {
1250 |       const relationshipData = {
1251 |         type: edge.type,
1252 |         weight: edge.weight,
1253 |         confidence: edge.confidence,
1254 |         createdAt: edge.lastUpdated,
1255 |         lastUpdated: edge.lastUpdated,
1256 |         metadata: edge.properties,
1257 |         ...edge.properties,
1258 |       };
1259 |       validateRelationship(relationshipData);
1260 |       return true;
1261 |     } catch (error) {
1262 |       console.error(`Edge validation failed for ${edge.id}:`, error);
1263 |       return false;
1264 |     }
1265 |   }
1266 | }
1267 | 
1268 | export default KnowledgeGraph;
1269 | 
```
Page 20/29FirstPrevNextLast