This is page 29 of 33. 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
├── ARCHITECTURAL_CHANGES_SUMMARY.md
├── 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
│ │ ├── adr-0001-mcp-server-architecture.md
│ │ ├── adr-0002-repository-analysis-engine.md
│ │ ├── adr-0003-static-site-generator-recommendation-engine.md
│ │ ├── adr-0004-diataxis-framework-integration.md
│ │ ├── adr-0005-github-pages-deployment-automation.md
│ │ ├── adr-0006-mcp-tools-api-design.md
│ │ ├── adr-0007-mcp-prompts-and-resources-integration.md
│ │ ├── adr-0008-intelligent-content-population-engine.md
│ │ ├── adr-0009-content-accuracy-validation-framework.md
│ │ ├── adr-0010-mcp-resource-pattern-redesign.md
│ │ ├── adr-0011-ce-mcp-compatibility.md
│ │ ├── adr-0012-priority-scoring-system-for-documentation-drift.md
│ │ ├── adr-0013-release-pipeline-and-package-distribution.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
│ ├── CE-MCP-FINDINGS.md
│ ├── 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
│ │ ├── change-watcher.md
│ │ ├── custom-domains.md
│ │ ├── documentation-freshness-tracking.md
│ │ ├── drift-priority-scoring.md
│ │ ├── github-pages-deployment.md
│ │ ├── index.md
│ │ ├── llm-integration.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
├── ISSUE_IMPLEMENTATION_SUMMARY.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
│ │ ├── change-watcher.ts
│ │ ├── check-documentation-links.ts
│ │ ├── cleanup-agent-artifacts.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
│ │ ├── simulate-execution.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
│ │ ├── artifact-detector.ts
│ │ ├── ast-analyzer.ts
│ │ ├── change-watcher.ts
│ │ ├── code-scanner.ts
│ │ ├── content-extractor.ts
│ │ ├── drift-detector.ts
│ │ ├── execution-simulator.ts
│ │ ├── freshness-tracker.ts
│ │ ├── language-parsers-simple.ts
│ │ ├── llm-client.ts
│ │ ├── permission-checker.ts
│ │ ├── semantic-analyzer.ts
│ │ ├── sitemap-generator.ts
│ │ ├── usage-metadata.ts
│ │ └── user-feedback-integration.ts
│ └── workflows
│ └── documentation-workflow.ts
├── test-docs-local.sh
├── tests
│ ├── api
│ │ └── mcp-responses.test.ts
│ ├── benchmarks
│ │ └── performance.test.ts
│ ├── call-graph-builder.test.ts
│ ├── change-watcher-priority.integration.test.ts
│ ├── change-watcher.test.ts
│ ├── edge-cases
│ │ └── error-handling.test.ts
│ ├── execution-simulator.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-documentation-examples.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-documentation-examples.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
│ │ ├── cleanup-agent-artifacts.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
│ ├── artifact-detector.test.ts
│ ├── ast-analyzer.test.ts
│ ├── content-extractor.test.ts
│ ├── drift-detector-diataxis.test.ts
│ ├── drift-detector-priority.test.ts
│ ├── drift-detector.test.ts
│ ├── freshness-tracker.test.ts
│ ├── llm-client.test.ts
│ ├── semantic-analyzer.test.ts
│ ├── sitemap-generator.test.ts
│ ├── usage-metadata.test.ts
│ └── user-feedback-integration.test.ts
├── tsconfig.json
└── typedoc.json
```
# Files
--------------------------------------------------------------------------------
/src/tools/validate-content.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Tool } from "@modelcontextprotocol/sdk/types.js";
2 | import * as fs from "fs/promises";
3 | import * as path from "path";
4 | import { exec } from "child_process";
5 | import { promisify } from "util";
6 | import { handleMemoryRecall } from "../memory/index.js";
7 | import {
8 | createExecutionSimulator,
9 | ExecutionSimulator,
10 | } from "../utils/execution-simulator.js";
11 | // ESM-compatible dirname replacement - fallback for test environments
12 | function getDirname(): string {
13 | // Use process.cwd() as fallback for all environments to avoid import.meta issues
14 | return process.cwd();
15 | }
16 |
17 | const currentDir = getDirname();
18 |
19 | const execAsync = promisify(exec);
20 |
21 | interface ValidationOptions {
22 | contentPath: string;
23 | analysisId?: string;
24 | validationType: "accuracy" | "completeness" | "compliance" | "all";
25 | includeCodeValidation: boolean;
26 | confidence: "strict" | "moderate" | "permissive";
27 | }
28 |
29 | interface ConfidenceMetrics {
30 | overall: number;
31 | breakdown: {
32 | technologyDetection: number;
33 | frameworkVersionAccuracy: number;
34 | codeExampleRelevance: number;
35 | architecturalAssumptions: number;
36 | businessContextAlignment: number;
37 | };
38 | riskFactors: RiskFactor[];
39 | }
40 |
41 | interface RiskFactor {
42 | type: "high" | "medium" | "low";
43 | category: string;
44 | description: string;
45 | impact: string;
46 | mitigation: string;
47 | }
48 |
49 | interface UncertaintyFlag {
50 | area: string;
51 | severity: "low" | "medium" | "high" | "critical";
52 | description: string;
53 | potentialImpact: string;
54 | clarificationNeeded: string;
55 | fallbackStrategy: string;
56 | }
57 |
58 | interface ValidationIssue {
59 | type: "error" | "warning" | "info";
60 | category: "accuracy" | "completeness" | "compliance" | "performance";
61 | location: {
62 | file: string;
63 | line?: number;
64 | section?: string;
65 | };
66 | description: string;
67 | evidence: string[];
68 | suggestedFix: string;
69 | confidence: number;
70 | }
71 |
72 | interface CodeValidationResult {
73 | overallSuccess: boolean;
74 | exampleResults: ExampleValidation[];
75 | confidence: number;
76 | }
77 |
78 | interface ExampleValidation {
79 | example: string;
80 | compilationSuccess: boolean;
81 | executionSuccess: boolean;
82 | issues: ValidationIssue[];
83 | confidence: number;
84 | }
85 |
86 | export interface ValidationResult {
87 | success: boolean;
88 | confidence: ConfidenceMetrics;
89 | issues: ValidationIssue[];
90 | uncertainties: UncertaintyFlag[];
91 | codeValidation?: CodeValidationResult;
92 | recommendations: string[];
93 | nextSteps: string[];
94 | }
95 |
96 | class ContentAccuracyValidator {
97 | private projectContext: any;
98 | private tempDir: string;
99 | private executionSimulator: ExecutionSimulator | null = null;
100 | private useExecutionSimulation: boolean;
101 |
102 | constructor(useExecutionSimulation: boolean = true) {
103 | this.tempDir = path.join(currentDir, ".tmp");
104 | this.useExecutionSimulation = useExecutionSimulation;
105 |
106 | // Initialize execution simulator if enabled
107 | if (useExecutionSimulation) {
108 | this.executionSimulator = createExecutionSimulator({
109 | maxDepth: 5,
110 | maxSteps: 50,
111 | timeoutMs: 15000,
112 | detectNullRefs: true,
113 | detectTypeMismatches: true,
114 | detectUnreachableCode: true,
115 | confidenceThreshold: 0.6,
116 | });
117 | }
118 | }
119 |
120 | async validateContent(
121 | options: ValidationOptions,
122 | context?: any,
123 | ): Promise<ValidationResult> {
124 | if (context?.meta?.progressToken) {
125 | await context.meta.reportProgress?.({ progress: 0, total: 100 });
126 | }
127 |
128 | const result: ValidationResult = {
129 | success: false,
130 | confidence: this.initializeConfidenceMetrics(),
131 | issues: [],
132 | uncertainties: [],
133 | recommendations: [],
134 | nextSteps: [],
135 | };
136 |
137 | // Load project context if analysis ID provided
138 | if (options.analysisId) {
139 | await context?.info?.("📊 Loading project context...");
140 | this.projectContext = await this.loadProjectContext(options.analysisId);
141 | }
142 |
143 | if (context?.meta?.progressToken) {
144 | await context.meta.reportProgress?.({ progress: 20, total: 100 });
145 | }
146 |
147 | // Determine if we should analyze application code vs documentation
148 | await context?.info?.("🔎 Analyzing content type...");
149 | const isApplicationValidation = await this.shouldAnalyzeApplicationCode(
150 | options.contentPath,
151 | );
152 |
153 | if (context?.meta?.progressToken) {
154 | await context.meta.reportProgress?.({ progress: 40, total: 100 });
155 | }
156 |
157 | // Perform different types of validation based on request
158 | if (
159 | options.validationType === "all" ||
160 | options.validationType === "accuracy"
161 | ) {
162 | await this.validateAccuracy(options.contentPath, result);
163 | }
164 |
165 | if (
166 | options.validationType === "all" ||
167 | options.validationType === "completeness"
168 | ) {
169 | await this.validateCompleteness(options.contentPath, result);
170 | }
171 |
172 | if (
173 | options.validationType === "all" ||
174 | options.validationType === "compliance"
175 | ) {
176 | if (isApplicationValidation) {
177 | await this.validateApplicationStructureCompliance(
178 | options.contentPath,
179 | result,
180 | );
181 | } else {
182 | await this.validateDiataxisCompliance(options.contentPath, result);
183 | }
184 | }
185 |
186 | // Code validation if requested
187 | if (options.includeCodeValidation) {
188 | result.codeValidation = await this.validateCodeExamples(
189 | options.contentPath,
190 | );
191 | // Set code example relevance confidence based on code validation results
192 | if (result.codeValidation) {
193 | const successRate =
194 | result.codeValidation.exampleResults.length > 0
195 | ? result.codeValidation.exampleResults.filter(
196 | (e) => e.compilationSuccess,
197 | ).length / result.codeValidation.exampleResults.length
198 | : 1;
199 | result.confidence.breakdown.codeExampleRelevance = Math.round(
200 | successRate * 100,
201 | );
202 | }
203 | } else {
204 | // If code validation is skipped, assume reasonable confidence
205 | result.confidence.breakdown.codeExampleRelevance = 75;
206 | }
207 |
208 | // Set framework version accuracy based on technology detection confidence
209 | result.confidence.breakdown.frameworkVersionAccuracy = Math.min(
210 | 90,
211 | result.confidence.breakdown.technologyDetection + 10,
212 | );
213 |
214 | // Set architectural assumptions confidence based on file structure and content analysis
215 | const filesAnalyzed = await this.getMarkdownFiles(options.contentPath);
216 | const hasStructuredContent = filesAnalyzed.length > 3; // Basic heuristic
217 | result.confidence.breakdown.architecturalAssumptions = hasStructuredContent
218 | ? 80
219 | : 60;
220 |
221 | // Calculate overall confidence and success
222 | this.calculateOverallMetrics(result);
223 |
224 | // Generate recommendations and next steps
225 | this.generateRecommendations(result, options);
226 |
227 | if (context?.meta?.progressToken) {
228 | await context.meta.reportProgress?.({ progress: 100, total: 100 });
229 | }
230 |
231 | const status = result.success ? "PASSED" : "ISSUES FOUND";
232 | await context?.info?.(
233 | `✅ Validation complete! Status: ${status} (${result.confidence.overall}% confidence, ${result.issues.length} issue(s))`,
234 | );
235 |
236 | return result;
237 | }
238 |
239 | private initializeConfidenceMetrics(): ConfidenceMetrics {
240 | return {
241 | overall: 0,
242 | breakdown: {
243 | technologyDetection: 0,
244 | frameworkVersionAccuracy: 0,
245 | codeExampleRelevance: 0,
246 | architecturalAssumptions: 0,
247 | businessContextAlignment: 0,
248 | },
249 | riskFactors: [],
250 | };
251 | }
252 |
253 | private async loadProjectContext(analysisId: string): Promise<any> {
254 | // Try to get analysis from memory system first
255 | try {
256 | const memoryRecall = await handleMemoryRecall({
257 | query: analysisId,
258 | type: "analysis",
259 | limit: 1,
260 | });
261 |
262 | // Handle the memory recall result structure
263 | if (
264 | memoryRecall &&
265 | memoryRecall.memories &&
266 | memoryRecall.memories.length > 0
267 | ) {
268 | const memory = memoryRecall.memories[0];
269 |
270 | // Handle wrapped content structure
271 | if (
272 | memory.data &&
273 | memory.data.content &&
274 | Array.isArray(memory.data.content)
275 | ) {
276 | // Extract the JSON from the first text content
277 | const firstContent = memory.data.content[0];
278 | if (
279 | firstContent &&
280 | firstContent.type === "text" &&
281 | firstContent.text
282 | ) {
283 | try {
284 | return JSON.parse(firstContent.text);
285 | } catch (parseError) {
286 | console.warn(
287 | "Failed to parse analysis content from memory:",
288 | parseError,
289 | );
290 | }
291 | }
292 | }
293 |
294 | // Try direct content or data access
295 | if (memory.content) {
296 | return memory.content;
297 | }
298 | if (memory.data) {
299 | return memory.data;
300 | }
301 | }
302 | } catch (error) {
303 | console.warn("Failed to retrieve from memory system:", error);
304 | }
305 |
306 | // Fallback to reading from cached analysis file
307 | try {
308 | const analysisPath = path.join(
309 | ".documcp",
310 | "analyses",
311 | `${analysisId}.json`,
312 | );
313 | const content = await fs.readFile(analysisPath, "utf-8");
314 | return JSON.parse(content);
315 | } catch {
316 | // Return default context if no analysis found
317 | return {
318 | metadata: { projectName: "unknown", primaryLanguage: "JavaScript" },
319 | technologies: {},
320 | dependencies: { packages: [] },
321 | };
322 | }
323 | }
324 |
325 | private async validateAccuracy(
326 | contentPath: string,
327 | result: ValidationResult,
328 | ): Promise<void> {
329 | const files = await this.getMarkdownFiles(contentPath);
330 |
331 | for (const file of files) {
332 | const content = await fs.readFile(file, "utf-8");
333 |
334 | // Check for common accuracy issues
335 | await this.checkTechnicalAccuracy(file, content, result);
336 | await this.checkFrameworkVersionCompatibility(file, content, result);
337 | await this.checkCommandAccuracy(file, content, result);
338 | await this.checkLinkValidity(file, content, result);
339 | }
340 |
341 | // Update confidence based on findings
342 | this.updateAccuracyConfidence(result);
343 | }
344 |
345 | private async checkTechnicalAccuracy(
346 | filePath: string,
347 | content: string,
348 | result: ValidationResult,
349 | ): Promise<void> {
350 | // Check for deprecated patterns
351 | const deprecatedPatterns = [
352 | {
353 | pattern: /npm install -g/,
354 | suggestion: "Use npx instead of global installs",
355 | },
356 | { pattern: /var\s+\w+/, suggestion: "Use const or let instead of var" },
357 | { pattern: /function\(\)/, suggestion: "Consider using arrow functions" },
358 | { pattern: /http:\/\//, suggestion: "Use HTTPS URLs for security" },
359 | ];
360 |
361 | for (const { pattern, suggestion } of deprecatedPatterns) {
362 | if (pattern.test(content)) {
363 | result.issues.push({
364 | type: "warning",
365 | category: "accuracy",
366 | location: { file: path.basename(filePath) },
367 | description: `Potentially outdated pattern detected: ${pattern.source}`,
368 | evidence: [content.match(pattern)?.[0] || ""],
369 | suggestedFix: suggestion,
370 | confidence: 80,
371 | });
372 | }
373 | }
374 |
375 | // Check for missing error handling in code examples
376 | const codeBlocks = this.extractCodeBlocks(content);
377 | for (const block of codeBlocks) {
378 | if (block.language === "javascript" || block.language === "typescript") {
379 | if (
380 | block.code.includes("await") &&
381 | !block.code.includes("try") &&
382 | !block.code.includes("catch")
383 | ) {
384 | result.issues.push({
385 | type: "warning",
386 | category: "accuracy",
387 | location: { file: path.basename(filePath) },
388 | description: "Async code without error handling",
389 | evidence: [block.code.substring(0, 100)],
390 | suggestedFix: "Add try-catch blocks for async operations",
391 | confidence: 90,
392 | });
393 | }
394 | }
395 | }
396 | }
397 |
398 | private async checkFrameworkVersionCompatibility(
399 | filePath: string,
400 | content: string,
401 | result: ValidationResult,
402 | ): Promise<void> {
403 | if (!this.projectContext) return;
404 |
405 | // Check if mentioned versions align with project dependencies
406 | const versionPattern = /@(\d+\.\d+\.\d+)/g;
407 | const matches = content.match(versionPattern);
408 |
409 | if (matches) {
410 | for (const match of matches) {
411 | const version = match.replace("@", "");
412 |
413 | result.uncertainties.push({
414 | area: "version-compatibility",
415 | severity: "medium",
416 | description: `Version ${version} mentioned in documentation`,
417 | potentialImpact: "May not match actual project dependencies",
418 | clarificationNeeded: "Verify version compatibility with project",
419 | fallbackStrategy: "Use generic version-agnostic examples",
420 | });
421 | }
422 | }
423 | }
424 |
425 | private async checkCommandAccuracy(
426 | filePath: string,
427 | content: string,
428 | result: ValidationResult,
429 | ): Promise<void> {
430 | const codeBlocks = this.extractCodeBlocks(content);
431 |
432 | for (const block of codeBlocks) {
433 | if (block.language === "bash" || block.language === "sh") {
434 | // Check for common command issues
435 | const commands = block.code
436 | .split("\n")
437 | .filter((line) => line.trim() && !line.startsWith("#"));
438 |
439 | for (const command of commands) {
440 | // Check for potentially dangerous commands
441 | const dangerousPatterns = [
442 | /rm -rf \//,
443 | /sudo rm/,
444 | /chmod 777/,
445 | /> \/dev\/null 2>&1/,
446 | ];
447 |
448 | for (const pattern of dangerousPatterns) {
449 | if (pattern.test(command)) {
450 | result.issues.push({
451 | type: "error",
452 | category: "accuracy",
453 | location: { file: path.basename(filePath) },
454 | description: "Potentially dangerous command in documentation",
455 | evidence: [command],
456 | suggestedFix: "Review and provide safer alternative",
457 | confidence: 95,
458 | });
459 | }
460 | }
461 |
462 | // Check for non-portable commands (Windows vs Unix)
463 | if (command.includes("\\") && command.includes("/")) {
464 | result.issues.push({
465 | type: "warning",
466 | category: "accuracy",
467 | location: { file: path.basename(filePath) },
468 | description: "Mixed path separators in command",
469 | evidence: [command],
470 | suggestedFix:
471 | "Use consistent path separators or provide OS-specific examples",
472 | confidence: 85,
473 | });
474 | }
475 | }
476 | }
477 | }
478 | }
479 |
480 | private async checkLinkValidity(
481 | filePath: string,
482 | content: string,
483 | result: ValidationResult,
484 | ): Promise<void> {
485 | const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
486 | const links: Array<{ text: string; url: string }> = [];
487 | let match;
488 |
489 | while ((match = linkPattern.exec(content)) !== null) {
490 | links.push({ text: match[1], url: match[2] });
491 | }
492 |
493 | for (const link of links) {
494 | // Check internal links
495 | if (!link.url.startsWith("http")) {
496 | const targetPath = path.resolve(path.dirname(filePath), link.url);
497 | try {
498 | await fs.access(targetPath);
499 | } catch {
500 | result.issues.push({
501 | type: "error",
502 | category: "accuracy",
503 | location: { file: path.basename(filePath) },
504 | description: `Broken internal link: ${link.url}`,
505 | evidence: [link.text],
506 | suggestedFix: "Fix the link path or create the missing file",
507 | confidence: 100,
508 | });
509 | }
510 | }
511 |
512 | // Flag external links for manual verification
513 | if (link.url.startsWith("http")) {
514 | result.uncertainties.push({
515 | area: "external-links",
516 | severity: "low",
517 | description: `External link: ${link.url}`,
518 | potentialImpact: "Link may become outdated or broken",
519 | clarificationNeeded: "Verify link is still valid",
520 | fallbackStrategy: "Archive important external content locally",
521 | });
522 | }
523 | }
524 | }
525 |
526 | private async validateCompleteness(
527 | contentPath: string,
528 | result: ValidationResult,
529 | ): Promise<void> {
530 | const files = await this.getMarkdownFiles(contentPath);
531 | const structure = await this.analyzeDiataxisStructure(contentPath);
532 |
533 | // Check for missing essential sections
534 | const requiredSections = [
535 | "tutorials",
536 | "how-to",
537 | "reference",
538 | "explanation",
539 | ];
540 | const missingSections = requiredSections.filter(
541 | (section) => !structure.sections.includes(section),
542 | );
543 |
544 | if (missingSections.length > 0) {
545 | result.issues.push({
546 | type: "warning",
547 | category: "completeness",
548 | location: { file: "documentation structure" },
549 | description: `Missing Diataxis sections: ${missingSections.join(", ")}`,
550 | evidence: structure.sections,
551 | suggestedFix:
552 | "Add missing Diataxis sections for complete documentation",
553 | confidence: 100,
554 | });
555 | }
556 |
557 | // Check content depth in each section
558 | for (const section of structure.sections) {
559 | const sectionFiles = files.filter((f) => f.includes(`/${section}/`));
560 | if (sectionFiles.length < 2) {
561 | result.issues.push({
562 | type: "info",
563 | category: "completeness",
564 | location: { file: section },
565 | description: `Limited content in ${section} section`,
566 | evidence: [`Only ${sectionFiles.length} files`],
567 | suggestedFix: "Consider adding more comprehensive coverage",
568 | confidence: 75,
569 | });
570 | }
571 | }
572 |
573 | // Update completeness confidence
574 | result.confidence.breakdown.businessContextAlignment = Math.max(
575 | 0,
576 | 100 - missingSections.length * 25,
577 | );
578 | }
579 |
580 | private async validateDiataxisCompliance(
581 | contentPath: string,
582 | result: ValidationResult,
583 | ): Promise<void> {
584 | const files = await this.getMarkdownFiles(contentPath);
585 |
586 | for (const file of files) {
587 | const content = await fs.readFile(file, "utf-8");
588 | const section = this.identifyDiataxisSection(file);
589 |
590 | if (section) {
591 | await this.checkSectionCompliance(file, content, section, result);
592 | }
593 | }
594 | }
595 |
596 | private async validateApplicationStructureCompliance(
597 | contentPath: string,
598 | result: ValidationResult,
599 | ): Promise<void> {
600 | // Analyze application source code for Diataxis compliance
601 | await this.validateSourceCodeDocumentation(contentPath, result);
602 | await this.validateApplicationArchitecture(contentPath, result);
603 | await this.validateInlineDocumentationPatterns(contentPath, result);
604 | }
605 |
606 | private async validateSourceCodeDocumentation(
607 | contentPath: string,
608 | result: ValidationResult,
609 | ): Promise<void> {
610 | const sourceFiles = await this.getSourceFiles(contentPath);
611 |
612 | for (const file of sourceFiles) {
613 | const content = await fs.readFile(file, "utf-8");
614 |
615 | // Check for proper JSDoc/TSDoc documentation
616 | await this.checkInlineDocumentationQuality(file, content, result);
617 |
618 | // Check for README files and their structure
619 | if (file.endsWith("README.md")) {
620 | await this.validateReadmeStructure(file, content, result);
621 | }
622 |
623 | // Check for proper module/class documentation
624 | await this.checkModuleDocumentation(file, content, result);
625 | }
626 | }
627 |
628 | private async validateApplicationArchitecture(
629 | contentPath: string,
630 | result: ValidationResult,
631 | ): Promise<void> {
632 | // Check if the application structure supports different types of documentation
633 | const hasToolsDir = await this.pathExists(path.join(contentPath, "tools"));
634 | const hasTypesDir = await this.pathExists(path.join(contentPath, "types"));
635 | // Check for workflows directory (currently not used but may be useful for future validation)
636 | // const hasWorkflowsDir = await this.pathExists(path.join(contentPath, 'workflows'));
637 |
638 | if (!hasToolsDir) {
639 | result.issues.push({
640 | type: "warning",
641 | category: "compliance",
642 | location: { file: "application structure" },
643 | description:
644 | "No dedicated tools directory found - may impact reference documentation organization",
645 | evidence: ["Missing /tools directory"],
646 | suggestedFix:
647 | "Organize tools into dedicated directory for better reference documentation",
648 | confidence: 80,
649 | });
650 | }
651 |
652 | if (!hasTypesDir) {
653 | result.issues.push({
654 | type: "info",
655 | category: "compliance",
656 | location: { file: "application structure" },
657 | description:
658 | "No types directory found - may impact API reference documentation",
659 | evidence: ["Missing /types directory"],
660 | suggestedFix: "Consider organizing types for better API documentation",
661 | confidence: 70,
662 | });
663 | }
664 | }
665 |
666 | private async validateInlineDocumentationPatterns(
667 | contentPath: string,
668 | result: ValidationResult,
669 | ): Promise<void> {
670 | const sourceFiles = await this.getSourceFiles(contentPath);
671 |
672 | for (const file of sourceFiles) {
673 | const content = await fs.readFile(file, "utf-8");
674 |
675 | // Check for proper function documentation that could support tutorials
676 | const functions = this.extractFunctions(content);
677 | for (const func of functions) {
678 | if (func.isExported && !func.hasDocumentation) {
679 | result.issues.push({
680 | type: "warning",
681 | category: "compliance",
682 | location: { file: path.basename(file), line: func.line },
683 | description: `Exported function '${func.name}' lacks documentation`,
684 | evidence: [func.signature],
685 | suggestedFix:
686 | "Add JSDoc/TSDoc documentation to support tutorial and reference content",
687 | confidence: 85,
688 | });
689 | }
690 | }
691 |
692 | // Check for proper error handling documentation
693 | const errorPatterns = content.match(/throw new \w*Error/g);
694 | if (errorPatterns && errorPatterns.length > 0) {
695 | const hasErrorDocs =
696 | content.includes("@throws") || content.includes("@error");
697 | if (!hasErrorDocs) {
698 | result.issues.push({
699 | type: "info",
700 | category: "compliance",
701 | location: { file: path.basename(file) },
702 | description:
703 | "Error throwing code found without error documentation",
704 | evidence: errorPatterns,
705 | suggestedFix:
706 | "Document error conditions to support troubleshooting guides",
707 | confidence: 75,
708 | });
709 | }
710 | }
711 | }
712 | }
713 |
714 | private identifyDiataxisSection(filePath: string): string | null {
715 | const sections = ["tutorials", "how-to", "reference", "explanation"];
716 |
717 | for (const section of sections) {
718 | if (filePath.includes(`/${section}/`)) {
719 | return section;
720 | }
721 | }
722 |
723 | return null;
724 | }
725 |
726 | private async checkSectionCompliance(
727 | filePath: string,
728 | content: string,
729 | section: string,
730 | result: ValidationResult,
731 | ): Promise<void> {
732 | const complianceRules = this.getDiataxisComplianceRules(section);
733 |
734 | for (const rule of complianceRules) {
735 | if (!rule.check(content)) {
736 | result.issues.push({
737 | type: "warning",
738 | category: "compliance",
739 | location: { file: path.basename(filePath), section },
740 | description: rule.message,
741 | evidence: [rule.evidence?.(content) || ""],
742 | suggestedFix: rule.fix,
743 | confidence: rule.confidence,
744 | });
745 | }
746 | }
747 | }
748 |
749 | private getDiataxisComplianceRules(section: string) {
750 | const rules: any = {
751 | tutorials: [
752 | {
753 | check: (content: string) =>
754 | content.includes("## Prerequisites") ||
755 | content.includes("## Requirements"),
756 | message: "Tutorial should include prerequisites section",
757 | fix: "Add a prerequisites or requirements section",
758 | confidence: 90,
759 | },
760 | {
761 | check: (content: string) => /step|Step|STEP/.test(content),
762 | message: "Tutorial should be organized in clear steps",
763 | fix: "Structure content with numbered steps or clear progression",
764 | confidence: 85,
765 | },
766 | {
767 | check: (content: string) => content.includes("```"),
768 | message: "Tutorial should include practical code examples",
769 | fix: "Add code blocks with working examples",
770 | confidence: 80,
771 | },
772 | ],
773 | "how-to": [
774 | {
775 | check: (content: string) => /how to|How to|HOW TO/.test(content),
776 | message: "How-to guide should focus on specific tasks",
777 | fix: "Frame content around achieving specific goals",
778 | confidence: 75,
779 | },
780 | {
781 | check: (content: string) => content.length > 500,
782 | message: "How-to guide should provide detailed guidance",
783 | fix: "Expand with more detailed instructions",
784 | confidence: 70,
785 | },
786 | ],
787 | reference: [
788 | {
789 | check: (content: string) => /##|###/.test(content),
790 | message: "Reference should be well-structured with clear sections",
791 | fix: "Add proper headings and organization",
792 | confidence: 95,
793 | },
794 | {
795 | check: (content: string) => /\|.*\|/.test(content),
796 | message: "Reference should include tables for structured information",
797 | fix: "Consider using tables for parameters, options, etc.",
798 | confidence: 60,
799 | },
800 | ],
801 | explanation: [
802 | {
803 | check: (content: string) =>
804 | content.includes("why") || content.includes("Why"),
805 | message: 'Explanation should address the "why" behind concepts',
806 | fix: "Include rationale and context for decisions/concepts",
807 | confidence: 80,
808 | },
809 | {
810 | check: (content: string) => content.length > 800,
811 | message: "Explanation should provide in-depth coverage",
812 | fix: "Expand with more comprehensive explanation",
813 | confidence: 70,
814 | },
815 | ],
816 | };
817 |
818 | return rules[section] || [];
819 | }
820 |
821 | private async validateCodeExamples(
822 | contentPath: string,
823 | ): Promise<CodeValidationResult> {
824 | const files = await this.getMarkdownFiles(contentPath);
825 | const allExamples: ExampleValidation[] = [];
826 |
827 | for (const file of files) {
828 | const content = await fs.readFile(file, "utf-8");
829 | const codeBlocks = this.extractCodeBlocks(content);
830 |
831 | for (const block of codeBlocks) {
832 | if (this.isValidatableLanguage(block.language)) {
833 | const validation = await this.validateCodeBlock(block, file);
834 | allExamples.push(validation);
835 | }
836 | }
837 | }
838 |
839 | return {
840 | overallSuccess: allExamples.every((e) => e.compilationSuccess),
841 | exampleResults: allExamples,
842 | confidence: this.calculateCodeValidationConfidence(allExamples),
843 | };
844 | }
845 |
846 | private extractCodeBlocks(
847 | content: string,
848 | ): Array<{ language: string; code: string; id: string }> {
849 | const codeBlockPattern = /```(\w+)?\n([\s\S]*?)```/g;
850 | const blocks: Array<{ language: string; code: string; id: string }> = [];
851 | let match;
852 | let index = 0;
853 |
854 | while ((match = codeBlockPattern.exec(content)) !== null) {
855 | blocks.push({
856 | language: match[1] || "text",
857 | code: match[2].trim(),
858 | id: `block-${index++}`,
859 | });
860 | }
861 |
862 | return blocks;
863 | }
864 |
865 | private isValidatableLanguage(language: string): boolean {
866 | const validatable = [
867 | "javascript",
868 | "typescript",
869 | "js",
870 | "ts",
871 | "json",
872 | "bash",
873 | "sh",
874 | ];
875 | return validatable.includes(language.toLowerCase());
876 | }
877 |
878 | private async validateCodeBlock(
879 | block: { language: string; code: string; id: string },
880 | filePath: string,
881 | ): Promise<ExampleValidation> {
882 | const validation: ExampleValidation = {
883 | example: block.id,
884 | compilationSuccess: false,
885 | executionSuccess: false,
886 | issues: [],
887 | confidence: 0,
888 | };
889 |
890 | try {
891 | if (block.language === "typescript" || block.language === "ts") {
892 | await this.validateTypeScriptCode(block.code, validation);
893 |
894 | // Use execution simulation for additional validation if available
895 | if (
896 | this.useExecutionSimulation &&
897 | this.executionSimulator &&
898 | validation.compilationSuccess
899 | ) {
900 | await this.enhanceWithExecutionSimulation(
901 | block.code,
902 | validation,
903 | filePath,
904 | );
905 | }
906 | } else if (block.language === "javascript" || block.language === "js") {
907 | await this.validateJavaScriptCode(block.code, validation);
908 |
909 | // Use execution simulation for additional validation if available
910 | if (
911 | this.useExecutionSimulation &&
912 | this.executionSimulator &&
913 | validation.compilationSuccess
914 | ) {
915 | await this.enhanceWithExecutionSimulation(
916 | block.code,
917 | validation,
918 | filePath,
919 | );
920 | }
921 | } else if (block.language === "json") {
922 | await this.validateJSONCode(block.code, validation);
923 | } else if (block.language === "bash" || block.language === "sh") {
924 | await this.validateBashCode(block.code, validation);
925 | }
926 | } catch (error: any) {
927 | validation.issues.push({
928 | type: "error",
929 | category: "accuracy",
930 | location: { file: path.basename(filePath) },
931 | description: `Code validation failed: ${error.message}`,
932 | evidence: [block.code.substring(0, 100)],
933 | suggestedFix: "Review and fix syntax errors",
934 | confidence: 95,
935 | });
936 | }
937 |
938 | return validation;
939 | }
940 |
941 | /**
942 | * Enhance validation with execution simulation results
943 | */
944 | private async enhanceWithExecutionSimulation(
945 | code: string,
946 | validation: ExampleValidation,
947 | filePath: string,
948 | ): Promise<void> {
949 | if (!this.executionSimulator) return;
950 |
951 | try {
952 | const simResult = await this.executionSimulator.validateExample(
953 | code,
954 | code,
955 | );
956 |
957 | // Add simulation-detected issues to validation
958 | for (const issue of simResult.issues) {
959 | validation.issues.push({
960 | type: issue.severity === "error" ? "error" : "warning",
961 | category: "accuracy",
962 | location: {
963 | file: path.basename(filePath),
964 | line: issue.location.line,
965 | },
966 | description: `[Simulation] ${issue.description}`,
967 | evidence: [issue.codeSnippet || ""],
968 | suggestedFix: issue.suggestion,
969 | confidence: Math.round(simResult.trace.confidenceScore * 100),
970 | });
971 | }
972 |
973 | // Update execution success based on simulation
974 | validation.executionSuccess = simResult.isValid;
975 |
976 | // Adjust confidence based on simulation confidence
977 | if (simResult.trace.confidenceScore > 0) {
978 | validation.confidence = Math.round(
979 | (validation.confidence + simResult.trace.confidenceScore * 100) / 2,
980 | );
981 | }
982 | } catch (error) {
983 | // Simulation is best-effort, don't fail validation on simulation errors
984 | console.warn("Execution simulation failed:", error);
985 | }
986 | }
987 |
988 | private async validateTypeScriptCode(
989 | code: string,
990 | validation: ExampleValidation,
991 | ): Promise<void> {
992 | // Ensure temp directory exists
993 | await fs.mkdir(this.tempDir, { recursive: true });
994 |
995 | const tempFile = path.join(this.tempDir, `temp-${Date.now()}.ts`);
996 |
997 | try {
998 | // Write code to temporary file
999 | await fs.writeFile(tempFile, code, "utf-8");
1000 |
1001 | // Try to compile with TypeScript
1002 | const { stderr } = await execAsync(
1003 | `npx tsc --noEmit --skipLibCheck ${tempFile}`,
1004 | );
1005 |
1006 | if (stderr && stderr.includes("error")) {
1007 | validation.issues.push({
1008 | type: "error",
1009 | category: "accuracy",
1010 | location: { file: "code-example" },
1011 | description: "TypeScript compilation error",
1012 | evidence: [stderr],
1013 | suggestedFix: "Fix TypeScript syntax and type errors",
1014 | confidence: 90,
1015 | });
1016 | } else {
1017 | validation.compilationSuccess = true;
1018 | validation.confidence = 85;
1019 | }
1020 | } catch (error: any) {
1021 | if (error.stderr && error.stderr.includes("error")) {
1022 | validation.issues.push({
1023 | type: "error",
1024 | category: "accuracy",
1025 | location: { file: "code-example" },
1026 | description: "TypeScript compilation failed",
1027 | evidence: [error.stderr],
1028 | suggestedFix: "Fix compilation errors",
1029 | confidence: 95,
1030 | });
1031 | }
1032 | } finally {
1033 | // Clean up temp file
1034 | try {
1035 | await fs.unlink(tempFile);
1036 | } catch {
1037 | // Ignore cleanup errors
1038 | }
1039 | }
1040 | }
1041 |
1042 | private async validateJavaScriptCode(
1043 | code: string,
1044 | validation: ExampleValidation,
1045 | ): Promise<void> {
1046 | try {
1047 | // Basic syntax check using Node.js
1048 | new Function(code);
1049 | validation.compilationSuccess = true;
1050 | validation.confidence = 75;
1051 | } catch (error: any) {
1052 | validation.issues.push({
1053 | type: "error",
1054 | category: "accuracy",
1055 | location: { file: "code-example" },
1056 | description: `JavaScript syntax error: ${error.message}`,
1057 | evidence: [code.substring(0, 100)],
1058 | suggestedFix: "Fix JavaScript syntax errors",
1059 | confidence: 90,
1060 | });
1061 | }
1062 | }
1063 |
1064 | private async validateJSONCode(
1065 | code: string,
1066 | validation: ExampleValidation,
1067 | ): Promise<void> {
1068 | try {
1069 | JSON.parse(code);
1070 | validation.compilationSuccess = true;
1071 | validation.confidence = 95;
1072 | } catch (error: any) {
1073 | validation.issues.push({
1074 | type: "error",
1075 | category: "accuracy",
1076 | location: { file: "code-example" },
1077 | description: `Invalid JSON: ${error.message}`,
1078 | evidence: [code.substring(0, 100)],
1079 | suggestedFix: "Fix JSON syntax errors",
1080 | confidence: 100,
1081 | });
1082 | }
1083 | }
1084 |
1085 | private async validateBashCode(
1086 | code: string,
1087 | validation: ExampleValidation,
1088 | ): Promise<void> {
1089 | // Basic bash syntax validation
1090 | const lines = code
1091 | .split("\n")
1092 | .filter((line) => line.trim() && !line.startsWith("#"));
1093 |
1094 | for (const line of lines) {
1095 | // Check for basic syntax issues
1096 | if (line.includes("&&") && line.includes("||")) {
1097 | validation.issues.push({
1098 | type: "warning",
1099 | category: "accuracy",
1100 | location: { file: "code-example" },
1101 | description: "Complex command chaining may be confusing",
1102 | evidence: [line],
1103 | suggestedFix:
1104 | "Consider breaking into separate commands or adding explanation",
1105 | confidence: 60,
1106 | });
1107 | }
1108 |
1109 | // Check for unquoted variables in dangerous contexts
1110 | if (line.includes("rm") && /\$\w+/.test(line) && !/'.*\$.*'/.test(line)) {
1111 | validation.issues.push({
1112 | type: "warning",
1113 | category: "accuracy",
1114 | location: { file: "code-example" },
1115 | description: "Unquoted variable in potentially dangerous command",
1116 | evidence: [line],
1117 | suggestedFix: "Quote variables to prevent word splitting",
1118 | confidence: 80,
1119 | });
1120 | }
1121 | }
1122 |
1123 | validation.compilationSuccess =
1124 | validation.issues.filter((i) => i.type === "error").length === 0;
1125 | validation.confidence = validation.compilationSuccess ? 70 : 20;
1126 | }
1127 |
1128 | private calculateCodeValidationConfidence(
1129 | examples: ExampleValidation[],
1130 | ): number {
1131 | if (examples.length === 0) return 0;
1132 |
1133 | const totalConfidence = examples.reduce(
1134 | (sum, ex) => sum + ex.confidence,
1135 | 0,
1136 | );
1137 | return Math.round(totalConfidence / examples.length);
1138 | }
1139 |
1140 | public async getMarkdownFiles(
1141 | contentPath: string,
1142 | maxDepth: number = 5,
1143 | ): Promise<string[]> {
1144 | const files: string[] = [];
1145 | const excludedDirs = new Set([
1146 | "node_modules",
1147 | ".git",
1148 | "dist",
1149 | "build",
1150 | ".next",
1151 | ".nuxt",
1152 | "coverage",
1153 | ".tmp",
1154 | "tmp",
1155 | ".cache",
1156 | ".vscode",
1157 | ".idea",
1158 | "logs",
1159 | ".logs",
1160 | ".npm",
1161 | ".yarn",
1162 | ]);
1163 |
1164 | const scan = async (
1165 | dir: string,
1166 | currentDepth: number = 0,
1167 | ): Promise<void> => {
1168 | if (currentDepth > maxDepth) return;
1169 |
1170 | try {
1171 | const entries = await fs.readdir(dir, { withFileTypes: true });
1172 |
1173 | for (const entry of entries) {
1174 | const fullPath = path.join(dir, entry.name);
1175 |
1176 | if (entry.isDirectory()) {
1177 | // Skip excluded directories
1178 | if (excludedDirs.has(entry.name) || entry.name.startsWith(".")) {
1179 | continue;
1180 | }
1181 |
1182 | // Prevent symlink loops
1183 | try {
1184 | const stats = await fs.lstat(fullPath);
1185 | if (stats.isSymbolicLink()) {
1186 | continue;
1187 | }
1188 | } catch {
1189 | continue;
1190 | }
1191 |
1192 | await scan(fullPath, currentDepth + 1);
1193 | } else if (entry.name.endsWith(".md")) {
1194 | files.push(fullPath);
1195 |
1196 | // Limit total files to prevent memory issues
1197 | if (files.length > 500) {
1198 | console.warn("Markdown file limit reached (500), stopping scan");
1199 | return;
1200 | }
1201 | }
1202 | }
1203 | } catch (error) {
1204 | // Skip directories that can't be read
1205 | console.warn(`Warning: Could not read directory ${dir}:`, error);
1206 | }
1207 | };
1208 |
1209 | try {
1210 | await scan(contentPath);
1211 | } catch (error) {
1212 | console.warn("Error scanning directory:", error);
1213 | }
1214 |
1215 | return files;
1216 | }
1217 |
1218 | private async getSourceFiles(
1219 | contentPath: string,
1220 | maxDepth: number = 5,
1221 | ): Promise<string[]> {
1222 | const files: string[] = [];
1223 | const excludedDirs = new Set([
1224 | "node_modules",
1225 | ".git",
1226 | "dist",
1227 | "build",
1228 | ".next",
1229 | ".nuxt",
1230 | "coverage",
1231 | ".tmp",
1232 | "tmp",
1233 | ".cache",
1234 | ".vscode",
1235 | ".idea",
1236 | "logs",
1237 | ".logs",
1238 | ".npm",
1239 | ".yarn",
1240 | ]);
1241 |
1242 | const scan = async (
1243 | dir: string,
1244 | currentDepth: number = 0,
1245 | ): Promise<void> => {
1246 | if (currentDepth > maxDepth) return;
1247 |
1248 | try {
1249 | const entries = await fs.readdir(dir, { withFileTypes: true });
1250 |
1251 | for (const entry of entries) {
1252 | const fullPath = path.join(dir, entry.name);
1253 |
1254 | if (entry.isDirectory()) {
1255 | // Skip excluded directories
1256 | if (excludedDirs.has(entry.name) || entry.name.startsWith(".")) {
1257 | continue;
1258 | }
1259 |
1260 | // Prevent symlink loops
1261 | try {
1262 | const stats = await fs.lstat(fullPath);
1263 | if (stats.isSymbolicLink()) {
1264 | continue;
1265 | }
1266 | } catch {
1267 | continue;
1268 | }
1269 |
1270 | await scan(fullPath, currentDepth + 1);
1271 | } else if (
1272 | entry.name.endsWith(".ts") ||
1273 | entry.name.endsWith(".js") ||
1274 | entry.name.endsWith(".md")
1275 | ) {
1276 | files.push(fullPath);
1277 |
1278 | // Limit total files to prevent memory issues
1279 | if (files.length > 1000) {
1280 | console.warn("File limit reached (1000), stopping scan");
1281 | return;
1282 | }
1283 | }
1284 | }
1285 | } catch (error) {
1286 | // Skip directories that can't be read
1287 | console.warn(`Warning: Could not read directory ${dir}:`, error);
1288 | }
1289 | };
1290 |
1291 | try {
1292 | await scan(contentPath);
1293 | } catch (error) {
1294 | console.warn("Error scanning directory:", error);
1295 | }
1296 |
1297 | return files;
1298 | }
1299 |
1300 | private async pathExists(filePath: string): Promise<boolean> {
1301 | try {
1302 | await fs.access(filePath);
1303 | return true;
1304 | } catch {
1305 | return false;
1306 | }
1307 | }
1308 |
1309 | private extractFunctions(content: string): Array<{
1310 | name: string;
1311 | line: number;
1312 | signature: string;
1313 | isExported: boolean;
1314 | hasDocumentation: boolean;
1315 | }> {
1316 | const functions: Array<{
1317 | name: string;
1318 | line: number;
1319 | signature: string;
1320 | isExported: boolean;
1321 | hasDocumentation: boolean;
1322 | }> = [];
1323 | const lines = content.split("\n");
1324 |
1325 | for (let i = 0; i < lines.length; i++) {
1326 | const line = lines[i];
1327 |
1328 | // Match function declarations and exports
1329 | const functionMatch = line.match(
1330 | /^(export\s+)?(async\s+)?function\s+(\w+)/,
1331 | );
1332 | const arrowMatch = line.match(
1333 | /^(export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(/,
1334 | );
1335 |
1336 | if (functionMatch) {
1337 | const name = functionMatch[3];
1338 | const isExported = !!functionMatch[1];
1339 | const hasDocumentation = this.checkForDocumentation(lines, i);
1340 |
1341 | functions.push({
1342 | name,
1343 | line: i + 1,
1344 | signature: line.trim(),
1345 | isExported,
1346 | hasDocumentation,
1347 | });
1348 | } else if (arrowMatch) {
1349 | const name = arrowMatch[2];
1350 | const isExported = !!arrowMatch[1];
1351 | const hasDocumentation = this.checkForDocumentation(lines, i);
1352 |
1353 | functions.push({
1354 | name,
1355 | line: i + 1,
1356 | signature: line.trim(),
1357 | isExported,
1358 | hasDocumentation,
1359 | });
1360 | }
1361 | }
1362 |
1363 | return functions;
1364 | }
1365 |
1366 | private async checkInlineDocumentationQuality(
1367 | _file: string,
1368 | _content: string,
1369 | _result: ValidationResult,
1370 | ): Promise<void> {
1371 | // Implementation for checking JSDoc/TSDoc quality
1372 | // This could check for proper parameter documentation, return types, etc.
1373 | }
1374 |
1375 | private async validateReadmeStructure(
1376 | _file: string,
1377 | content: string,
1378 | result: ValidationResult,
1379 | ): Promise<void> {
1380 | // Check if README follows good structure
1381 | const hasTitle = /^#\s+/.test(content);
1382 | const hasDescription =
1383 | content.includes("## Description") || content.includes("## Overview");
1384 | const hasInstallation =
1385 | content.includes("## Installation") || content.includes("## Setup");
1386 | const hasUsage =
1387 | content.includes("## Usage") || content.includes("## Getting Started");
1388 |
1389 | if (!hasTitle) {
1390 | result.issues.push({
1391 | type: "warning",
1392 | category: "compliance",
1393 | location: { file: "README.md" },
1394 | description: "README missing clear title",
1395 | evidence: ["No H1 heading found"],
1396 | suggestedFix: "Add clear title with # heading",
1397 | confidence: 90,
1398 | });
1399 | }
1400 |
1401 | if (!hasDescription && !hasInstallation && !hasUsage) {
1402 | result.issues.push({
1403 | type: "warning",
1404 | category: "compliance",
1405 | location: { file: "README.md" },
1406 | description:
1407 | "README lacks essential sections (description, installation, usage)",
1408 | evidence: ["Missing standard README sections"],
1409 | suggestedFix:
1410 | "Add sections for description, installation, and usage following Diataxis principles",
1411 | confidence: 85,
1412 | });
1413 | }
1414 | }
1415 |
1416 | private async checkModuleDocumentation(
1417 | _file: string,
1418 | _content: string,
1419 | _result: ValidationResult,
1420 | ): Promise<void> {
1421 | // Implementation for checking module-level documentation
1422 | // This could check for file-level JSDoc, proper exports documentation, etc.
1423 | }
1424 |
1425 | private checkForDocumentation(
1426 | lines: string[],
1427 | functionLineIndex: number,
1428 | ): boolean {
1429 | // Look backwards from the function line to find documentation
1430 | let checkIndex = functionLineIndex - 1;
1431 |
1432 | // Skip empty lines
1433 | while (checkIndex >= 0 && lines[checkIndex].trim() === "") {
1434 | checkIndex--;
1435 | }
1436 |
1437 | // Check if we found the end of a JSDoc comment
1438 | if (checkIndex >= 0 && lines[checkIndex].trim() === "*/") {
1439 | // Look backwards to find the start of the JSDoc block
1440 | let jsDocStart = checkIndex;
1441 | while (jsDocStart >= 0) {
1442 | const line = lines[jsDocStart].trim();
1443 | if (line.startsWith("/**")) {
1444 | return true; // Found complete JSDoc block
1445 | }
1446 | if (!line.startsWith("*") && line !== "*/") {
1447 | break; // Not part of JSDoc block
1448 | }
1449 | jsDocStart--;
1450 | }
1451 | }
1452 |
1453 | // Also check for single-line JSDoc comments
1454 | if (
1455 | checkIndex >= 0 &&
1456 | lines[checkIndex].trim().startsWith("/**") &&
1457 | lines[checkIndex].includes("*/")
1458 | ) {
1459 | return true;
1460 | }
1461 |
1462 | return false;
1463 | }
1464 |
1465 | private async shouldAnalyzeApplicationCode(
1466 | contentPath: string,
1467 | ): Promise<boolean> {
1468 | // Check if the path contains application source code vs documentation
1469 | const hasSrcDir = await this.pathExists(path.join(contentPath, "src"));
1470 | const hasPackageJson = await this.pathExists(
1471 | path.join(contentPath, "package.json"),
1472 | );
1473 | const hasTypescriptFiles = (await this.getSourceFiles(contentPath)).some(
1474 | (file) => file.endsWith(".ts"),
1475 | );
1476 |
1477 | // If path ends with 'src' or is a project root with src/, analyze as application code
1478 | if (
1479 | contentPath.endsWith("/src") ||
1480 | contentPath.endsWith("\\src") ||
1481 | (hasSrcDir && hasPackageJson)
1482 | ) {
1483 | return true;
1484 | }
1485 |
1486 | // If path contains TypeScript/JavaScript files and package.json, treat as application code
1487 | if (hasTypescriptFiles && hasPackageJson) {
1488 | return true;
1489 | }
1490 |
1491 | // If path is specifically a documentation directory, analyze as documentation
1492 | if (contentPath.includes("/docs") || contentPath.includes("\\docs")) {
1493 | return false;
1494 | }
1495 |
1496 | return false;
1497 | }
1498 |
1499 | private async analyzeDiataxisStructure(
1500 | contentPath: string,
1501 | ): Promise<{ sections: string[] }> {
1502 | const sections: string[] = [];
1503 |
1504 | try {
1505 | const entries = await fs.readdir(contentPath, { withFileTypes: true });
1506 |
1507 | for (const entry of entries) {
1508 | if (entry.isDirectory()) {
1509 | const dirName = entry.name;
1510 | if (
1511 | ["tutorials", "how-to", "reference", "explanation"].includes(
1512 | dirName,
1513 | )
1514 | ) {
1515 | sections.push(dirName);
1516 | }
1517 | }
1518 | }
1519 | } catch {
1520 | // Directory doesn't exist
1521 | }
1522 |
1523 | return { sections };
1524 | }
1525 |
1526 | private updateAccuracyConfidence(result: ValidationResult): void {
1527 | const errorCount = result.issues.filter((i) => i.type === "error").length;
1528 | const warningCount = result.issues.filter(
1529 | (i) => i.type === "warning",
1530 | ).length;
1531 |
1532 | // Base confidence starts high and decreases with issues
1533 | let confidence = 95;
1534 | confidence -= errorCount * 20;
1535 | confidence -= warningCount * 5;
1536 | confidence = Math.max(0, confidence);
1537 |
1538 | result.confidence.breakdown.technologyDetection = confidence;
1539 | }
1540 |
1541 | private calculateOverallMetrics(result: ValidationResult): void {
1542 | const breakdown = result.confidence.breakdown;
1543 | const values = Object.values(breakdown).filter((v) => v > 0);
1544 |
1545 | if (values.length > 0) {
1546 | result.confidence.overall = Math.round(
1547 | values.reduce((a, b) => a + b, 0) / values.length,
1548 | );
1549 | }
1550 |
1551 | // Determine overall success
1552 | const criticalIssues = result.issues.filter(
1553 | (i) => i.type === "error",
1554 | ).length;
1555 | result.success = criticalIssues === 0;
1556 |
1557 | // Add risk factors based on issues
1558 | if (criticalIssues > 0) {
1559 | result.confidence.riskFactors.push({
1560 | type: "high",
1561 | category: "accuracy",
1562 | description: `${criticalIssues} critical accuracy issues found`,
1563 | impact: "Users may encounter broken examples or incorrect information",
1564 | mitigation: "Fix all critical issues before publication",
1565 | });
1566 | }
1567 |
1568 | const uncertaintyCount = result.uncertainties.length;
1569 | if (uncertaintyCount > 5) {
1570 | result.confidence.riskFactors.push({
1571 | type: "medium",
1572 | category: "completeness",
1573 | description: `${uncertaintyCount} areas requiring clarification`,
1574 | impact: "Documentation may lack specificity for user context",
1575 | mitigation: "Address high-priority uncertainties with user input",
1576 | });
1577 | }
1578 | }
1579 |
1580 | private generateRecommendations(
1581 | result: ValidationResult,
1582 | _options: ValidationOptions,
1583 | ): void {
1584 | const recommendations: string[] = [];
1585 | const nextSteps: string[] = [];
1586 |
1587 | // Generate recommendations based on issues found
1588 | const errorCount = result.issues.filter((i) => i.type === "error").length;
1589 | if (errorCount > 0) {
1590 | recommendations.push(
1591 | `Fix ${errorCount} critical accuracy issues before publication`,
1592 | );
1593 | nextSteps.push("Review and resolve all error-level validation issues");
1594 | }
1595 |
1596 | const warningCount = result.issues.filter(
1597 | (i) => i.type === "warning",
1598 | ).length;
1599 | if (warningCount > 0) {
1600 | recommendations.push(
1601 | `Address ${warningCount} potential accuracy concerns`,
1602 | );
1603 | nextSteps.push(
1604 | "Review warning-level issues and apply fixes where appropriate",
1605 | );
1606 | }
1607 |
1608 | const uncertaintyCount = result.uncertainties.filter(
1609 | (u) => u.severity === "high" || u.severity === "critical",
1610 | ).length;
1611 | if (uncertaintyCount > 0) {
1612 | recommendations.push(
1613 | `Clarify ${uncertaintyCount} high-uncertainty areas`,
1614 | );
1615 | nextSteps.push("Gather user input on areas flagged for clarification");
1616 | }
1617 |
1618 | // Code validation recommendations
1619 | if (result.codeValidation && !result.codeValidation.overallSuccess) {
1620 | recommendations.push(
1621 | "Fix code examples that fail compilation or execution tests",
1622 | );
1623 | nextSteps.push(
1624 | "Test all code examples in appropriate development environment",
1625 | );
1626 | }
1627 |
1628 | // Completeness recommendations
1629 | const missingCompliance = result.issues.filter(
1630 | (i) => i.category === "compliance",
1631 | ).length;
1632 | if (missingCompliance > 0) {
1633 | recommendations.push(
1634 | "Improve Diataxis framework compliance for better user experience",
1635 | );
1636 | nextSteps.push(
1637 | "Restructure content to better align with Diataxis principles",
1638 | );
1639 | }
1640 |
1641 | // General recommendations based on confidence level
1642 | if (result.confidence.overall < 70) {
1643 | recommendations.push(
1644 | "Overall confidence is below recommended threshold - consider comprehensive review",
1645 | );
1646 | nextSteps.push(
1647 | "Conduct manual review of generated content before publication",
1648 | );
1649 | }
1650 |
1651 | if (recommendations.length === 0) {
1652 | recommendations.push("Content validation passed - ready for publication");
1653 | nextSteps.push("Deploy documentation and monitor for user feedback");
1654 | }
1655 |
1656 | result.recommendations = recommendations;
1657 | result.nextSteps = nextSteps;
1658 | }
1659 | }
1660 |
1661 | export const validateDiataxisContent: Tool = {
1662 | name: "validate_diataxis_content",
1663 | description:
1664 | "Validate the accuracy, completeness, and compliance of generated Diataxis documentation",
1665 | inputSchema: {
1666 | type: "object",
1667 | properties: {
1668 | contentPath: {
1669 | type: "string",
1670 | description: "Path to the documentation directory to validate",
1671 | },
1672 | analysisId: {
1673 | type: "string",
1674 | description:
1675 | "Optional repository analysis ID for context-aware validation",
1676 | },
1677 | validationType: {
1678 | type: "string",
1679 | enum: ["accuracy", "completeness", "compliance", "all"],
1680 | default: "all",
1681 | description: "Type of validation to perform",
1682 | },
1683 | includeCodeValidation: {
1684 | type: "boolean",
1685 | default: true,
1686 | description: "Whether to validate code examples for correctness",
1687 | },
1688 | confidence: {
1689 | type: "string",
1690 | enum: ["strict", "moderate", "permissive"],
1691 | default: "moderate",
1692 | description:
1693 | "Validation confidence level - stricter levels catch more issues",
1694 | },
1695 | },
1696 | required: ["contentPath"],
1697 | },
1698 | };
1699 |
1700 | /**
1701 | * Validates Diataxis-compliant documentation content for accuracy, completeness, and compliance.
1702 | *
1703 | * Performs comprehensive validation of documentation content including accuracy verification,
1704 | * completeness assessment, compliance checking, and code example validation. Uses advanced
1705 | * confidence scoring and risk assessment to provide detailed validation results with
1706 | * actionable recommendations.
1707 | *
1708 | * @param args - The validation parameters
1709 | * @param args.contentPath - Path to the documentation content directory
1710 | * @param args.analysisId - Optional repository analysis ID for context-aware validation
1711 | * @param args.validationType - Type of validation to perform: "accuracy", "completeness", "compliance", or "all"
1712 | * @param args.includeCodeValidation - Whether to validate code examples and syntax
1713 | * @param args.confidence - Validation confidence level: "strict", "moderate", or "permissive"
1714 | *
1715 | * @returns Promise resolving to comprehensive validation results
1716 | * @returns success - Whether validation passed overall
1717 | * @returns confidence - Confidence metrics and risk assessment
1718 | * @returns issues - Array of validation issues found
1719 | * @returns uncertainties - Areas requiring clarification
1720 | * @returns codeValidation - Code example validation results
1721 | * @returns recommendations - Suggested improvements
1722 | * @returns nextSteps - Recommended next actions
1723 | *
1724 | * @throws {Error} When content path is inaccessible
1725 | * @throws {Error} When validation processing fails
1726 | *
1727 | * @example
1728 | * ```typescript
1729 | * // Comprehensive validation
1730 | * const result = await handleValidateDiataxisContent({
1731 | * contentPath: "./docs",
1732 | * validationType: "all",
1733 | * includeCodeValidation: true,
1734 | * confidence: "moderate"
1735 | * });
1736 | *
1737 | * console.log(`Validation success: ${result.success}`);
1738 | * console.log(`Overall confidence: ${result.confidence.overall}%`);
1739 | * console.log(`Issues found: ${result.issues.length}`);
1740 | *
1741 | * // Strict accuracy validation
1742 | * const accuracy = await handleValidateDiataxisContent({
1743 | * contentPath: "./docs",
1744 | * validationType: "accuracy",
1745 | * confidence: "strict"
1746 | * });
1747 | * ```
1748 | *
1749 | * @since 1.0.0
1750 | */
1751 | export async function handleValidateDiataxisContent(
1752 | args: any,
1753 | context?: any,
1754 | ): Promise<ValidationResult> {
1755 | await context?.info?.("🔍 Starting Diataxis content validation...");
1756 |
1757 | const validator = new ContentAccuracyValidator();
1758 |
1759 | // Add timeout protection to prevent infinite hangs
1760 | const timeoutMs = 120000; // 2 minutes
1761 | let timeoutHandle: NodeJS.Timeout;
1762 | const timeoutPromise = new Promise<ValidationResult>((_, reject) => {
1763 | timeoutHandle = setTimeout(() => {
1764 | reject(
1765 | new Error(
1766 | `Validation timed out after ${
1767 | timeoutMs / 1000
1768 | } seconds. This may be due to a large directory structure. Try validating a smaller subset or specific directory.`,
1769 | ),
1770 | );
1771 | }, timeoutMs);
1772 | });
1773 |
1774 | const validationPromise = validator.validateContent(args, context);
1775 |
1776 | try {
1777 | const result = await Promise.race([validationPromise, timeoutPromise]);
1778 | clearTimeout(timeoutHandle!);
1779 | return result;
1780 | } catch (error: any) {
1781 | clearTimeout(timeoutHandle!);
1782 | // Return a partial result with error information
1783 | return {
1784 | success: false,
1785 | confidence: {
1786 | overall: 0,
1787 | breakdown: {
1788 | technologyDetection: 0,
1789 | frameworkVersionAccuracy: 0,
1790 | codeExampleRelevance: 0,
1791 | architecturalAssumptions: 0,
1792 | businessContextAlignment: 0,
1793 | },
1794 | riskFactors: [
1795 | {
1796 | type: "high",
1797 | category: "validation",
1798 | description: "Validation process failed or timed out",
1799 | impact: "Unable to complete content validation",
1800 | mitigation:
1801 | "Try validating a smaller directory or specific subset of files",
1802 | },
1803 | ],
1804 | },
1805 | issues: [],
1806 | uncertainties: [],
1807 | recommendations: [
1808 | "Validation failed or timed out",
1809 | "Consider validating smaller directory subsets",
1810 | "Check for very large files or deep directory structures",
1811 | `Error: ${error.message}`,
1812 | ],
1813 | nextSteps: [
1814 | "Verify the content path is correct and accessible",
1815 | "Try validating specific subdirectories instead of the entire project",
1816 | "Check for circular symlinks or very deep directory structures",
1817 | ],
1818 | };
1819 | }
1820 | }
1821 |
1822 | interface GeneralValidationResult {
1823 | success: boolean;
1824 | linksChecked: number;
1825 | brokenLinks: string[];
1826 | codeBlocksValidated: number;
1827 | codeErrors: string[];
1828 | recommendations: string[];
1829 | summary: string;
1830 | }
1831 |
1832 | export async function validateGeneralContent(
1833 | args: any,
1834 | ): Promise<GeneralValidationResult> {
1835 | const {
1836 | contentPath,
1837 | validationType = "all",
1838 | includeCodeValidation = true,
1839 | followExternalLinks = false,
1840 | } = args;
1841 |
1842 | const result: GeneralValidationResult = {
1843 | success: true,
1844 | linksChecked: 0,
1845 | brokenLinks: [],
1846 | codeBlocksValidated: 0,
1847 | codeErrors: [],
1848 | recommendations: [],
1849 | summary: "",
1850 | };
1851 |
1852 | try {
1853 | // Get all markdown files
1854 | const validator = new ContentAccuracyValidator();
1855 | const files = await validator.getMarkdownFiles(contentPath);
1856 |
1857 | // Check links if requested
1858 | if (validationType === "all" || validationType === "links") {
1859 | for (const file of files) {
1860 | const content = await fs.readFile(file, "utf-8");
1861 | const links = extractLinksFromMarkdown(content);
1862 |
1863 | for (const link of links) {
1864 | result.linksChecked++;
1865 |
1866 | // Skip external links unless explicitly requested
1867 | if (link.startsWith("http") && !followExternalLinks) continue;
1868 |
1869 | // Check internal links
1870 | if (!link.startsWith("http")) {
1871 | const fullPath = path.resolve(path.dirname(file), link);
1872 | try {
1873 | await fs.access(fullPath);
1874 | } catch {
1875 | result.brokenLinks.push(`${path.basename(file)}: ${link}`);
1876 | result.success = false;
1877 | }
1878 | }
1879 | }
1880 | }
1881 | }
1882 |
1883 | // Validate code blocks if requested
1884 | if (
1885 | includeCodeValidation &&
1886 | (validationType === "all" || validationType === "code")
1887 | ) {
1888 | for (const file of files) {
1889 | const content = await fs.readFile(file, "utf-8");
1890 | const codeBlocks = extractCodeBlocks(content);
1891 |
1892 | for (const block of codeBlocks) {
1893 | result.codeBlocksValidated++;
1894 |
1895 | // Basic syntax validation
1896 | if (block.language && block.code.trim()) {
1897 | if (block.language === "javascript" || block.language === "js") {
1898 | try {
1899 | // Basic JS syntax check - look for common issues
1900 | if (
1901 | block.code.includes("console.log") &&
1902 | !block.code.includes(";")
1903 | ) {
1904 | result.codeErrors.push(
1905 | `${path.basename(file)}: Missing semicolon in JS code`,
1906 | );
1907 | }
1908 | } catch (error) {
1909 | result.codeErrors.push(
1910 | `${path.basename(file)}: JS syntax error - ${error}`,
1911 | );
1912 | result.success = false;
1913 | }
1914 | }
1915 | }
1916 | }
1917 | }
1918 | }
1919 |
1920 | // Generate recommendations
1921 | if (result.brokenLinks.length > 0) {
1922 | result.recommendations.push(
1923 | `Fix ${result.brokenLinks.length} broken internal links`,
1924 | );
1925 | result.recommendations.push(
1926 | "Run documentation build to catch additional link issues",
1927 | );
1928 | }
1929 |
1930 | if (result.codeErrors.length > 0) {
1931 | result.recommendations.push(
1932 | `Review and fix ${result.codeErrors.length} code syntax issues`,
1933 | );
1934 | }
1935 |
1936 | if (result.success) {
1937 | result.recommendations.push(
1938 | "Content validation passed - no critical issues found",
1939 | );
1940 | }
1941 |
1942 | // Create summary
1943 | result.summary = `Validated ${files.length} files, ${
1944 | result.linksChecked
1945 | } links, ${result.codeBlocksValidated} code blocks. ${
1946 | result.success
1947 | ? "PASSED"
1948 | : `ISSUES FOUND: ${
1949 | result.brokenLinks.length + result.codeErrors.length
1950 | }`
1951 | }`;
1952 |
1953 | return result;
1954 | } catch (error) {
1955 | result.success = false;
1956 | result.recommendations.push(`Validation failed: ${error}`);
1957 | result.summary = `Validation error: ${error}`;
1958 | return result;
1959 | }
1960 | }
1961 |
1962 | // Helper function to extract links from markdown
1963 | function extractLinksFromMarkdown(content: string): string[] {
1964 | const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
1965 | const links: string[] = [];
1966 | let match;
1967 |
1968 | while ((match = linkRegex.exec(content)) !== null) {
1969 | links.push(match[2]); // The URL part
1970 | }
1971 |
1972 | return links;
1973 | }
1974 |
1975 | // Helper function to extract code blocks from markdown
1976 | function extractCodeBlocks(
1977 | content: string,
1978 | ): { language: string; code: string }[] {
1979 | const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
1980 | const blocks: { language: string; code: string }[] = [];
1981 | let match;
1982 |
1983 | while ((match = codeBlockRegex.exec(content)) !== null) {
1984 | blocks.push({
1985 | language: match[1] || "text",
1986 | code: match[2],
1987 | });
1988 | }
1989 |
1990 | return blocks;
1991 | }
1992 |
```
--------------------------------------------------------------------------------
/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 | * Call graph node representing a function and its calls (Issue #72)
126 | */
127 | export interface CallGraphNode {
128 | /** Function signature with full metadata */
129 | function: FunctionSignature;
130 | /** File location of this function */
131 | location: {
132 | file: string;
133 | line: number;
134 | column?: number;
135 | };
136 | /** Child function calls made by this function */
137 | calls: CallGraphNode[];
138 | /** Conditional branches (if/else, switch) with their paths */
139 | conditionalBranches: ConditionalPath[];
140 | /** Exception types that can be raised */
141 | exceptions: ExceptionPath[];
142 | /** Current recursion depth */
143 | depth: number;
144 | /** Whether this node was truncated due to maxDepth */
145 | truncated: boolean;
146 | /** Whether this is an external/imported function */
147 | isExternal: boolean;
148 | /** Source of the import if external */
149 | importSource?: string;
150 | }
151 |
152 | /**
153 | * Conditional execution path (if/else, switch, ternary)
154 | */
155 | export interface ConditionalPath {
156 | /** Type of conditional */
157 | type: "if" | "else-if" | "else" | "switch-case" | "ternary";
158 | /** The condition expression as string */
159 | condition: string;
160 | /** Line number of the conditional */
161 | lineNumber: number;
162 | /** Functions called in the true/case branch */
163 | trueBranch: CallGraphNode[];
164 | /** Functions called in the false/else branch */
165 | falseBranch: CallGraphNode[];
166 | }
167 |
168 | /**
169 | * Exception path tracking
170 | */
171 | export interface ExceptionPath {
172 | /** Exception type/class being thrown */
173 | exceptionType: string;
174 | /** Line number of the throw statement */
175 | lineNumber: number;
176 | /** The throw expression as string */
177 | expression: string;
178 | /** Whether this is caught within the function */
179 | isCaught: boolean;
180 | }
181 |
182 | /**
183 | * Complete call graph for an entry point (Issue #72)
184 | */
185 | export interface CallGraph {
186 | /** Name of the entry point function */
187 | entryPoint: string;
188 | /** Root node of the call graph */
189 | root: CallGraphNode;
190 | /** All discovered functions in the call graph */
191 | allFunctions: Map<string, FunctionSignature>;
192 | /** Maximum depth that was actually reached */
193 | maxDepthReached: number;
194 | /** Files that were analyzed */
195 | analyzedFiles: string[];
196 | /** Circular references detected */
197 | circularReferences: Array<{ from: string; to: string }>;
198 | /** External calls that couldn't be resolved */
199 | unresolvedCalls: Array<{
200 | name: string;
201 | location: { file: string; line: number };
202 | }>;
203 | /** Build timestamp */
204 | buildTime: string;
205 | }
206 |
207 | /**
208 | * Options for building call graphs
209 | */
210 | export interface CallGraphOptions {
211 | /** Maximum recursion depth (default: 3) */
212 | maxDepth?: number;
213 | /** Whether to resolve cross-file imports (default: true) */
214 | resolveImports?: boolean;
215 | /** Whether to extract conditional branches (default: true) */
216 | extractConditionals?: boolean;
217 | /** Whether to track exceptions (default: true) */
218 | trackExceptions?: boolean;
219 | /** File extensions to consider for import resolution */
220 | extensions?: string[];
221 | }
222 |
223 | /**
224 | * Main AST Analyzer class
225 | */
226 | export class ASTAnalyzer {
227 | private parsers: Map<string, any> = new Map();
228 | private initialized = false;
229 |
230 | /**
231 | * Initialize tree-sitter parsers for all languages
232 | */
233 | async initialize(): Promise<void> {
234 | if (this.initialized) return;
235 |
236 | // Note: Tree-sitter initialization would happen here in a full implementation
237 | // For now, we're primarily using TypeScript/JavaScript parser
238 | // console.log(
239 | // "AST Analyzer initialized with language support:",
240 | // Object.keys(LANGUAGE_CONFIGS),
241 | // );
242 | this.initialized = true;
243 | }
244 |
245 | /**
246 | * Analyze a single file and extract AST information
247 | */
248 | async analyzeFile(filePath: string): Promise<ASTAnalysisResult | null> {
249 | if (!this.initialized) {
250 | await this.initialize();
251 | }
252 |
253 | const ext = path.extname(filePath);
254 | const language = this.detectLanguage(ext);
255 |
256 | if (!language) {
257 | console.warn(`Unsupported file extension: ${ext}`);
258 | return null;
259 | }
260 |
261 | const content = await fs.readFile(filePath, "utf-8");
262 | const stats = await fs.stat(filePath);
263 |
264 | // Use TypeScript parser for .ts/.tsx files
265 | if (language === "typescript" || language === "javascript") {
266 | return this.analyzeTypeScript(
267 | filePath,
268 | content,
269 | stats.mtime.toISOString(),
270 | );
271 | }
272 |
273 | // For other languages, use tree-sitter (placeholder)
274 | return this.analyzeWithTreeSitter(
275 | filePath,
276 | content,
277 | language,
278 | stats.mtime.toISOString(),
279 | );
280 | }
281 |
282 | /**
283 | * Analyze TypeScript/JavaScript using typescript-estree
284 | */
285 | private async analyzeTypeScript(
286 | filePath: string,
287 | content: string,
288 | lastModified: string,
289 | ): Promise<ASTAnalysisResult> {
290 | const functions: FunctionSignature[] = [];
291 | const classes: ClassInfo[] = [];
292 | const interfaces: InterfaceInfo[] = [];
293 | const types: TypeInfo[] = [];
294 | const imports: ImportInfo[] = [];
295 | const exports: string[] = [];
296 |
297 | try {
298 | const ast = parseTypeScript(content, {
299 | loc: true,
300 | range: true,
301 | tokens: false,
302 | comment: true,
303 | jsx: filePath.endsWith(".tsx") || filePath.endsWith(".jsx"),
304 | });
305 |
306 | // Extract functions
307 | this.extractFunctions(ast, content, functions);
308 |
309 | // Extract classes
310 | this.extractClasses(ast, content, classes);
311 |
312 | // Extract interfaces
313 | this.extractInterfaces(ast, content, interfaces);
314 |
315 | // Extract type aliases
316 | this.extractTypes(ast, content, types);
317 |
318 | // Extract imports
319 | this.extractImports(ast, imports);
320 |
321 | // Extract exports
322 | this.extractExports(ast, exports);
323 | } catch (error) {
324 | console.warn(`Failed to parse TypeScript file ${filePath}:`, error);
325 | }
326 |
327 | const contentHash = crypto
328 | .createHash("sha256")
329 | .update(content)
330 | .digest("hex");
331 | const linesOfCode = content.split("\n").length;
332 | const complexity = this.calculateComplexity(functions, classes);
333 |
334 | return {
335 | filePath,
336 | language:
337 | filePath.endsWith(".ts") || filePath.endsWith(".tsx")
338 | ? "typescript"
339 | : "javascript",
340 | functions,
341 | classes,
342 | interfaces,
343 | types,
344 | imports,
345 | exports,
346 | contentHash,
347 | lastModified,
348 | linesOfCode,
349 | complexity,
350 | };
351 | }
352 |
353 | /**
354 | * Analyze using tree-sitter (placeholder for other languages)
355 | */
356 | private async analyzeWithTreeSitter(
357 | filePath: string,
358 | content: string,
359 | language: string,
360 | lastModified: string,
361 | ): Promise<ASTAnalysisResult> {
362 | // Placeholder for tree-sitter analysis
363 | // In a full implementation, we'd parse the content using tree-sitter
364 | // and extract language-specific constructs
365 |
366 | const contentHash = crypto
367 | .createHash("sha256")
368 | .update(content)
369 | .digest("hex");
370 | const linesOfCode = content.split("\n").length;
371 |
372 | return {
373 | filePath,
374 | language,
375 | functions: [],
376 | classes: [],
377 | interfaces: [],
378 | types: [],
379 | imports: [],
380 | exports: [],
381 | contentHash,
382 | lastModified,
383 | linesOfCode,
384 | complexity: 0,
385 | };
386 | }
387 |
388 | /**
389 | * Extract function declarations from AST
390 | */
391 | private extractFunctions(
392 | ast: any,
393 | content: string,
394 | functions: FunctionSignature[],
395 | ): void {
396 | const lines = content.split("\n");
397 |
398 | const traverse = (node: any, isExported = false) => {
399 | if (!node) return;
400 |
401 | // Handle export declarations
402 | if (
403 | node.type === "ExportNamedDeclaration" ||
404 | node.type === "ExportDefaultDeclaration"
405 | ) {
406 | if (node.declaration) {
407 | traverse(node.declaration, true);
408 | }
409 | return;
410 | }
411 |
412 | // Function declarations
413 | if (node.type === "FunctionDeclaration") {
414 | const func = this.parseFunctionNode(node, lines, isExported);
415 | if (func) functions.push(func);
416 | }
417 |
418 | // Arrow functions assigned to variables
419 | if (node.type === "VariableDeclaration") {
420 | for (const declarator of node.declarations || []) {
421 | if (declarator.init?.type === "ArrowFunctionExpression") {
422 | const func = this.parseArrowFunction(declarator, lines, isExported);
423 | if (func) functions.push(func);
424 | }
425 | }
426 | }
427 |
428 | // Traverse children
429 | for (const key in node) {
430 | if (typeof node[key] === "object" && node[key] !== null) {
431 | if (Array.isArray(node[key])) {
432 | node[key].forEach((child: any) => traverse(child, false));
433 | } else {
434 | traverse(node[key], false);
435 | }
436 | }
437 | }
438 | };
439 |
440 | traverse(ast);
441 | }
442 |
443 | /**
444 | * Parse function node
445 | */
446 | private parseFunctionNode(
447 | node: any,
448 | lines: string[],
449 | isExported: boolean,
450 | ): FunctionSignature | null {
451 | if (!node.id?.name) return null;
452 |
453 | const docComment = this.extractDocComment(node.loc?.start.line - 1, lines);
454 | const parameters = this.extractParameters(node.params);
455 |
456 | return {
457 | name: node.id.name,
458 | parameters,
459 | returnType: this.extractReturnType(node),
460 | isAsync: node.async || false,
461 | isExported,
462 | isPublic: true,
463 | docComment,
464 | startLine: node.loc?.start.line || 0,
465 | endLine: node.loc?.end.line || 0,
466 | complexity: this.calculateFunctionComplexity(node),
467 | dependencies: [],
468 | };
469 | }
470 |
471 | /**
472 | * Parse arrow function
473 | */
474 | private parseArrowFunction(
475 | declarator: any,
476 | lines: string[],
477 | isExported: boolean,
478 | ): FunctionSignature | null {
479 | if (!declarator.id?.name) return null;
480 |
481 | const node = declarator.init;
482 | const docComment = this.extractDocComment(
483 | declarator.loc?.start.line - 1,
484 | lines,
485 | );
486 | const parameters = this.extractParameters(node.params);
487 |
488 | return {
489 | name: declarator.id.name,
490 | parameters,
491 | returnType: this.extractReturnType(node),
492 | isAsync: node.async || false,
493 | isExported,
494 | isPublic: true,
495 | docComment,
496 | startLine: declarator.loc?.start.line || 0,
497 | endLine: declarator.loc?.end.line || 0,
498 | complexity: this.calculateFunctionComplexity(node),
499 | dependencies: [],
500 | };
501 | }
502 |
503 | /**
504 | * Extract classes from AST
505 | */
506 | private extractClasses(
507 | ast: any,
508 | content: string,
509 | classes: ClassInfo[],
510 | ): void {
511 | const lines = content.split("\n");
512 |
513 | const traverse = (node: any, isExported = false) => {
514 | if (!node) return;
515 |
516 | // Handle export declarations
517 | if (
518 | node.type === "ExportNamedDeclaration" ||
519 | node.type === "ExportDefaultDeclaration"
520 | ) {
521 | if (node.declaration) {
522 | traverse(node.declaration, true);
523 | }
524 | return;
525 | }
526 |
527 | if (node.type === "ClassDeclaration" && node.id?.name) {
528 | const classInfo = this.parseClassNode(node, lines, isExported);
529 | if (classInfo) classes.push(classInfo);
530 | }
531 |
532 | for (const key in node) {
533 | if (typeof node[key] === "object" && node[key] !== null) {
534 | if (Array.isArray(node[key])) {
535 | node[key].forEach((child: any) => traverse(child, false));
536 | } else {
537 | traverse(node[key], false);
538 | }
539 | }
540 | }
541 | };
542 |
543 | traverse(ast);
544 | }
545 |
546 | /**
547 | * Parse class node
548 | */
549 | private parseClassNode(
550 | node: any,
551 | lines: string[],
552 | isExported: boolean,
553 | ): ClassInfo | null {
554 | const methods: FunctionSignature[] = [];
555 | const properties: PropertyInfo[] = [];
556 |
557 | // Extract methods and properties
558 | if (node.body?.body) {
559 | for (const member of node.body.body) {
560 | if (member.type === "MethodDefinition") {
561 | const method = this.parseMethodNode(member, lines);
562 | if (method) methods.push(method);
563 | } else if (member.type === "PropertyDefinition") {
564 | const property = this.parsePropertyNode(member);
565 | if (property) properties.push(property);
566 | }
567 | }
568 | }
569 |
570 | return {
571 | name: node.id.name,
572 | isExported,
573 | extends: node.superClass?.name || null,
574 | implements:
575 | node.implements?.map((i: any) => i.expression?.name || "unknown") || [],
576 | methods,
577 | properties,
578 | docComment: this.extractDocComment(node.loc?.start.line - 1, lines),
579 | startLine: node.loc?.start.line || 0,
580 | endLine: node.loc?.end.line || 0,
581 | };
582 | }
583 |
584 | /**
585 | * Parse method node
586 | */
587 | private parseMethodNode(
588 | node: any,
589 | lines: string[],
590 | ): FunctionSignature | null {
591 | if (!node.key?.name) return null;
592 |
593 | return {
594 | name: node.key.name,
595 | parameters: this.extractParameters(node.value?.params || []),
596 | returnType: this.extractReturnType(node.value),
597 | isAsync: node.value?.async || false,
598 | isExported: false,
599 | isPublic: !node.key.name.startsWith("_"),
600 | docComment: this.extractDocComment(node.loc?.start.line - 1, lines),
601 | startLine: node.loc?.start.line || 0,
602 | endLine: node.loc?.end.line || 0,
603 | complexity: this.calculateFunctionComplexity(node.value),
604 | dependencies: [],
605 | };
606 | }
607 |
608 | /**
609 | * Parse property node
610 | */
611 | private parsePropertyNode(node: any): PropertyInfo | null {
612 | if (!node.key?.name) return null;
613 |
614 | return {
615 | name: node.key.name,
616 | type: this.extractTypeAnnotation(node.typeAnnotation),
617 | isStatic: node.static || false,
618 | isReadonly: node.readonly || false,
619 | visibility: this.determineVisibility(node),
620 | };
621 | }
622 |
623 | /**
624 | * Extract interfaces from AST
625 | */
626 | private extractInterfaces(
627 | ast: any,
628 | content: string,
629 | interfaces: InterfaceInfo[],
630 | ): void {
631 | const lines = content.split("\n");
632 |
633 | const traverse = (node: any, isExported = false) => {
634 | if (!node) return;
635 |
636 | // Handle export declarations
637 | if (
638 | node.type === "ExportNamedDeclaration" ||
639 | node.type === "ExportDefaultDeclaration"
640 | ) {
641 | if (node.declaration) {
642 | traverse(node.declaration, true);
643 | }
644 | return;
645 | }
646 |
647 | if (node.type === "TSInterfaceDeclaration" && node.id?.name) {
648 | const interfaceInfo = this.parseInterfaceNode(node, lines, isExported);
649 | if (interfaceInfo) interfaces.push(interfaceInfo);
650 | }
651 |
652 | for (const key in node) {
653 | if (typeof node[key] === "object" && node[key] !== null) {
654 | if (Array.isArray(node[key])) {
655 | node[key].forEach((child: any) => traverse(child, false));
656 | } else {
657 | traverse(node[key], false);
658 | }
659 | }
660 | }
661 | };
662 |
663 | traverse(ast);
664 | }
665 |
666 | /**
667 | * Parse interface node
668 | */
669 | private parseInterfaceNode(
670 | node: any,
671 | lines: string[],
672 | isExported: boolean,
673 | ): InterfaceInfo | null {
674 | const properties: PropertyInfo[] = [];
675 | const methods: FunctionSignature[] = [];
676 |
677 | if (node.body?.body) {
678 | for (const member of node.body.body) {
679 | if (member.type === "TSPropertySignature") {
680 | const prop = this.parseInterfaceProperty(member);
681 | if (prop) properties.push(prop);
682 | } else if (member.type === "TSMethodSignature") {
683 | const method = this.parseInterfaceMethod(member);
684 | if (method) methods.push(method);
685 | }
686 | }
687 | }
688 |
689 | return {
690 | name: node.id.name,
691 | isExported,
692 | extends:
693 | node.extends?.map((e: any) => e.expression?.name || "unknown") || [],
694 | properties,
695 | methods,
696 | docComment: this.extractDocComment(node.loc?.start.line - 1, lines),
697 | startLine: node.loc?.start.line || 0,
698 | endLine: node.loc?.end.line || 0,
699 | };
700 | }
701 |
702 | /**
703 | * Parse interface property
704 | */
705 | private parseInterfaceProperty(node: any): PropertyInfo | null {
706 | if (!node.key?.name) return null;
707 |
708 | return {
709 | name: node.key.name,
710 | type: this.extractTypeAnnotation(node.typeAnnotation),
711 | isStatic: false,
712 | isReadonly: node.readonly || false,
713 | visibility: "public",
714 | };
715 | }
716 |
717 | /**
718 | * Parse interface method
719 | */
720 | private parseInterfaceMethod(node: any): FunctionSignature | null {
721 | if (!node.key?.name) return null;
722 |
723 | return {
724 | name: node.key.name,
725 | parameters: this.extractParameters(node.params || []),
726 | returnType: this.extractTypeAnnotation(node.returnType),
727 | isAsync: false,
728 | isExported: false,
729 | isPublic: true,
730 | docComment: null,
731 | startLine: node.loc?.start.line || 0,
732 | endLine: node.loc?.end.line || 0,
733 | complexity: 0,
734 | dependencies: [],
735 | };
736 | }
737 |
738 | /**
739 | * Extract type aliases from AST
740 | */
741 | private extractTypes(ast: any, content: string, types: TypeInfo[]): void {
742 | const lines = content.split("\n");
743 |
744 | const traverse = (node: any, isExported = false) => {
745 | if (!node) return;
746 |
747 | // Handle export declarations
748 | if (
749 | node.type === "ExportNamedDeclaration" ||
750 | node.type === "ExportDefaultDeclaration"
751 | ) {
752 | if (node.declaration) {
753 | traverse(node.declaration, true);
754 | }
755 | return;
756 | }
757 |
758 | if (node.type === "TSTypeAliasDeclaration" && node.id?.name) {
759 | const typeInfo = this.parseTypeNode(node, lines, isExported);
760 | if (typeInfo) types.push(typeInfo);
761 | }
762 |
763 | for (const key in node) {
764 | if (typeof node[key] === "object" && node[key] !== null) {
765 | if (Array.isArray(node[key])) {
766 | node[key].forEach((child: any) => traverse(child, false));
767 | } else {
768 | traverse(node[key], false);
769 | }
770 | }
771 | }
772 | };
773 |
774 | traverse(ast);
775 | }
776 |
777 | /**
778 | * Parse type alias node
779 | */
780 | private parseTypeNode(
781 | node: any,
782 | lines: string[],
783 | isExported: boolean,
784 | ): TypeInfo | null {
785 | return {
786 | name: node.id.name,
787 | isExported,
788 | definition: this.extractTypeDefinition(node.typeAnnotation),
789 | docComment: this.extractDocComment(node.loc?.start.line - 1, lines),
790 | startLine: node.loc?.start.line || 0,
791 | endLine: node.loc?.end.line || 0,
792 | };
793 | }
794 |
795 | /**
796 | * Extract imports from AST
797 | */
798 | private extractImports(ast: any, imports: ImportInfo[]): void {
799 | const traverse = (node: any) => {
800 | if (!node) return;
801 |
802 | if (node.type === "ImportDeclaration") {
803 | const importInfo: ImportInfo = {
804 | source: node.source?.value || "",
805 | imports: [],
806 | isDefault: false,
807 | startLine: node.loc?.start.line || 0,
808 | };
809 |
810 | for (const specifier of node.specifiers || []) {
811 | if (specifier.type === "ImportDefaultSpecifier") {
812 | importInfo.isDefault = true;
813 | importInfo.imports.push({
814 | name: specifier.local?.name || "default",
815 | });
816 | } else if (specifier.type === "ImportSpecifier") {
817 | importInfo.imports.push({
818 | name: specifier.imported?.name || "",
819 | alias:
820 | specifier.local?.name !== specifier.imported?.name
821 | ? specifier.local?.name
822 | : undefined,
823 | });
824 | }
825 | }
826 |
827 | imports.push(importInfo);
828 | }
829 |
830 | for (const key in node) {
831 | if (typeof node[key] === "object" && node[key] !== null) {
832 | if (Array.isArray(node[key])) {
833 | node[key].forEach((child: any) => traverse(child));
834 | } else {
835 | traverse(node[key]);
836 | }
837 | }
838 | }
839 | };
840 |
841 | traverse(ast);
842 | }
843 |
844 | /**
845 | * Extract exports from AST
846 | */
847 | private extractExports(ast: any, exports: string[]): void {
848 | const traverse = (node: any) => {
849 | if (!node) return;
850 |
851 | // Named exports
852 | if (node.type === "ExportNamedDeclaration") {
853 | if (node.declaration) {
854 | if (node.declaration.id?.name) {
855 | exports.push(node.declaration.id.name);
856 | } else if (node.declaration.declarations) {
857 | for (const decl of node.declaration.declarations) {
858 | if (decl.id?.name) exports.push(decl.id.name);
859 | }
860 | }
861 | }
862 | for (const specifier of node.specifiers || []) {
863 | if (specifier.exported?.name) exports.push(specifier.exported.name);
864 | }
865 | }
866 |
867 | // Default export
868 | if (node.type === "ExportDefaultDeclaration") {
869 | if (node.declaration?.id?.name) {
870 | exports.push(node.declaration.id.name);
871 | } else {
872 | exports.push("default");
873 | }
874 | }
875 |
876 | for (const key in node) {
877 | if (typeof node[key] === "object" && node[key] !== null) {
878 | if (Array.isArray(node[key])) {
879 | node[key].forEach((child: any) => traverse(child));
880 | } else {
881 | traverse(node[key]);
882 | }
883 | }
884 | }
885 | };
886 |
887 | traverse(ast);
888 | }
889 |
890 | // Helper methods
891 |
892 | private extractParameters(params: any[]): ParameterInfo[] {
893 | return params.map((param) => ({
894 | name: param.name || param.argument?.name || param.left?.name || "unknown",
895 | type: this.extractTypeAnnotation(param.typeAnnotation),
896 | optional: param.optional || false,
897 | defaultValue: param.right ? this.extractDefaultValue(param.right) : null,
898 | }));
899 | }
900 |
901 | private extractReturnType(node: any): string | null {
902 | return this.extractTypeAnnotation(node?.returnType);
903 | }
904 |
905 | private extractTypeAnnotation(typeAnnotation: any): string | null {
906 | if (!typeAnnotation) return null;
907 | if (typeAnnotation.typeAnnotation)
908 | return this.extractTypeDefinition(typeAnnotation.typeAnnotation);
909 | return this.extractTypeDefinition(typeAnnotation);
910 | }
911 |
912 | private extractTypeDefinition(typeNode: any): string {
913 | if (!typeNode) return "unknown";
914 | if (typeNode.type === "TSStringKeyword") return "string";
915 | if (typeNode.type === "TSNumberKeyword") return "number";
916 | if (typeNode.type === "TSBooleanKeyword") return "boolean";
917 | if (typeNode.type === "TSAnyKeyword") return "any";
918 | if (typeNode.type === "TSVoidKeyword") return "void";
919 | if (typeNode.type === "TSTypeReference")
920 | return typeNode.typeName?.name || "unknown";
921 | return "unknown";
922 | }
923 |
924 | private extractDefaultValue(node: any): string | null {
925 | if (node.type === "Literal") return String(node.value);
926 | if (node.type === "Identifier") return node.name;
927 | return null;
928 | }
929 |
930 | private extractDocComment(
931 | lineNumber: number,
932 | lines: string[],
933 | ): string | null {
934 | if (lineNumber < 0 || lineNumber >= lines.length) return null;
935 |
936 | const comment: string[] = [];
937 | let currentLine = lineNumber;
938 |
939 | // Look backwards for JSDoc comment
940 | while (currentLine >= 0) {
941 | const line = lines[currentLine].trim();
942 | if (line.startsWith("*/")) {
943 | comment.unshift(line);
944 | currentLine--;
945 | continue;
946 | }
947 | if (line.startsWith("*") || line.startsWith("/**")) {
948 | comment.unshift(line);
949 | if (line.startsWith("/**")) break;
950 | currentLine--;
951 | continue;
952 | }
953 | if (comment.length > 0) break;
954 | currentLine--;
955 | }
956 |
957 | return comment.length > 0 ? comment.join("\n") : null;
958 | }
959 |
960 | private isExported(node: any): boolean {
961 | if (!node) return false;
962 |
963 | // Check parent for export
964 | let current = node;
965 | while (current) {
966 | if (
967 | current.type === "ExportNamedDeclaration" ||
968 | current.type === "ExportDefaultDeclaration"
969 | ) {
970 | return true;
971 | }
972 | current = current.parent;
973 | }
974 |
975 | return false;
976 | }
977 |
978 | private determineVisibility(node: any): "public" | "private" | "protected" {
979 | if (node.accessibility) return node.accessibility;
980 | if (node.key?.name?.startsWith("_")) return "private";
981 | if (node.key?.name?.startsWith("#")) return "private";
982 | return "public";
983 | }
984 |
985 | private calculateFunctionComplexity(node: any): number {
986 | // Simplified cyclomatic complexity
987 | let complexity = 1;
988 |
989 | const traverse = (n: any) => {
990 | if (!n) return;
991 |
992 | // Increment for control flow statements
993 | if (
994 | [
995 | "IfStatement",
996 | "ConditionalExpression",
997 | "ForStatement",
998 | "WhileStatement",
999 | "DoWhileStatement",
1000 | "SwitchCase",
1001 | "CatchClause",
1002 | ].includes(n.type)
1003 | ) {
1004 | complexity++;
1005 | }
1006 |
1007 | for (const key in n) {
1008 | if (typeof n[key] === "object" && n[key] !== null) {
1009 | if (Array.isArray(n[key])) {
1010 | n[key].forEach((child: any) => traverse(child));
1011 | } else {
1012 | traverse(n[key]);
1013 | }
1014 | }
1015 | }
1016 | };
1017 |
1018 | traverse(node);
1019 | return complexity;
1020 | }
1021 |
1022 | private calculateComplexity(
1023 | functions: FunctionSignature[],
1024 | classes: ClassInfo[],
1025 | ): number {
1026 | const functionComplexity = functions.reduce(
1027 | (sum, f) => sum + f.complexity,
1028 | 0,
1029 | );
1030 | const classComplexity = classes.reduce(
1031 | (sum, c) =>
1032 | sum + c.methods.reduce((methodSum, m) => methodSum + m.complexity, 0),
1033 | 0,
1034 | );
1035 | return functionComplexity + classComplexity;
1036 | }
1037 |
1038 | private detectLanguage(ext: string): string | null {
1039 | for (const [lang, config] of Object.entries(LANGUAGE_CONFIGS)) {
1040 | if (config.extensions.includes(ext)) return lang;
1041 | }
1042 | return null;
1043 | }
1044 |
1045 | /**
1046 | * Compare two AST analysis results and detect changes
1047 | */
1048 | async detectDrift(
1049 | oldAnalysis: ASTAnalysisResult,
1050 | newAnalysis: ASTAnalysisResult,
1051 | ): Promise<CodeDiff[]> {
1052 | const diffs: CodeDiff[] = [];
1053 |
1054 | // Compare functions
1055 | diffs.push(
1056 | ...this.compareFunctions(oldAnalysis.functions, newAnalysis.functions),
1057 | );
1058 |
1059 | // Compare classes
1060 | diffs.push(
1061 | ...this.compareClasses(oldAnalysis.classes, newAnalysis.classes),
1062 | );
1063 |
1064 | // Compare interfaces
1065 | diffs.push(
1066 | ...this.compareInterfaces(oldAnalysis.interfaces, newAnalysis.interfaces),
1067 | );
1068 |
1069 | // Compare types
1070 | diffs.push(...this.compareTypes(oldAnalysis.types, newAnalysis.types));
1071 |
1072 | return diffs;
1073 | }
1074 |
1075 | private compareFunctions(
1076 | oldFuncs: FunctionSignature[],
1077 | newFuncs: FunctionSignature[],
1078 | ): CodeDiff[] {
1079 | const diffs: CodeDiff[] = [];
1080 | const oldMap = new Map(oldFuncs.map((f) => [f.name, f]));
1081 | const newMap = new Map(newFuncs.map((f) => [f.name, f]));
1082 |
1083 | // Check for removed functions
1084 | for (const [name, func] of oldMap) {
1085 | if (!newMap.has(name)) {
1086 | diffs.push({
1087 | type: "removed",
1088 | category: "function",
1089 | name,
1090 | details: `Function '${name}' was removed`,
1091 | oldSignature: this.formatFunctionSignature(func),
1092 | impactLevel: func.isExported ? "breaking" : "minor",
1093 | });
1094 | }
1095 | }
1096 |
1097 | // Check for added functions
1098 | for (const [name, func] of newMap) {
1099 | if (!oldMap.has(name)) {
1100 | diffs.push({
1101 | type: "added",
1102 | category: "function",
1103 | name,
1104 | details: `Function '${name}' was added`,
1105 | newSignature: this.formatFunctionSignature(func),
1106 | impactLevel: "patch",
1107 | });
1108 | }
1109 | }
1110 |
1111 | // Check for modified functions
1112 | for (const [name, newFunc] of newMap) {
1113 | const oldFunc = oldMap.get(name);
1114 | if (oldFunc) {
1115 | const changes = this.detectFunctionChanges(oldFunc, newFunc);
1116 | if (changes.length > 0) {
1117 | diffs.push({
1118 | type: "modified",
1119 | category: "function",
1120 | name,
1121 | details: changes.join("; "),
1122 | oldSignature: this.formatFunctionSignature(oldFunc),
1123 | newSignature: this.formatFunctionSignature(newFunc),
1124 | impactLevel: this.determineFunctionImpact(oldFunc, newFunc),
1125 | });
1126 | }
1127 | }
1128 | }
1129 |
1130 | return diffs;
1131 | }
1132 |
1133 | private compareClasses(
1134 | oldClasses: ClassInfo[],
1135 | newClasses: ClassInfo[],
1136 | ): CodeDiff[] {
1137 | const diffs: CodeDiff[] = [];
1138 | const oldMap = new Map(oldClasses.map((c) => [c.name, c]));
1139 | const newMap = new Map(newClasses.map((c) => [c.name, c]));
1140 |
1141 | for (const [name, oldClass] of oldMap) {
1142 | if (!newMap.has(name)) {
1143 | diffs.push({
1144 | type: "removed",
1145 | category: "class",
1146 | name,
1147 | details: `Class '${name}' was removed`,
1148 | impactLevel: oldClass.isExported ? "breaking" : "minor",
1149 | });
1150 | }
1151 | }
1152 |
1153 | for (const [name] of newMap) {
1154 | if (!oldMap.has(name)) {
1155 | diffs.push({
1156 | type: "added",
1157 | category: "class",
1158 | name,
1159 | details: `Class '${name}' was added`,
1160 | impactLevel: "patch",
1161 | });
1162 | }
1163 | }
1164 |
1165 | return diffs;
1166 | }
1167 |
1168 | private compareInterfaces(
1169 | oldInterfaces: InterfaceInfo[],
1170 | newInterfaces: InterfaceInfo[],
1171 | ): CodeDiff[] {
1172 | const diffs: CodeDiff[] = [];
1173 | const oldMap = new Map(oldInterfaces.map((i) => [i.name, i]));
1174 | const newMap = new Map(newInterfaces.map((i) => [i.name, i]));
1175 |
1176 | for (const [name, oldInterface] of oldMap) {
1177 | if (!newMap.has(name)) {
1178 | diffs.push({
1179 | type: "removed",
1180 | category: "interface",
1181 | name,
1182 | details: `Interface '${name}' was removed`,
1183 | impactLevel: oldInterface.isExported ? "breaking" : "minor",
1184 | });
1185 | }
1186 | }
1187 |
1188 | for (const [name] of newMap) {
1189 | if (!oldMap.has(name)) {
1190 | diffs.push({
1191 | type: "added",
1192 | category: "interface",
1193 | name,
1194 | details: `Interface '${name}' was added`,
1195 | impactLevel: "patch",
1196 | });
1197 | }
1198 | }
1199 |
1200 | return diffs;
1201 | }
1202 |
1203 | private compareTypes(oldTypes: TypeInfo[], newTypes: TypeInfo[]): CodeDiff[] {
1204 | const diffs: CodeDiff[] = [];
1205 | const oldMap = new Map(oldTypes.map((t) => [t.name, t]));
1206 | const newMap = new Map(newTypes.map((t) => [t.name, t]));
1207 |
1208 | for (const [name, oldType] of oldMap) {
1209 | if (!newMap.has(name)) {
1210 | diffs.push({
1211 | type: "removed",
1212 | category: "type",
1213 | name,
1214 | details: `Type '${name}' was removed`,
1215 | impactLevel: oldType.isExported ? "breaking" : "minor",
1216 | });
1217 | }
1218 | }
1219 |
1220 | for (const [name] of newMap) {
1221 | if (!oldMap.has(name)) {
1222 | diffs.push({
1223 | type: "added",
1224 | category: "type",
1225 | name,
1226 | details: `Type '${name}' was added`,
1227 | impactLevel: "patch",
1228 | });
1229 | }
1230 | }
1231 |
1232 | return diffs;
1233 | }
1234 |
1235 | private detectFunctionChanges(
1236 | oldFunc: FunctionSignature,
1237 | newFunc: FunctionSignature,
1238 | ): string[] {
1239 | const changes: string[] = [];
1240 |
1241 | // Check parameter changes
1242 | if (oldFunc.parameters.length !== newFunc.parameters.length) {
1243 | changes.push(
1244 | `Parameter count changed from ${oldFunc.parameters.length} to ${newFunc.parameters.length}`,
1245 | );
1246 | }
1247 |
1248 | // Check return type changes
1249 | if (oldFunc.returnType !== newFunc.returnType) {
1250 | changes.push(
1251 | `Return type changed from '${oldFunc.returnType}' to '${newFunc.returnType}'`,
1252 | );
1253 | }
1254 |
1255 | // Check async changes
1256 | if (oldFunc.isAsync !== newFunc.isAsync) {
1257 | changes.push(
1258 | newFunc.isAsync
1259 | ? "Function became async"
1260 | : "Function is no longer async",
1261 | );
1262 | }
1263 |
1264 | // Check export changes
1265 | if (oldFunc.isExported !== newFunc.isExported) {
1266 | changes.push(
1267 | newFunc.isExported
1268 | ? "Function is now exported"
1269 | : "Function is no longer exported",
1270 | );
1271 | }
1272 |
1273 | return changes;
1274 | }
1275 |
1276 | private determineFunctionImpact(
1277 | oldFunc: FunctionSignature,
1278 | newFunc: FunctionSignature,
1279 | ): "breaking" | "major" | "minor" | "patch" {
1280 | // Breaking changes
1281 | if (oldFunc.isExported) {
1282 | if (oldFunc.parameters.length !== newFunc.parameters.length)
1283 | return "breaking";
1284 | if (oldFunc.returnType !== newFunc.returnType) return "breaking";
1285 | // If a function was exported and is no longer exported, that's breaking
1286 | if (oldFunc.isExported && !newFunc.isExported) return "breaking";
1287 | }
1288 |
1289 | // Major changes
1290 | if (oldFunc.isAsync !== newFunc.isAsync) return "major";
1291 |
1292 | // Minor changes (new API surface)
1293 | // If a function becomes exported, that's a minor change (new feature/API)
1294 | if (!oldFunc.isExported && newFunc.isExported) return "minor";
1295 |
1296 | return "patch";
1297 | }
1298 |
1299 | private formatFunctionSignature(func: FunctionSignature): string {
1300 | const params = func.parameters
1301 | .map((p) => `${p.name}: ${p.type || "any"}`)
1302 | .join(", ");
1303 | const returnType = func.returnType || "void";
1304 | const asyncPrefix = func.isAsync ? "async " : "";
1305 | return `${asyncPrefix}${func.name}(${params}): ${returnType}`;
1306 | }
1307 |
1308 | // ============================================================================
1309 | // Call Graph Builder (Issue #72)
1310 | // ============================================================================
1311 |
1312 | /**
1313 | * Build a call graph starting from an entry function
1314 | *
1315 | * @param entryFunction - Name of the function to start from
1316 | * @param projectPath - Root path of the project for cross-file resolution
1317 | * @param options - Call graph building options
1318 | * @returns Complete call graph with all discovered paths
1319 | */
1320 | async buildCallGraph(
1321 | entryFunction: string,
1322 | projectPath: string,
1323 | options: CallGraphOptions = {},
1324 | ): Promise<CallGraph> {
1325 | const resolvedOptions: Required<CallGraphOptions> = {
1326 | maxDepth: options.maxDepth ?? 3,
1327 | resolveImports: options.resolveImports ?? true,
1328 | extractConditionals: options.extractConditionals ?? true,
1329 | trackExceptions: options.trackExceptions ?? true,
1330 | extensions: options.extensions ?? [".ts", ".tsx", ".js", ".jsx", ".mjs"],
1331 | };
1332 |
1333 | if (!this.initialized) {
1334 | await this.initialize();
1335 | }
1336 |
1337 | // Find the entry file containing the function
1338 | const entryFile = await this.findFunctionFile(
1339 | entryFunction,
1340 | projectPath,
1341 | resolvedOptions.extensions,
1342 | );
1343 |
1344 | if (!entryFile) {
1345 | return this.createEmptyCallGraph(entryFunction);
1346 | }
1347 |
1348 | // Analyze the entry file
1349 | const entryAnalysis = await this.analyzeFile(entryFile);
1350 | if (!entryAnalysis) {
1351 | return this.createEmptyCallGraph(entryFunction);
1352 | }
1353 |
1354 | const entryFunc = entryAnalysis.functions.find(
1355 | (f) => f.name === entryFunction,
1356 | );
1357 | if (!entryFunc) {
1358 | // Check class methods
1359 | for (const cls of entryAnalysis.classes) {
1360 | const method = cls.methods.find((m) => m.name === entryFunction);
1361 | if (method) {
1362 | return this.buildCallGraphFromFunction(
1363 | method,
1364 | entryFile,
1365 | entryAnalysis,
1366 | projectPath,
1367 | resolvedOptions,
1368 | );
1369 | }
1370 | }
1371 | return this.createEmptyCallGraph(entryFunction);
1372 | }
1373 |
1374 | return this.buildCallGraphFromFunction(
1375 | entryFunc,
1376 | entryFile,
1377 | entryAnalysis,
1378 | projectPath,
1379 | resolvedOptions,
1380 | );
1381 | }
1382 |
1383 | /**
1384 | * Build call graph from a specific function
1385 | */
1386 | private async buildCallGraphFromFunction(
1387 | entryFunc: FunctionSignature,
1388 | entryFile: string,
1389 | entryAnalysis: ASTAnalysisResult,
1390 | projectPath: string,
1391 | options: Required<CallGraphOptions>,
1392 | ): Promise<CallGraph> {
1393 | const allFunctions = new Map<string, FunctionSignature>();
1394 | const analyzedFiles: string[] = [entryFile];
1395 | const circularReferences: Array<{ from: string; to: string }> = [];
1396 | const unresolvedCalls: Array<{
1397 | name: string;
1398 | location: { file: string; line: number };
1399 | }> = [];
1400 |
1401 | // Cache for analyzed files
1402 | const analysisCache = new Map<string, ASTAnalysisResult>();
1403 | analysisCache.set(entryFile, entryAnalysis);
1404 |
1405 | // Track visited functions to prevent infinite loops
1406 | const visited = new Set<string>();
1407 | let maxDepthReached = 0;
1408 |
1409 | const root = await this.buildCallGraphNode(
1410 | entryFunc,
1411 | entryFile,
1412 | entryAnalysis,
1413 | projectPath,
1414 | options,
1415 | 0,
1416 | visited,
1417 | allFunctions,
1418 | analysisCache,
1419 | circularReferences,
1420 | unresolvedCalls,
1421 | analyzedFiles,
1422 | (depth) => {
1423 | maxDepthReached = Math.max(maxDepthReached, depth);
1424 | },
1425 | );
1426 |
1427 | return {
1428 | entryPoint: entryFunc.name,
1429 | root,
1430 | allFunctions,
1431 | maxDepthReached,
1432 | analyzedFiles: [...new Set(analyzedFiles)],
1433 | circularReferences,
1434 | unresolvedCalls,
1435 | buildTime: new Date().toISOString(),
1436 | };
1437 | }
1438 |
1439 | /**
1440 | * Build a single call graph node recursively
1441 | */
1442 | private async buildCallGraphNode(
1443 | func: FunctionSignature,
1444 | filePath: string,
1445 | analysis: ASTAnalysisResult,
1446 | projectPath: string,
1447 | options: Required<CallGraphOptions>,
1448 | depth: number,
1449 | visited: Set<string>,
1450 | allFunctions: Map<string, FunctionSignature>,
1451 | analysisCache: Map<string, ASTAnalysisResult>,
1452 | circularReferences: Array<{ from: string; to: string }>,
1453 | unresolvedCalls: Array<{
1454 | name: string;
1455 | location: { file: string; line: number };
1456 | }>,
1457 | analyzedFiles: string[],
1458 | updateMaxDepth: (depth: number) => void,
1459 | ): Promise<CallGraphNode> {
1460 | const funcKey = `${filePath}:${func.name}`;
1461 | updateMaxDepth(depth);
1462 |
1463 | // Check for circular reference
1464 | if (visited.has(funcKey)) {
1465 | circularReferences.push({ from: funcKey, to: func.name });
1466 | return this.createTruncatedNode(func, filePath, depth, true);
1467 | }
1468 |
1469 | // Check max depth
1470 | if (depth >= options.maxDepth) {
1471 | return this.createTruncatedNode(func, filePath, depth, false);
1472 | }
1473 |
1474 | visited.add(funcKey);
1475 | allFunctions.set(func.name, func);
1476 |
1477 | // Read the file content to extract function body
1478 | let content: string;
1479 | try {
1480 | content = await fs.readFile(filePath, "utf-8");
1481 | } catch {
1482 | return this.createTruncatedNode(func, filePath, depth, false);
1483 | }
1484 |
1485 | // Parse the function body to find calls, conditionals, and exceptions
1486 | const functionBody = this.extractFunctionBody(content, func);
1487 | const callExpressions = this.extractCallExpressions(
1488 | functionBody,
1489 | func.startLine,
1490 | );
1491 | const conditionalBranches: ConditionalPath[] = options.extractConditionals
1492 | ? await this.extractConditionalPaths(
1493 | functionBody,
1494 | func.startLine,
1495 | analysis,
1496 | filePath,
1497 | projectPath,
1498 | options,
1499 | depth,
1500 | visited,
1501 | allFunctions,
1502 | analysisCache,
1503 | circularReferences,
1504 | unresolvedCalls,
1505 | analyzedFiles,
1506 | updateMaxDepth,
1507 | )
1508 | : [];
1509 | const exceptions: ExceptionPath[] = options.trackExceptions
1510 | ? this.extractExceptions(functionBody, func.startLine)
1511 | : [];
1512 |
1513 | // Build child nodes for each call
1514 | const calls: CallGraphNode[] = [];
1515 | for (const call of callExpressions) {
1516 | const childNode = await this.resolveAndBuildChildNode(
1517 | call,
1518 | filePath,
1519 | analysis,
1520 | projectPath,
1521 | options,
1522 | depth + 1,
1523 | visited,
1524 | allFunctions,
1525 | analysisCache,
1526 | circularReferences,
1527 | unresolvedCalls,
1528 | analyzedFiles,
1529 | updateMaxDepth,
1530 | );
1531 | if (childNode) {
1532 | calls.push(childNode);
1533 | }
1534 | }
1535 |
1536 | visited.delete(funcKey); // Allow revisiting from different paths
1537 |
1538 | return {
1539 | function: func,
1540 | location: {
1541 | file: filePath,
1542 | line: func.startLine,
1543 | },
1544 | calls,
1545 | conditionalBranches,
1546 | exceptions,
1547 | depth,
1548 | truncated: false,
1549 | isExternal: false,
1550 | };
1551 | }
1552 |
1553 | /**
1554 | * Resolve a function call and build its child node
1555 | */
1556 | private async resolveAndBuildChildNode(
1557 | call: { name: string; line: number; isMethod: boolean; object?: string },
1558 | currentFile: string,
1559 | currentAnalysis: ASTAnalysisResult,
1560 | projectPath: string,
1561 | options: Required<CallGraphOptions>,
1562 | depth: number,
1563 | visited: Set<string>,
1564 | allFunctions: Map<string, FunctionSignature>,
1565 | analysisCache: Map<string, ASTAnalysisResult>,
1566 | circularReferences: Array<{ from: string; to: string }>,
1567 | unresolvedCalls: Array<{
1568 | name: string;
1569 | location: { file: string; line: number };
1570 | }>,
1571 | analyzedFiles: string[],
1572 | updateMaxDepth: (depth: number) => void,
1573 | ): Promise<CallGraphNode | null> {
1574 | // First, try to find the function in the current file
1575 | let targetFunc = currentAnalysis.functions.find(
1576 | (f) => f.name === call.name,
1577 | );
1578 | let targetFile = currentFile;
1579 | let targetAnalysis = currentAnalysis;
1580 |
1581 | // Check class methods if it's a method call
1582 | if (!targetFunc && call.isMethod) {
1583 | for (const cls of currentAnalysis.classes) {
1584 | const method = cls.methods.find((m) => m.name === call.name);
1585 | if (method) {
1586 | targetFunc = method;
1587 | break;
1588 | }
1589 | }
1590 | }
1591 |
1592 | // If not found locally, try to resolve from imports
1593 | if (!targetFunc && options.resolveImports) {
1594 | const resolvedImport = await this.resolveImportedFunction(
1595 | call.name,
1596 | currentAnalysis.imports,
1597 | currentFile,
1598 | projectPath,
1599 | options.extensions,
1600 | analysisCache,
1601 | analyzedFiles,
1602 | );
1603 |
1604 | if (resolvedImport) {
1605 | targetFunc = resolvedImport.func;
1606 | targetFile = resolvedImport.file;
1607 | targetAnalysis = resolvedImport.analysis;
1608 | }
1609 | }
1610 |
1611 | if (!targetFunc) {
1612 | // Track as unresolved call (might be built-in or external library)
1613 | if (!this.isBuiltInFunction(call.name)) {
1614 | unresolvedCalls.push({
1615 | name: call.name,
1616 | location: { file: currentFile, line: call.line },
1617 | });
1618 | }
1619 | return null;
1620 | }
1621 |
1622 | return this.buildCallGraphNode(
1623 | targetFunc,
1624 | targetFile,
1625 | targetAnalysis,
1626 | projectPath,
1627 | options,
1628 | depth,
1629 | visited,
1630 | allFunctions,
1631 | analysisCache,
1632 | circularReferences,
1633 | unresolvedCalls,
1634 | analyzedFiles,
1635 | updateMaxDepth,
1636 | );
1637 | }
1638 |
1639 | /**
1640 | * Resolve an imported function to its source file
1641 | */
1642 | private async resolveImportedFunction(
1643 | funcName: string,
1644 | imports: ImportInfo[],
1645 | currentFile: string,
1646 | projectPath: string,
1647 | extensions: string[],
1648 | analysisCache: Map<string, ASTAnalysisResult>,
1649 | analyzedFiles: string[],
1650 | ): Promise<{
1651 | func: FunctionSignature;
1652 | file: string;
1653 | analysis: ASTAnalysisResult;
1654 | } | null> {
1655 | // Find the import that provides this function
1656 | for (const imp of imports) {
1657 | const importedItem = imp.imports.find(
1658 | (i) => i.name === funcName || i.alias === funcName,
1659 | );
1660 |
1661 | if (
1662 | importedItem ||
1663 | (imp.isDefault && imp.imports[0]?.name === funcName)
1664 | ) {
1665 | // Resolve the import path
1666 | const resolvedPath = await this.resolveImportPath(
1667 | imp.source,
1668 | currentFile,
1669 | projectPath,
1670 | extensions,
1671 | );
1672 |
1673 | if (!resolvedPath) continue;
1674 |
1675 | // Check cache first
1676 | let analysis: ASTAnalysisResult | undefined =
1677 | analysisCache.get(resolvedPath);
1678 | if (!analysis) {
1679 | const analyzedFile = await this.analyzeFile(resolvedPath);
1680 | if (analyzedFile) {
1681 | analysis = analyzedFile;
1682 | analysisCache.set(resolvedPath, analysis);
1683 | analyzedFiles.push(resolvedPath);
1684 | }
1685 | }
1686 |
1687 | if (!analysis) continue;
1688 |
1689 | // Find the function in the resolved file
1690 | const func = analysis.functions.find(
1691 | (f) => f.name === (importedItem?.name || funcName),
1692 | );
1693 | if (func) {
1694 | return { func, file: resolvedPath, analysis };
1695 | }
1696 |
1697 | // Check class methods
1698 | for (const cls of analysis.classes) {
1699 | const method = cls.methods.find(
1700 | (m) => m.name === (importedItem?.name || funcName),
1701 | );
1702 | if (method) {
1703 | return { func: method, file: resolvedPath, analysis };
1704 | }
1705 | }
1706 | }
1707 | }
1708 |
1709 | return null;
1710 | }
1711 |
1712 | /**
1713 | * Resolve an import path to an absolute file path
1714 | */
1715 | private async resolveImportPath(
1716 | importSource: string,
1717 | currentFile: string,
1718 | projectPath: string,
1719 | extensions: string[],
1720 | ): Promise<string | null> {
1721 | // Skip node_modules and external packages
1722 | if (
1723 | !importSource.startsWith(".") &&
1724 | !importSource.startsWith("/") &&
1725 | !importSource.startsWith("@/")
1726 | ) {
1727 | return null;
1728 | }
1729 |
1730 | const currentDir = path.dirname(currentFile);
1731 | let basePath: string;
1732 |
1733 | if (importSource.startsWith("@/")) {
1734 | // Handle alias imports (common in Next.js, etc.)
1735 | basePath = path.join(projectPath, importSource.slice(2));
1736 | } else {
1737 | basePath = path.resolve(currentDir, importSource);
1738 | }
1739 |
1740 | // Try with different extensions
1741 | const candidates = [
1742 | basePath,
1743 | ...extensions.map((ext) => basePath + ext),
1744 | path.join(basePath, "index.ts"),
1745 | path.join(basePath, "index.tsx"),
1746 | path.join(basePath, "index.js"),
1747 | ];
1748 |
1749 | for (const candidate of candidates) {
1750 | try {
1751 | await fs.access(candidate);
1752 | return candidate;
1753 | } catch {
1754 | // File doesn't exist, try next
1755 | }
1756 | }
1757 |
1758 | return null;
1759 | }
1760 |
1761 | /**
1762 | * Find the file containing a function
1763 | */
1764 | private async findFunctionFile(
1765 | funcName: string,
1766 | projectPath: string,
1767 | extensions: string[],
1768 | ): Promise<string | null> {
1769 | // Search common source directories
1770 | const searchDirs = ["src", "lib", "app", "."];
1771 |
1772 | for (const dir of searchDirs) {
1773 | const searchPath = path.join(projectPath, dir);
1774 | try {
1775 | const files = await this.findFilesRecursive(searchPath, extensions);
1776 | for (const file of files) {
1777 | const analysis = await this.analyzeFile(file);
1778 | if (analysis) {
1779 | const func = analysis.functions.find((f) => f.name === funcName);
1780 | if (func) return file;
1781 |
1782 | // Check class methods
1783 | for (const cls of analysis.classes) {
1784 | if (cls.methods.find((m) => m.name === funcName)) {
1785 | return file;
1786 | }
1787 | }
1788 | }
1789 | }
1790 | } catch {
1791 | // Directory doesn't exist, continue
1792 | }
1793 | }
1794 |
1795 | return null;
1796 | }
1797 |
1798 | /**
1799 | * Recursively find files with given extensions
1800 | */
1801 | private async findFilesRecursive(
1802 | dir: string,
1803 | extensions: string[],
1804 | maxDepth: number = 5,
1805 | currentDepth: number = 0,
1806 | ): Promise<string[]> {
1807 | if (currentDepth >= maxDepth) return [];
1808 |
1809 | const files: string[] = [];
1810 | try {
1811 | const entries = await fs.readdir(dir, { withFileTypes: true });
1812 |
1813 | for (const entry of entries) {
1814 | const fullPath = path.join(dir, entry.name);
1815 |
1816 | // Skip node_modules and hidden directories
1817 | if (
1818 | entry.name === "node_modules" ||
1819 | entry.name.startsWith(".") ||
1820 | entry.name === "dist" ||
1821 | entry.name === "build"
1822 | ) {
1823 | continue;
1824 | }
1825 |
1826 | if (entry.isDirectory()) {
1827 | files.push(
1828 | ...(await this.findFilesRecursive(
1829 | fullPath,
1830 | extensions,
1831 | maxDepth,
1832 | currentDepth + 1,
1833 | )),
1834 | );
1835 | } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
1836 | files.push(fullPath);
1837 | }
1838 | }
1839 | } catch {
1840 | // Directory access error
1841 | }
1842 |
1843 | return files;
1844 | }
1845 |
1846 | /**
1847 | * Extract the body of a function from source code
1848 | */
1849 | private extractFunctionBody(
1850 | content: string,
1851 | func: FunctionSignature,
1852 | ): string {
1853 | const lines = content.split("\n");
1854 | return lines.slice(func.startLine - 1, func.endLine).join("\n");
1855 | }
1856 |
1857 | /**
1858 | * Extract function call expressions from code
1859 | */
1860 | private extractCallExpressions(
1861 | code: string,
1862 | startLine: number,
1863 | ): Array<{ name: string; line: number; isMethod: boolean; object?: string }> {
1864 | const calls: Array<{
1865 | name: string;
1866 | line: number;
1867 | isMethod: boolean;
1868 | object?: string;
1869 | }> = [];
1870 |
1871 | try {
1872 | const ast = parseTypeScript(code, {
1873 | loc: true,
1874 | range: true,
1875 | tokens: false,
1876 | comment: false,
1877 | });
1878 |
1879 | const traverse = (node: any) => {
1880 | if (!node) return;
1881 |
1882 | if (node.type === "CallExpression") {
1883 | const callee = node.callee;
1884 | const line = (node.loc?.start.line || 0) + startLine - 1;
1885 |
1886 | if (callee.type === "Identifier") {
1887 | calls.push({
1888 | name: callee.name,
1889 | line,
1890 | isMethod: false,
1891 | });
1892 | } else if (callee.type === "MemberExpression") {
1893 | if (callee.property?.name) {
1894 | calls.push({
1895 | name: callee.property.name,
1896 | line,
1897 | isMethod: true,
1898 | object: callee.object?.name,
1899 | });
1900 | }
1901 | }
1902 | }
1903 |
1904 | for (const key in node) {
1905 | if (typeof node[key] === "object" && node[key] !== null) {
1906 | if (Array.isArray(node[key])) {
1907 | node[key].forEach((child: any) => traverse(child));
1908 | } else {
1909 | traverse(node[key]);
1910 | }
1911 | }
1912 | }
1913 | };
1914 |
1915 | traverse(ast);
1916 | } catch {
1917 | // Parse error, return empty
1918 | }
1919 |
1920 | return calls;
1921 | }
1922 |
1923 | /**
1924 | * Extract conditional paths from function body
1925 | */
1926 | private async extractConditionalPaths(
1927 | code: string,
1928 | startLine: number,
1929 | currentAnalysis: ASTAnalysisResult,
1930 | currentFile: string,
1931 | projectPath: string,
1932 | options: Required<CallGraphOptions>,
1933 | depth: number,
1934 | visited: Set<string>,
1935 | allFunctions: Map<string, FunctionSignature>,
1936 | analysisCache: Map<string, ASTAnalysisResult>,
1937 | circularReferences: Array<{ from: string; to: string }>,
1938 | unresolvedCalls: Array<{
1939 | name: string;
1940 | location: { file: string; line: number };
1941 | }>,
1942 | analyzedFiles: string[],
1943 | updateMaxDepth: (depth: number) => void,
1944 | ): Promise<ConditionalPath[]> {
1945 | const conditionals: ConditionalPath[] = [];
1946 |
1947 | try {
1948 | const ast = parseTypeScript(code, {
1949 | loc: true,
1950 | range: true,
1951 | tokens: false,
1952 | comment: false,
1953 | });
1954 |
1955 | const extractConditionString = (node: any): string => {
1956 | if (!node) return "unknown";
1957 | if (node.type === "Identifier") return node.name;
1958 | if (node.type === "BinaryExpression") {
1959 | return `${extractConditionString(node.left)} ${
1960 | node.operator
1961 | } ${extractConditionString(node.right)}`;
1962 | }
1963 | if (node.type === "MemberExpression") {
1964 | return `${extractConditionString(node.object)}.${
1965 | node.property?.name || "?"
1966 | }`;
1967 | }
1968 | if (node.type === "UnaryExpression") {
1969 | return `${node.operator}${extractConditionString(node.argument)}`;
1970 | }
1971 | if (node.type === "Literal") {
1972 | return String(node.value);
1973 | }
1974 | return "complex";
1975 | };
1976 |
1977 | const extractBranchCalls = async (
1978 | branchNode: any,
1979 | ): Promise<CallGraphNode[]> => {
1980 | if (!branchNode) return [];
1981 |
1982 | const branchCode =
1983 | branchNode.type === "BlockStatement"
1984 | ? code.slice(branchNode.range[0], branchNode.range[1])
1985 | : code.slice(
1986 | branchNode.range?.[0] || 0,
1987 | branchNode.range?.[1] || 0,
1988 | );
1989 |
1990 | const branchCalls = this.extractCallExpressions(
1991 | branchCode,
1992 | (branchNode.loc?.start.line || 0) + startLine - 1,
1993 | );
1994 | const nodes: CallGraphNode[] = [];
1995 |
1996 | for (const call of branchCalls) {
1997 | const childNode = await this.resolveAndBuildChildNode(
1998 | call,
1999 | currentFile,
2000 | currentAnalysis,
2001 | projectPath,
2002 | options,
2003 | depth + 1,
2004 | visited,
2005 | allFunctions,
2006 | analysisCache,
2007 | circularReferences,
2008 | unresolvedCalls,
2009 | analyzedFiles,
2010 | updateMaxDepth,
2011 | );
2012 | if (childNode) {
2013 | nodes.push(childNode);
2014 | }
2015 | }
2016 |
2017 | return nodes;
2018 | };
2019 |
2020 | const traverse = async (node: any) => {
2021 | if (!node) return;
2022 |
2023 | // If statement
2024 | if (node.type === "IfStatement") {
2025 | const condition = extractConditionString(node.test);
2026 | const line = (node.loc?.start.line || 0) + startLine - 1;
2027 |
2028 | conditionals.push({
2029 | type: "if",
2030 | condition,
2031 | lineNumber: line,
2032 | trueBranch: await extractBranchCalls(node.consequent),
2033 | falseBranch: await extractBranchCalls(node.alternate),
2034 | });
2035 | }
2036 |
2037 | // Switch statement
2038 | if (node.type === "SwitchStatement") {
2039 | const discriminant = extractConditionString(node.discriminant);
2040 |
2041 | for (const switchCase of node.cases || []) {
2042 | conditionals.push({
2043 | type: "switch-case",
2044 | condition: switchCase.test
2045 | ? `${discriminant} === ${extractConditionString(
2046 | switchCase.test,
2047 | )}`
2048 | : "default",
2049 | lineNumber: (switchCase.loc?.start.line || 0) + startLine - 1,
2050 | trueBranch: await extractBranchCalls(switchCase),
2051 | falseBranch: [],
2052 | });
2053 | }
2054 | }
2055 |
2056 | // Ternary operator
2057 | if (node.type === "ConditionalExpression") {
2058 | const condition = extractConditionString(node.test);
2059 | const line = (node.loc?.start.line || 0) + startLine - 1;
2060 |
2061 | conditionals.push({
2062 | type: "ternary",
2063 | condition,
2064 | lineNumber: line,
2065 | trueBranch: await extractBranchCalls(node.consequent),
2066 | falseBranch: await extractBranchCalls(node.alternate),
2067 | });
2068 | }
2069 |
2070 | for (const key in node) {
2071 | if (typeof node[key] === "object" && node[key] !== null) {
2072 | if (Array.isArray(node[key])) {
2073 | for (const child of node[key]) {
2074 | await traverse(child);
2075 | }
2076 | } else {
2077 | await traverse(node[key]);
2078 | }
2079 | }
2080 | }
2081 | };
2082 |
2083 | await traverse(ast);
2084 | } catch {
2085 | // Parse error
2086 | }
2087 |
2088 | return conditionals;
2089 | }
2090 |
2091 | /**
2092 | * Extract exception paths (throw statements)
2093 | */
2094 | private extractExceptions(code: string, startLine: number): ExceptionPath[] {
2095 | const exceptions: ExceptionPath[] = [];
2096 |
2097 | try {
2098 | const ast = parseTypeScript(code, {
2099 | loc: true,
2100 | range: true,
2101 | tokens: false,
2102 | comment: false,
2103 | });
2104 |
2105 | // Track try-catch blocks to determine if throws are caught
2106 | const catchRanges: Array<[number, number]> = [];
2107 |
2108 | const collectCatchBlocks = (node: any) => {
2109 | if (!node) return;
2110 |
2111 | if (node.type === "TryStatement" && node.handler) {
2112 | const handlerRange = node.handler.range || [0, 0];
2113 | catchRanges.push(handlerRange);
2114 | }
2115 |
2116 | for (const key in node) {
2117 | if (typeof node[key] === "object" && node[key] !== null) {
2118 | if (Array.isArray(node[key])) {
2119 | node[key].forEach((child: any) => collectCatchBlocks(child));
2120 | } else {
2121 | collectCatchBlocks(node[key]);
2122 | }
2123 | }
2124 | }
2125 | };
2126 |
2127 | const traverse = (node: any, inTryBlock = false) => {
2128 | if (!node) return;
2129 |
2130 | if (node.type === "TryStatement") {
2131 | traverse(node.block, true);
2132 | if (node.handler) traverse(node.handler.body, false);
2133 | if (node.finalizer) traverse(node.finalizer, false);
2134 | return;
2135 | }
2136 |
2137 | if (node.type === "ThrowStatement") {
2138 | const line = (node.loc?.start.line || 0) + startLine - 1;
2139 | const argument = node.argument;
2140 | let exceptionType = "Error";
2141 | let expression = "unknown";
2142 |
2143 | if (argument?.type === "NewExpression") {
2144 | exceptionType = argument.callee?.name || "Error";
2145 | expression = `new ${exceptionType}(...)`;
2146 | } else if (argument?.type === "Identifier") {
2147 | exceptionType = argument.name;
2148 | expression = argument.name;
2149 | } else if (argument?.type === "CallExpression") {
2150 | exceptionType = argument.callee?.name || "Error";
2151 | expression = `${exceptionType}(...)`;
2152 | }
2153 |
2154 | exceptions.push({
2155 | exceptionType,
2156 | lineNumber: line,
2157 | expression,
2158 | isCaught: inTryBlock,
2159 | });
2160 | }
2161 |
2162 | for (const key in node) {
2163 | if (
2164 | key !== "handler" &&
2165 | key !== "finalizer" &&
2166 | typeof node[key] === "object" &&
2167 | node[key] !== null
2168 | ) {
2169 | if (Array.isArray(node[key])) {
2170 | node[key].forEach((child: any) => traverse(child, inTryBlock));
2171 | } else {
2172 | traverse(node[key], inTryBlock);
2173 | }
2174 | }
2175 | }
2176 | };
2177 |
2178 | collectCatchBlocks(ast);
2179 | traverse(ast);
2180 | } catch {
2181 | // Parse error
2182 | }
2183 |
2184 | return exceptions;
2185 | }
2186 |
2187 | /**
2188 | * Check if a function name is a built-in JavaScript function
2189 | */
2190 | private isBuiltInFunction(name: string): boolean {
2191 | const builtIns = new Set([
2192 | // Console
2193 | "log",
2194 | "warn",
2195 | "error",
2196 | "info",
2197 | "debug",
2198 | "trace",
2199 | "table",
2200 | // Array methods
2201 | "map",
2202 | "filter",
2203 | "reduce",
2204 | "forEach",
2205 | "find",
2206 | "findIndex",
2207 | "some",
2208 | "every",
2209 | "includes",
2210 | "indexOf",
2211 | "push",
2212 | "pop",
2213 | "shift",
2214 | "unshift",
2215 | "slice",
2216 | "splice",
2217 | "concat",
2218 | "join",
2219 | "sort",
2220 | "reverse",
2221 | "flat",
2222 | "flatMap",
2223 | // String methods
2224 | "split",
2225 | "trim",
2226 | "toLowerCase",
2227 | "toUpperCase",
2228 | "replace",
2229 | "substring",
2230 | "substr",
2231 | "charAt",
2232 | "startsWith",
2233 | "endsWith",
2234 | "padStart",
2235 | "padEnd",
2236 | "repeat",
2237 | // Object methods
2238 | "keys",
2239 | "values",
2240 | "entries",
2241 | "assign",
2242 | "freeze",
2243 | "seal",
2244 | "hasOwnProperty",
2245 | // Math methods
2246 | "max",
2247 | "min",
2248 | "abs",
2249 | "floor",
2250 | "ceil",
2251 | "round",
2252 | "random",
2253 | "sqrt",
2254 | "pow",
2255 | // JSON
2256 | "stringify",
2257 | "parse",
2258 | // Promise
2259 | "then",
2260 | "catch",
2261 | "finally",
2262 | "resolve",
2263 | "reject",
2264 | "all",
2265 | "race",
2266 | "allSettled",
2267 | // Timers
2268 | "setTimeout",
2269 | "setInterval",
2270 | "clearTimeout",
2271 | "clearInterval",
2272 | // Common
2273 | "require",
2274 | "import",
2275 | "console",
2276 | "Date",
2277 | "Error",
2278 | "Promise",
2279 | "fetch",
2280 | ]);
2281 | return builtIns.has(name);
2282 | }
2283 |
2284 | /**
2285 | * Create an empty call graph for when entry function is not found
2286 | */
2287 | private createEmptyCallGraph(entryFunction: string): CallGraph {
2288 | return {
2289 | entryPoint: entryFunction,
2290 | root: {
2291 | function: {
2292 | name: entryFunction,
2293 | parameters: [],
2294 | returnType: null,
2295 | isAsync: false,
2296 | isExported: false,
2297 | isPublic: true,
2298 | docComment: null,
2299 | startLine: 0,
2300 | endLine: 0,
2301 | complexity: 0,
2302 | dependencies: [],
2303 | },
2304 | location: { file: "unknown", line: 0 },
2305 | calls: [],
2306 | conditionalBranches: [],
2307 | exceptions: [],
2308 | depth: 0,
2309 | truncated: false,
2310 | isExternal: true,
2311 | },
2312 | allFunctions: new Map(),
2313 | maxDepthReached: 0,
2314 | analyzedFiles: [],
2315 | circularReferences: [],
2316 | unresolvedCalls: [
2317 | {
2318 | name: entryFunction,
2319 | location: { file: "unknown", line: 0 },
2320 | },
2321 | ],
2322 | buildTime: new Date().toISOString(),
2323 | };
2324 | }
2325 |
2326 | /**
2327 | * Create a truncated node when max depth is reached or circular reference detected
2328 | */
2329 | private createTruncatedNode(
2330 | func: FunctionSignature,
2331 | filePath: string,
2332 | depth: number,
2333 | _isCircular: boolean,
2334 | ): CallGraphNode {
2335 | return {
2336 | function: func,
2337 | location: {
2338 | file: filePath,
2339 | line: func.startLine,
2340 | },
2341 | calls: [],
2342 | conditionalBranches: [],
2343 | exceptions: [],
2344 | depth,
2345 | truncated: true,
2346 | isExternal: false,
2347 | };
2348 | }
2349 | }
2350 |
```